Skip to Content
Modlock

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:

NameDirectionMeaning
modlock.host_call(ptr, len) -> lenmod to hostHand the host an encoded Call; returns the length of the encoded Reply.
modlock.host_read(ptr, len)mod to hostCopy the host's pending message, a Call or a Reply, into mod memory.
modlock_event(len) -> i64host to modDeliver 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.

On this page