Skip to content

Script developer guide ​

Runtime: Lua 5.4 · Host: Arcane overlay · Workspace: Documents\SharpTib\scripts · Packets: official GameclientMessage* via the RTTI catalog

Players: how to use scripts. Publish: upload to the Community Store. Packet fallback: build from zero. Generated API: modules, events, changelog, SDK zip.

What you are building on ​

Scripts run in the injected client. Prefer require("sdk.*"). game and game.dbg remain the native kernel and research/fallback surface.

  • Lua 5.4 + sol2, crash reports, timed/cancelled coroutines
  • Overlay Scripts tab for load / reload / logs / quarantine
  • Game MCP on 127.0.0.1:38472, seeded into the Documents scripts folder
  • Packet send by RTTI name, not hardcoded client RVAs

Look up GameclientMessageTalk / TalkMessage. Do not paste 0x4ECC90 into a new script. Those numbers move every client update.

Default method: sdk.engine, LocalPlayer, Inventory, Npc, Data, Path, Kite, Navigator, farm_planner, typed events. Use sdk.input for official TGameAction* (mount, attack-next, SetTactics). Use sdk.keys for "Ctrl+Shift+K" combos with game.input_snapshot / game.on("key_down"). Use sdk.packet for an uncovered protocol op. Use raw game.dbg only as the last Lua-level fallback.

Overlay while developing ​

Main menu X leaves Lua HUDs open. Insert hides every overlay surface. Pause combat with game.overlay.mouse_captured(), not “is the overlay visible?”.

Development roadmap ​

  1. Inject Arcane. Overlay → Scripts → Open folder (Documents\SharpTib\scripts).
  2. Open that folder as the Cursor / VS Code / Claude / Codex workspace.
  3. Restart MCP so Game MCP (game-mcp) appears.
  4. Start from 01_hello.lua or a 10-line print probe.
  5. Confirm logs, then add one SDK call or one packet.
  6. Resolve packets with game.dbg.packet("TalkMessage").
  7. Write p.va into the object. Send with game.dbg.enqueue(p, queue, msg).
  8. Nested objects use game.dbg.vtable("ObjectPosition") — they are not packets. Tag: 1=worldmap, 2=inventory, 3=container.
  9. If the catalog has no row, ask for the IDA GameclientMessage* name. Do not invent an RVA.

Workspace and MCP ​

Live folder:

text
%USERPROFILE%\Documents\SharpTib\scripts

Store modules: scripts\store\<uuid>\filename.lua — edit in place.
Backups: scripts\backups\ — not auto-loaded.

Arcane is easier than Sharp Mist for editors: the game process is Game MCP. No Node sidecar for Cursor, VS Code, Claude Code, Codex, or Gemini.

On every inject it writes (and refreshes if the file still contains game-mcp, the old arcane key, or the older sharp-tibia key):

ClientFile
Cursor.cursor/mcp.json
VS Code Copilot.vscode/mcp.json
Claude Code.mcp.json
Codex Desktop / CLI / IDE.codex/config.toml
Gemini CLI.gemini/settings.json

Dummy-user setup: inject → open the Documents scripts folder → reload Game MCP.

Optional Node stdio bridge (only if an editor cannot use HTTP):

text
node %USERPROFILE%\Documents\SharpTib\scripts\.arcane\mcp-stdio.js

Game MCP tools include list_functions, describe_function, list_types, describe_type, list_scripts, execute_lua, validate_lua, reload_scripts. execute_lua is owned as mcp/execute_lua.

Open the Documents scripts folder for modules (ui_*, examples). The hunter is CaveBot in scripts/sdk/cavebot. The Lua SDK is scripts/sdk in the Sharp-NewTibia repo; on the author machine Documents\SharpTib\scripts\sdk is a junction to that same folder. Do not keep a second SDK copy. Do not open %APPDATA%\SharpTibia\scripts.

High-level game additions ​

Full signatures: game, game.overlay, game.http.

APIUse for
game.on("tick"|"key_down"|"render"|packet name, fn)Callbacks. Failed handlers are dropped. game.off(id) removes one
game.overlay.*Show/hide/maximize/move host window; mouse_captured()
game.tile_to_screen / game.screen_to_tileMap widget projection (orbwalker, farm ESP)
game.input_snapshot(vk?)Mouse, buttons, optional key, Ctrl/Alt/Shift, foreground
sdk.keysParse "Ctrl+Shift+K" to a Win32 VK + modifier mask; isPressed / matches
game.walk / game.move_to / game.rotateOfficial movement intents; directional walk and rotate are one-shot
game.attack / game.stop_attackOfficial attack actions 35/36; new targets may cancel move_to
game.follow / game.stop_followOfficial follow actions 37/38 with client-side follow state
game.attack_target_id / Player:attackTargetId() / Player:isAttacking()Exact red target box on the world and Battle List. attackTargetId returns id, known — known=false means the native read failed (keep the last observation); only an observed zero clears the box. Outgoing attack requests do not count
game.action(name, fields?)Publish a simple TGameAction* (ToggleMounted, AttackFirstTarget)
game.get_local_player / game.get_creaturesLive reads; vocation/level also from player-data messages
Engine.IsInProtectionZone()Tri-state PZ (true/false/nil)
game.http.request / game.http.pollAsync HTTP. request returns an id; poll(id) is nil while in flight, then a result table

Kite (sdk.kite) ​

