Appearance
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. 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
- HTTP 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.packet for an uncovered action. 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
- Inject Arcane. Overlay → Scripts → Open folder (
Documents\SharpTib\scripts). - Open that folder as the Cursor / VS Code / Claude / Codex workspace.
- Restart MCP so
arcaneappears. - Start from
01_hello.luaor a 10-line print probe. - Confirm logs, then add one SDK call or one packet.
- Resolve packets with
game.dbg.packet("TalkMessage"). - Write
p.vainto the object. Send withgame.dbg.enqueue(p, queue, msg). - Nested objects use
game.dbg.vtable("ObjectPosition")— they are not packets. Tag: 1=worldmap, 2=inventory, 3=container. - 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\scriptsStore 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 the MCP server. No Node sidecar for Cursor, VS Code, Claude Code, Codex, or Gemini.
On every inject it writes (and refreshes if the file still contains arcane or the old sharp-tibia key):
| Client | File |
|---|---|
| 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 MCP.
Optional Node stdio bridge (only if an editor cannot use HTTP):
text
node %USERPROFILE%\Documents\SharpTib\scripts\.arcane\mcp-stdio.jsMCP 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 (09_farm, farming\, ui_*). 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.
| API | Use 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_tile | Map widget projection (orbwalker, farm ESP) |
game.input_snapshot(vk?) | Mouse, buttons, optional key, foreground |
game.walk / game.move_to | Movement (walk gated to one official Go per 500 ms) |
game.get_local_player / game.get_creatures | Live reads; vocation/level also from player-data messages |
Engine.IsInProtectionZone() | Tri-state PZ (true/false/nil) |
game.http.request / game.http.poll | Async HTTP. request returns an id; poll(id) is nil while in flight, then a result table |
game.dbg — live client
Full signatures: game.dbg.
| API | Use for |
|---|---|
d.packet(name|type) / d.packets() | Catalog row: type, name, rtti, va |
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_f32 | Build protobuf objects in memory |
d.set_world_map_scale | Official 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) → put a new enqueue helper RVA only in Engine/RttiPackets.inc.
Do not:
- Hardcode
d.rva(0x1D9…)ord.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
| Test | Pass condition |
|---|---|
| hello print | 01_hello.lua shows one log line |
| catalog | 02_research.lua prints TalkMessage type=150 and a live va |
| overlay hide | X closes main menu; Lua HUD stays; Insert hides both |
| PZ | Engine.IsInProtectionZone() is true on a temple tile |
| reload | No duplicate ticks after Reload all |
| MCP | list_scripts / execute_lua work from the Documents workspace |
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 arcane 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
| Topic | Answer |
|---|---|
| Language | Lua 5.4 · see versions for the current SDK zip |
| Workspace | Documents\SharpTib\scripts |
| Send | d.packet(name) · d.enqueue(...) |
| MCP / map | 38472 / 38574 on 127.0.0.1 |
| RVAs | Only inside RttiPackets.inc, never in new Lua |