Appearance
Lua bot services
For the complete IDA-recovered packet catalog and subscription API, see PROTOCOL-EVENTS.md.
The SDK now exposes the reusable parts normally buried inside a CaveBot or TargetBot. Scripts can compose these services without competing for movement, combat, healing, inventory, NPC, or communication ownership.
Editor completion and API contracts
scripts/sdk/_types.lua is the LuaLS/EmmyLua reference for the public API. It documents native game.* calls, Player/Inventory/Tile records, service constructors, coordinate limits, tri-state reads, and nil, error contracts. The file is metadata-only and has no runtime side effects.
Protection zones
lua
local Engine = require("sdk.engine")
local in_pz = Engine.IsInProtectionZone()The result is tri-state. true and false are authoritative answers; nil means neither the latest server state nor the mapped tile can answer yet. The same value is available from player:isInProtectionZone() and player:isInPz(). PZ lock remains separate as player:isPzLocked().
Login state and reconnect
game.session remains a credential-free Lua facade over Tibia's official login state machine. It never accepts or returns an account name, password, authenticator, session key, selected-character pointer, or raw login packet. The planner's Run control → Automatic login & relogin card is a separate native-only service: it saves one email/password/character profile as a Windows-DPAPI protected blob in Documents\SharpTib\state\auto-login.dat. The protected password never enters a hunt plan, config.json, Lua, runtime status, logs, incident reports, or browser storage.
lua
local Engine = require("sdk.engine")
local state = assert(Engine.GetSessionState())
if state.phase == "disconnected" and state.can_reconnect then
local ok, status_or_error = Engine.Reconnect()
if not ok then print(status_or_error) end
endA successful request means the action was queued on Tibia's Qt thread. It is not server acknowledgement; wait for WorldEntered or state.in_world. Native calls are idempotent while an attempt is pending and enforce a five second retry cooldown.
The official client itself persists only loginEmailAddress (and rememberLoginEmail). game.session.reconnect() therefore still requires an official in-memory secret. When the planner auto-login profile is enabled, the native service can restore the protected password after a process restart, submit it through Tibia's official account controller, wait for the official character model, verify the configured name exists, and select it. Mid-session disconnects prefer Tibia's retained-session reconnect path before falling back to a fresh credential login.
The native profile retries login failures indefinitely every 60 seconds, which covers server maintenance and temporary authentication/game-server outages. The same profile can be configured in the normal overlay under CONFIG → Automatic login, so the recovery service works for market bots and other loaded scripts even when the web planner is closed. If CaveBot was running when the disconnect occurred, its running intent is snapshotted and one new planner start command is issued after WorldEntered; a session that was paused or stopped is not started.
Automatic recovery is opt-in and only arms after this process has observed a real in-world session:
lua
local Session = require("sdk.session")
assert(Session.start({
initial_delay_ms = 1000,
max_delay_ms = 30000,
multiplier = 2,
}))LoginWait suppresses retries and, when the official object carries a wait time, holds the native pending window to that delay instead of a fixed five minutes. A new LoginError pauses automatic recovery until Session.resume() explicitly acknowledges it. Dead never counts as a disconnect. Use Session.stop() during script teardown to release the tick subscription.
Authentication lifecycle events remain subscribable by name and type, but their hex field is intentionally empty. This prevents challenge values and internal session pointers from leaking to wildcard event consumers.
Typed inbound events
Engine.on(name, callback) decodes the following server messages before the callback runs:
- Creature, map, target, inventory, cooldown, player-state, and player-data messages already supported by the SDK.
Dead,PartyHuntAnalyser,FullMap, andFieldData.- Full container state plus
CloseContainerand create/change/delete deltas. GraphicalEffectsandRemoveGraphicalEffect.UnjustifiedPoints,ItemWasted,ItemLooted, andKillTracking.
Every other inbound catalog type still reaches Lua through Engine.on(name) or Engine.onAny with named schema fields (ev.wait_time, ev.fields). Engine.help(name) / Packet.help(name) print subscribe/send usage plus every recovered field. Engine.Schema(name) and Packet.fields(name) return the row.
The native receive hook snapshots deferred payloads before returning to the client. Lua never receives a borrowed pointer whose message has already been destroyed. Polling consumers can use:
lua
local latest = Engine.GetEventState("ItemLooted")
local one = Engine.GetContainerState(3)
local all = Engine.GetContainerStates()Some complex nested client records are intentionally exposed only when their layout is proven. Missing fields mean unavailable, not zero.
Action broker
All mutating automation should go through sdk.action_broker. It arbitrates the movement, combat, healing, inventory, interaction, and communication channels, supports atomic multi-channel leases, and cancels stale work across session generations.
lua
local Broker = require("sdk.action_broker")
local owner = assert(Broker.owner("my-script", { heartbeat_ms = 3000 }))
local ticket = assert(owner:submit({
key = "heal:spell",
channel = "healing",
priority = Broker.priorities.HEALING,
send = function() return player:useSkill("exura") end,
ack = Broker.ack.spell("exura"),
}))The SDK owns one global tick pump. Consumer scripts must not call Broker.pump() in production. Heartbeat the owner while active, cancel tickets on pause, and release the owner on stop. A ticket is successful only when its terminal snapshot says completed; packet acceptance alone is not completion.
Canonical priorities are EMERGENCY, RECOVERY, USER, HEALING, COMBAT, LOOT, SUPPORT, MOVEMENT, and BACKGROUND.
Behavior Trees
sdk.cavebot.bt is the reusable decision layer used by CaveBot. Nodes return exactly BT.Status.SUCCESS, BT.Status.FAILURE, or BT.Status.RUNNING. Selectors provide ordered fallback, sequences express prerequisites, and an ordered parallel node lets independent branches run without hiding their authored priority. Every node has a stable id, recursive halt/reset, and runner diagnostics with active paths and contained tracebacks.
lua
local BT = require("sdk.cavebot.bt")
local S = BT.Status
local attack = BT.reactive_selector({
id = "combat.attack", name = "Attack fallback", children = {
BT.action({ id = "combat.best", name = "Use best spell",
run = function(bb) return can_best(bb) and cast_best() and S.SUCCESS or S.FAILURE end }),
BT.action({ id = "combat.secondary", name = "Use secondary spell",
run = function(bb) return can_secondary(bb) and cast_secondary() and S.SUCCESS or S.FAILURE end }),
BT.action({ id = "combat.basic", name = "Basic attack",
run = function(bb) return attack_target(bb) and S.RUNNING or S.FAILURE end }),
},
})
local runner = BT.new(attack, {
now_ms = function() return game.log.now_ms() end,
log = function(level, message, fields) game.log.console(message) end,
})Available composites are reactive_sequence, memory_sequence, reactive_selector, memory_selector, and ordered_parallel. Decorators are guard, inverter, succeed, repeater, and cooldown. BT.validate(root) rejects duplicate IDs, cycles, shared stateful node instances, invalid arity, and malformed options before execution. runner:snapshot() exposes node visit counts, last status/timing, active paths, and recent errors.
Actions that send game input still use sdk.action_broker; a BT does not replace acknowledgement or lease ownership. A brokered action remains RUNNING until its ticket completes, and its on_halt must cancel/release the pending work. CaveBot's web plans remain semantic schema-v1 documents; they do not serialize arbitrary BT topology or Lua functions.
Validated plans cross an immutable CompiledPlanV1 boundary before graph or tree code can read them. Every schema-v1 field is classified as actuating, metadata-only, or an explicit capability; unknown fields and envelope ID/version/hash mismatches are rejected without replacing the last safe plan. Runtime revision 29 also checkpoints plan-bound graph labels, Recovery/Stop state, analytics baselines, and quarantine signatures. Only action-free, branch-free graph labels with no pending ticket or delay are durable; raw action indexes and variables are never replayed. Restore requires the same plan/compiler/tree identity, character, floor, nearby position, and a fresh timestamp; otherwise CaveBot starts clean.
Runtime revision 30 adds generated world journeys. AutomaticOutbound and AutomaticReturn reuse sdk.world_navigation for bounded off-thread static paths, then execute exact NPC travel legs through sdk.action_broker. The journey exposes its phase, target, NPC leg, navigation status, elapsed time, error, and cleanup state in bot:status(), MCP, trace context, and the in-game Behavior Tree panel. Start only arms the service; routing, broker acquisition, and NPC interaction occur on later winning BT ticks.
The running CaveBot also exposes read-mostly diagnostics:
lua
local CaveBot = require("sdk.cavebot")
local snapshot = CaveBot.debug.full_snapshot()
local trace = CaveBot.debug.recent_trace(32)
local validation = CaveBot.debug.validate_tree()full_snapshot contains the hierarchy, active paths, node timing/counters, fallbacks, resource claims, quarantine, cleanup/critical failures, controller state, plan, tree generation, route test, target, and StopGuard state. Trace entries retain their plan hash and generation together with active node paths, graph label/action position, halt reason, and the relevant error/quarantine context. The explicitly mutating debug calls are dump_trace(reason) and reset_quarantine(name); debug code cannot replace topology or invoke arbitrary actions. MCP clients can use the read-only inspect_cavebot_tree tool. The in-game CAVEBOT → Behavior Tree panel renders the same snapshot with live status colors, plan provenance, structured blockers, per-node halt reasons, and warnings. Its bounded compact payload carries up to 256 nodes, which includes the complete default CaveBot tree while retaining an explicit truncation flag for larger custom trees.
Combat geometry and safety
sdk.combat_shapes contains the exact client spell/rune shapes, including beams, waves, square waves, circles, and all eight directions. sdk.combat_safety validates a cast again at broker execution time. It can protect the local player, friends, party members, summons, secure-mode targets, PZ tiles, skull limits, and unjustified-kill budgets.
CaveBot directional attacks use those exact shapes as a stricter rotation gate. Immediately before the broker sends, the selected target must still have the same authoritative ID, be alive and on-map in a fresh creature snapshot, remain in live range, and satisfy the configured minimum hit count in one of the four server-turnable cardinal directions. Failure falls through to the next spell or basic attack without turning or casting.
lua
local Safety = require("sdk.combat_safety").new({
friends = { "Trusted Knight" },
pvp = { protect_all_aoe_players = true },
})
local ok, decision = Safety:can({
spell = "exevo gran mas vis",
target = target,
}, { player = player })
if not ok then
print(decision.code, decision.message)
endUnknown direction, incomplete player visibility, or unknown target identity is denied when a strict safety decision requires it.
Containers
sdk.container_service maintains event-driven container generations, stable physical identities, roles, pagination state, O(1) item indexes, and bounded BFS discovery.
lua
local Containers = assert(require("sdk.container_service").new())
local hit = Containers:find(3160)
local total = Containers:count(3160)
local snapshot = Containers:snapshot()Open, close, parent, and page operations are brokered and acknowledged from typed container events. Page seeking fails closed if the client category is unknown. Watch notifications are drained on tick rather than called from a native receive callback.
Rules, healing, and combat rotation
sdk.rule_engine supplies deterministic ordered predicates, enable/disable, cooldowns, diagnostics, and snapshots. sdk.healing_service builds on it for self healing, cures, potions, and defensive buffs. sdk.combat_rotation handles ordered spells/runes, resource thresholds, exact geometry, and the combat-safety guard.
CaveBot uses the shared healing service and combat safety. The cursor orbwalker uses the same broker and safety policy, so the two modules no longer issue conflicting actions.
Party assistance
sdk.party_assist follows a trusted leader by ID or name through the SDK path and navigator APIs. It does not use the client's follow packet, so target combat can continue. It supports catch-up distance, maximum distance, same-floor guards, and a bounded last-known position.
Optional friend healing is restricted to configured friends/party identities, respects a local-player health floor, and runs below self-healing priority.
Unified tiles and CaveBot actions
sdk.tile combines static world flags/state/cost with the live item stack, creatures, effects, and catalog data:
lua
local tile = require("sdk.tile").at({ x = 32369, y = 32241, z = 7 })
if tile:is_complete() and tile:pathable() then
print(tile:in_protection_zone(), tile:floor_change(), tile:door())
endsdk.cave_action_graph validates, compiles, and executes data-only checkpoint graphs. Supported actions are goto, delay, check_supplies, check_capacity, tool, npc, depot, stash, bank, travel, and transition, interact, cast_spell, use_item, equip, unequip, and callback. Branches support supply/capacity checks, variables, and registered callbacks. Spell and inventory actions wait for the matching server/inventory acknowledgement before the graph advances.
lua
local Cave = require("sdk.cave_action_graph")
Cave.register("route.prepare", function(kind, args, context)
if kind == "condition" then return context.capacity >= args.minimum end
return true
end)
local graph = assert(Cave.new({
entry = "start",
nodes = {
{
label = "start", x = 100, y = 200, z = 7,
actions = {
{ type = "check_capacity", minimum = 100,
then_label = "hunt", else_label = "depot" },
},
},
{ label = "hunt", x = 110, y = 210, z = 7 },
{ label = "depot", x = 90, y = 190, z = 7,
actions = { { type = "depot" } } },
},
}))An authored danger segment can equip at one checkpoint and empty the slot at a later checkpoint. Equipment slot 9 is the ring slot. Item target self is for potions/runes; use invokes the item directly (for example food).
lua
{ label = "venom", x = 100, y = 200, z = 7, actions = {
{ type = "cast_spell", words = "exana pox" },
{ type = "equip", item_id = 3049 }, -- stealth ring
} },
{ label = "safe", x = 120, y = 200, z = 7, actions = {
{ type = "unequip", slot = 9 },
} },To use the potion alternative, replace the cast_spell row with { type = "use_item", item_id = 7644, target = "self" }.
Raw code, lua, script, source, and function fields are rejected. Custom behavior must reference a registered callback name. Pause cancels an in-flight graph ticket; session changes invalidate its generation. CaveBot runs checkpoint actions before normal hunt arbitration. The live-map planner can edit, validate, persist, and simulate the same schema.
Hunt analytics
sdk.hunt_analytics uses typed server events rather than chat parsing:
lua
local Analytics = require("sdk.hunt_analytics")
assert(Analytics.start("manual hunt"))
local metrics = Analytics.snapshot()
print(metrics.kills_per_hour, metrics.xp_per_hour, metrics.balance)
print(Analytics.summary())It tracks lifecycle time, XP gained/lost, kills and monster breakdown, item loot/waste occurrences, party analyser totals, deaths, and terminal broker actions. Values are priced only when the event supplies an explicit value or the party analyser supplies an authoritative total. Completeness metadata marks unknown item quantity/value instead of estimating it. No analytics are sent to a network endpoint.
Monster telemetry
sdk.monster_telemetry is passive. It samples bounded creature snapshots and learns movement distance, direction changes, health loss, reported/inferred speed, preferred distance, and closing/opening ratios. KillTracking is the authoritative kill source.
Each active and species snapshot reports a descriptive behavior (static, chaser, kiter, ranged, erratic, or mobile) plus confidence and its evidence ratios. It never moves, attacks, or claims that an unattributed visual effect was a monster spell.
Alerts
Windows builds expose:
lua
game.alert.available()
game.alert.foreground()
game.alert.beep("warning")
game.alert.flash(3)sdk.alert_service adds low-HP/mana hysteresis, damage/death/disconnect, player/creature detection with a friend allow-list, private messages, literal custom-message matching, cooldowns, bounded history, and watchers. Native events are queued and drained on tick before watchers run. Beep/flash is optional; headless/test builds retain history and callbacks with inert native stubs.
Conditional equipment
sdk.equipment_rules selects the first matching ordered loadout rule. It supports all, any, and not, HP/mana thresholds, tri-state PZ, player states, nearby monster/player counts, and exact/substring target names. Stability time, threshold hysteresis, and minimum switch time prevent flapping.
The service never equips or moves an item itself. It calls the existing brokered 20_equipment_loadouts.lua apply API and, in that integration, waits until the resulting job is confirmed before marking the rule active. Automatic rules are off by default:
lua
Loadouts.configure({
auto_enabled = true,
rules = {
{ loadout = "Emergency", when = { hp_below = 30 } },
{ loadout = "Town", when = { pz = true } },
{ loadout = "Default", when = {} },
},
})Lifecycle checklist
- Start services after the character/session is available.
- Treat
nilstate and incomplete snapshots as unknown. - Keep one action-broker owner per running script instance.
- On pause, cancel pending work but preserve resumable state where intended.
- On
WorldEntered,SessionEndInformation,SessionEnd, orDead, reset the service according to its documented lifecycle. - Call
stop()on hot reload so subscriptions, tickets, and owners are released.