Scripting
Moongate shards are scripted in Lua. The server exposes a small, curated surface of global modules that let scripts log, run work on the game loop, subscribe to server events, and create or manipulate items and mobiles by serial. Each module is a C# class bridged into the Lua runtime (the SquidStd scripting engine, built on MoonSharp).
NPC behavior is authored separately as NPC brains.
Brain files run in the same scripting runtime as everything else: they can use
the modules below and act on themselves through the imperative
ai module.
Modules
| Module | Purpose | Reference |
|---|---|---|
log |
Write to the server log (Serilog). | log |
game |
Run work on the game-loop thread and schedule timers. | game |
events |
Subscribe to named server events. | events |
item |
Create and manipulate items by serial. | item |
mobile |
Create and manipulate mobiles by serial. | mobile |
loot |
Roll loot tables into items. | loot |
account |
Create and manage accounts by username. | account |
ai |
Act as the current NPC brain (valid only inside a brain hook). | ai |
memory |
Durable per-NPC key/value memory (valid only inside a brain hook). | memory |
| enums | skill_name, gender_type, race_type, layer_type, account_level_type. |
Enums |
Values and types
Scripts never hold C# object handles. Items and mobiles are referenced by
serial — a plain number that round-trips as a Lua number and is re-resolved
on every call. Accounts are the exception: they are referenced by username,
the handle they log in with. Functions that create something return the new serial, or nil
when creation failed (for example an unknown template). Functions that read
state return a Lua table of fields, or nil when the subject does not
exist. Functions that return several serials return them as a Lua
array-table (iterate with ipairs).
Thread model
Warning
The item.*, mobile.* and loot.* functions are synchronous and mutate
world state directly, so they must run on the game-loop thread — the
single-writer boundary for world state.
- Callbacks registered with
events.on(includingworld_ready) are dispatched on the game-loop thread automatically. You do not need to wrap their body ingame.post. - From any other context (an async continuation, a thread of your own —
anything not already running on the loop),
reach the loop with
game.post,game.scheduleorgame.schedule_repeating.
As a diagnostic safety net, every mutating item.* / mobile.* / loot.*
call checks whether it is running on the loop thread and logs a warning
if it is not. It never blocks or throws — it just makes an otherwise-silent
single-writer violation visible in the log.
The account.* functions are the exception: they touch the account store,
not the world, and carry no such requirement. The one that does reach the
world is account.delete — it deletes
the account's characters — and it warns off-loop like the rest.