Skip to content

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 is a credential-free 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.

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
end

A 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 persists only loginEmailAddress (and rememberLoginEmail). The password is never written to disk. After a process restart you type it again in the official login dialog; state.has_credentials stays false until that in-memory secret exists. can_reconnect is false in that case, and reconnect() returns password required instead of submitting an empty password. Mid-session disconnect can still reconnect because the typed password or later session/token strings remain in RAM.

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, and FieldData.
  • Full container state plus CloseContainer and create/change/delete deltas.
  • GraphicalEffects and RemoveGraphicalEffect.
  • UnjustifiedPoints, ItemWasted, ItemLooted, and KillTracking.

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.

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.

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)
end

Unknown 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.

Farmer 09 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())
end

sdk.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 callback. Branches support supply/capacity checks, variables, and registered callbacks.

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" } } },
	},
}))

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. Farmer 09 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 nil state 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, or Dead, reset the service according to its documented lifecycle.
  • Call stop() on hot reload so subscriptions, tickets, and owners are released.

Arcane SDK catalog generated from scripts/sdk