Kite.to picks a stand tile. It does not walk or send Attack. CaveBot fight submits cavebot:kite through the action broker after this planner returns.

  • In weapon range it scores the eight neighbors. Cardinal tiles (north, south, east, west) win. A diagonal is used only when every cardinal neighbor is blocked, and only if both side tiles are open unless squeezing.
  • Closing from farther away still uses Path.to (native FindPath first, Lua A* if the native route cuts a blocked tile).
  • Returns hold=true when already in the [min_range, attack_range] band.
lua
local Kite = require("sdk.kite")
local hit, err = Kite.to(here, {
  target = monster,
  pack = pack,
  min_range = 2,
  attack_range = 4,
  step_only = true,
})

Full signatures: sdk.kite.

game.dbg — live client ​

Full signatures: game.dbg.

APIUse for
d.packet(name|type) / d.packets()Catalog row: type, name, rtti, va
d.action(name) / d.actions()tibia::input::TGameAction catalog. game.action publishes simple rows
d.enqueue(p|name|type, queue, msg)Typed enqueue. Do not pass an RVA
d.vtable(name) / d.type(name)Live vtable from MSVC RTTI
d.queue() / d.session() / d.creature_id()Session objects
d.alloc / d.write_u64 / d.read_ptr / d.read_f32Build protobuf objects in memory
d.set_world_map_scaleOfficial camera scale (posted to the GUI thread)

Nested types are not packets:

lua
d.write_u64(pos, d.vtable("ObjectPosition"))
d.write_u64(ident, d.vtable("ObjectIdentifier"))

Stable keys: TalkMessage / GameclientMessageTalk / 150, UseOnCreature / 132.

Protocol fields — how to use them ​

Every official inbound and outbound type has a usage page: Events (subscribe) and Packets (send). Print the live block for one type:

lua
local Engine = require("sdk.engine")
local Packet = require("sdk.packet")

print(Engine.help("Wait"))
print(Engine.help("Talk"))
print(Packet.help("JoinChannel"))
print(Packet.help("SeekInContainer"))

Subscribe and read named scalars:

lua
Engine.on("Wait", function(ev)
    print(ev.wait_time)
end)
Engine.on("Talk", function(ev)
    print(ev.speaker, ev.text, ev.mode)
    -- ev.position is a nested Coordinate (x/y/z)
end)

Send scalars/strings from the schema. Nested msg/rep still need a helper:

lua
Packet.send("JoinChannel", { channel = 5 })
Packet.send("SeekInContainer", { container = 0, index = 20, category = 0 })
Packet.talk("hello")
Packet.attack(creature_id)

kind u8/u32/i32/u64/str work with Packet.send. msg/rep do not. Presence is (1 << hasbit). Login/session events redact hex. DisbandParty has no enqueue helper. Engine.Schema("Wait") / Packet.fields("Talk") return the same rows help prints.

If a row is missing: get the RTTI short name from IDA → d.vtable(name) → add type_id / key / RTTI / object_size in Engine/RttiPackets.inc. Do not paste an enqueue RVA.

Do not:

  • Hardcode d.rva(0x1D9…) or d.call(0x4E…)
  • Use Canary opcodes (0x84) as protobuf type ids
  • Call Qt metacall as a send API

AI-assisted development ​

text
Write this Arcane SDK feature from zero in Lua 5.4. No new C++.
Live folder: Documents\SharpTib\scripts. SDK first (require("sdk.*")).
Use game.dbg.packet / vtable / enqueue like 04_say_hello.lua when the SDK has no helper.
Canary Lua is intent and item ids only — never opcodes.
If IDA is open: list_instances, analysis_wait, find GameclientMessage*.
Do not invent RVAs or field layouts.
Goal: <one behavior>
First print the catalog row + queue, then send once.

Test checklist ​

TestPass condition
hello print01_hello.lua shows one log line
catalog02_research.lua prints TalkMessage type=150 and a live va
overlay hideX closes main menu; Lua HUD stays; Insert hides both
PZEngine.IsInProtectionZone() is true on a temple tile
reloadNo duplicate ticks after Reload all
Game MCPlist_scripts / execute_lua work from the Documents workspace

Host suite (SDK / farm Lua) ​

NEW PROJECT/Sharp-NewTibia/tests/x64/DEV/Sharp-Tibia.Tests.exe loads the live Documents scripts and the SDK junction. No arguments runs every suite (~36, about a minute, including map assets). Pass a slice instead:

text
Sharp-Tibia.Tests.exe farm
Sharp-Tibia.Tests.exe sdk cave
Sharp-Tibia.Tests.exe --list
.\tests\run.ps1 -NoBuild cave
.\tests\run.ps1 farm

farm is sdk + cave + broker + hud. run.ps1 incremental-builds with /m, then forwards those names. Lua-only edits do not need MSBuild (-NoBuild). Use the bare exe for host C++ changes or a full sign-off. Do not delete suites to go faster.

Troubleshooting ​

packet not in catalog / RTTI miss
DLL is old, RTTI scan did not join, or the name is wrong. Rebuild, inject, then d.packet("GameclientMessageTalk").

Editor has no Game MCP tools
Workspace is the wrong folder. Open %USERPROFILE%\Documents\SharpTib\scripts and restart MCP.

Script crashed and will not tick
It was quarantined. Scripts tab shows the report path. Fix, then reload that module.

Quick reference ​

TopicAnswer
LanguageLua 5.4 · see versions for the current SDK zip
WorkspaceDocuments\SharpTib\scripts
Sendd.packet(name) · d.enqueue(...)
Game MCP / map38472 / 38574 on 127.0.0.1
RVAsNever in Lua or RttiPackets.inc; warmup fills enqueue_rva

Arcane SDK catalog generated from scripts/sdk