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

  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 arcane 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 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):

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

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

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

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

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, foreground
game.walk / game.move_toMovement (walk gated to one official Go per 500 ms)
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

game.dbg — live client

Full signatures: game.dbg.

APIUse 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_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) → put a new enqueue helper RVA only in Engine/RttiPackets.inc.

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
MCPlist_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

TopicAnswer
LanguageLua 5.4 · see versions for the current SDK zip
WorkspaceDocuments\SharpTib\scripts
Sendd.packet(name) · d.enqueue(...)
MCP / map38472 / 38574 on 127.0.0.1
RVAsOnly inside RttiPackets.inc, never in new Lua

Arcane SDK catalog generated from scripts/sdk