Skip to main content

Cookbook

Complete working setups. Each recipe is a libscript plugin, usually paired with a libstrat strategy, derived from the example files that ship with your installation — not from invented code.

Start here if you would rather adapt something that already runs than build one from scratch.

The recipes

RecipeWhat it doesWhat it assumes
Farm loopFind a named mob, pull it, let the strategy win, recover, repeatOne account, one zone, a mob you can beat repeatedly
GardeningPlant a seed in every empty plot in a gardenYou are standing in the house, garden loaded, seeds in a favourites slot
Support teamTwo accounts in one fight: one buffs, one hitsTwo clients hooked as p1 and p2, in the same zone. Console only — see the recipe.
Drop trackingRecord what a farm run yields to a JSONL file and mine it afterwardsAny of the above already running

They build on each other in that order. Farm loop is the one to read first even if you want one of the others — it is where the plugin-and-strategy handoff is explained in full.

Two things the cookbook deliberately does not cover, because no shipped example does them reliably yet: following another character across a zone boundary, and restocking potions from a vendor. Both are noted where they come up.

The shape they share

Every long-running plugin here follows the same skeleton, for reasons covered in Ticks and timing:

---@type KebabPlugin
plugin = { name = 'x', version = '1.0.0' }

local ticks = utils.Ticks
local state = {}

local function log(msg) utils.Log('[x] ' .. msg) end
local function log_throttled(k, msg) utils.LogThrottled('x_' .. k, '[x] ' .. msg, 15000) end

local function reset()
state = { enabled = true, phase = 'start', wait = 0 }
end

local function ready_client()
local c = clients:First()
if not c or not c:IsConnected() then return nil end
if c:IsLoading() then return nil end
return c
end

local function tick()
if not state.enabled then return end
local c = ready_client()
if not c then return end
if state.wait and state.wait > 0 then
state.wait = state.wait - 1
return
end
-- dispatch on state.phase
end

plugin.on_load = function() reset() end
plugin.on_tick = tick
plugin.on_stop = function() state.enabled = false end
plugin.hotkeys = {
f8 = function() state.enabled = not state.enabled end,
}

Five things that are not optional if you want the thing to run unattended:

  1. An enabled flag, checked first, flipped by the hotkey and by on_stop.
  2. A readiness gate so no other code has to handle a missing or loading client.
  3. A single wait counter, checked before dispatch, so any phase can pause the machine.
  4. Throttled logging — four ticks a second will bury your log output otherwise.
  5. A pause hotkey, so you can freeze a misbehaving bot without tearing down your whole session.

Note the ticks alias at the top. Every duration in these recipes is written as ticks(seconds), never as a raw tick count, so the plugin follows the host if its tick rate ever changes.

Before you run any of these

Put the files where both hosts look

mkdir -p ~/.kebab/plugins ~/.kebab/strategies

Plugin .lua files go in the first, strategy .yml/.yaml files in the second. The desktop app always reads those two folders. The console prefers whatever you passed as --plugins-dir and --strategy (or set in its config file), and falls back to ~/.kebab/plugins and ~/.kebab/strategies only when those are unset and the folders already exist. If the console does not see your plugin, check which directory it actually loaded: scripts prints the plugin directory at the top of its listing, and doctor prints the strategies directory along with a count of validation errors in it.

Loading is not running

A plugin that loads sits at status stopped. Nothing calls on_load or on_tick until you start it: Run on the plugin's row in the desktop app, or scripts run <name> in the console. scripts run is what starts a plugin — for a plugin that also defines execute, like farm_tw, running it again re-runs execute, which is how those plugins implement "restart".

A strategy's name is its name: field

The runner looks a strategy up by the name: inside the file, never by the filename. The shipped blade_fire.yml declares name: blade-fire, and that hyphenated form is what enable combat wants.

Duplicate names overwrite silently

Two files in the strategies folder with the same name: do not produce an error. One wins, the other is discarded, and nothing tells you which. When you copy a shipped strategy as a starting point, change the name: in the same edit.

Validate the folder, not the file

Loading the strategies directory is all-or-nothing: one file that fails to parse blocks every strategy in the directory, not just the bad one. In the console this is fatal at startup — a single broken file and the console will not start at all, whether the folder came from --strategy or from ~/.kebab/strategies. In the desktop app it surfaces as a warning in the logs and an empty strategy list.

hook
scripts
strategies validate

hook attaches to running clients and assigns the p1, p2 aliases. scripts lists the plugins that loaded. strategies validate is what tells you which file is at fault.

Validation does not catch a mistyped action

An action written as a bare word that libstrat does not recognise — - pss instead of - pass — parses with no error and resolves to nothing. The file loads, the phase reports one action, and the strategy does nothing on that branch. If a strategy validates but never acts, re-read the action names first. Mistyped action keys in mapping form (- cst: {}) are caught properly.

Hotkeys may not bind

Every recipe binds f8 to pause. In the console, plugin hotkeys are registered globally, and two things stop that working: the name is already claimed by the quest-teleport hotkeys (g, k, x), or the process cannot read the keyboard devices — on Linux that means access to /dev/input/event*. The console logs a warning naming the key it could not bind. You can always fire the callback by hand instead:

scripts hotkey f8

What ships with your installation

FileKindNotes
farm_tw.luaPluginThe farm loop recipe. Potions, wisps, zone sweep.
farm_sm.luaPluginA farm loop that recovers through death, with drop logging.
garden69.luaPluginThe gardening recipe. Pure UI automation, no combat.
farm_oyo.lua, kat_farm.luaPluginFixed-route farm loops for specific zones.
storm_cp.yml, storm_cp_sm.ymlStrategyOne-line trash clearers.
storm.yml, storm_quest.yml, balance.ymlStrategySingle-character strategies with blades and enchants.
life_fire.yml, blade_fire.ymlStrategyTwo-character team strategies. The support team recipe.

Copy one under a new name before you edit it — for a strategy, change its name: field in the same edit — so that refreshing the examples can never clobber your version.