How mods reach the game
The protocol between a mod and the server that runs it.
A mod is a WASI preview 1 reactor module. It imports two functions and exports one:
| Name | Direction | Meaning |
|---|---|---|
modlock.host_call(ptr, len) -> len | mod to host | Hand the host an encoded Call; returns the length of the encoded Reply. |
modlock.host_read(ptr, len) | mod to host | Copy the host's pending message, a Call or a Reply, into mod memory. |
modlock_event(len) -> i64 | host to mod | Deliver a Call of len bytes, which the mod copies with host_read. Returns the address and length of the encoded Reply, packed as address << 32 | length, or zero for an empty reply. |
Take one server frame in which the mod sends a player a chat line. The host encodes a Call for the frame event, holds it and calls
modlock_event with its length. The mod passes a buffer of exactly that length
to host_read, which copies the call into it. The mod's frame handler runs and
sends the chat line: the mod encodes that request as a Call and passes it to
host_call. The host answers it at once, holds the encoded Reply and returns
its length, and the mod copies it out with host_read as before. The host's
time on that call counts against the frame's budget. When the handler returns,
modlock_event returns where the mod left its own Reply, and the host decodes
it from mod memory.
The calls are in src/wasm/wasm.proto. A
Call contains an encoded request for one method. The method is on the Host
service when a mod calls the game, and on the Mod service when the host
delivers an event. The Reply contains the encoded response or an error. A new
capability is added as a new method, so the three functions never change. Each
event runs within a time budget, which stops a runaway loop, and each mod has a
memory limit.
A program that embeds the host can offer mods more than the game, such as a game
mode's match rules. The program provides a service with WasmHost::Provide, and
mods call it with the host's CallService method, callService(service, method, payload) in a mod's library. It calls a service a mod serves with
WasmHost::Call, which delivers the mod's Serve event to the handler the mod
registered with serve(service, handler). Each
service defines its methods and the encoding of their payloads. A mod cannot be
called while it is calling the host.
An interpreted mod's built mod.json sets its runtime, such as
RUNTIME_QUICKJS, in place of a module. The host runs the interpreter module
beside modlock-host and hands it the mod's source in the start event.
Message packages
Each concern's messages live in a .proto beside the code that implements it,
in its own package with its directory's name. For example,
src/wasm/players/players.proto declares:
- the protobuf package
modlock.wasm.players - the C++ namespace
modlock::wasm::players - the Go package
github.com/paralin/modlock/src/wasm/players
The mod boundary's calls are in
src/wasm/wasm.proto,
and their messages are in the packages beside it. bun run gen regenerates all
of the Go, TypeScript, Luau, Python, and C++ code with the protobuf toolchain
the repository pins. Building
Modlock says where
modlock build gets the TypeScript library.