Write your first gamemode
A gamemode is an ordinary mod that the server designates as the session’s rule layer. The smallest useful one takes over spawning: it tells the host to stop placing players, then places them itself.
The files
Section titled “The files”Under the game’s content root — assets/workshop/ beside the install — you are creating a
namespace, an addon marker, and a mod directory. Your mod will be tutorial:hellomode:
identity comes from the path, never from a manifest, so nothing below repeats the name.
Directoryworkshop
Directorytutorial your namespace
- addon.toml marks this an addon root
Directorymods
Directoryhellomode the mod — this name IS its identity
- mod.toml
- Cargo.toml
- .cargo/config.toml
Directorywit
- world.wit
- deps/ironlark-host/host.wit a copy of the host’s contract
Directoryserver
- Cargo.toml
- src/lib.rs
-
Mark the addon root —
addon.tomlversion = "0.1.0" -
Declare the mod —
mod.tomlThe
rolesline is what makes this a gamemode candidate rather than an ordinary mod.[mod]version = "0.1.0"[provides]roles = ["gamemode"] -
Set up the workspace —
Cargo.tomlA workspace rather than a plain crate, because a mod may later grow a
client/half beside the server one.[workspace]resolver = "2"members = ["server"][workspace.package]version = "0.1.0"edition = "2024"[workspace.dependencies]wit-bindgen = "0.58"[profile.release]opt-level = "z"lto = truestrip = true -
Pin the target —
.cargo/config.toml[build]target = "wasm32-wasip2" -
Name your WIT package —
wit/world.witpackage ironlark:hellomode@0.1.0; -
Vendor the host contract —
wit/deps/ironlark-host/host.witA copy of the contract your mod compiles against: download it here.
-
Declare the server crate —
server/Cargo.toml[package]name = "hellomode-server"version.workspace = trueedition.workspace = true[lib]crate-type = ["cdylib"][dependencies]wit-bindgen = { workspace = true }
The code
Section titled “The code”server/src/lib.rs. The interesting part is eleven lines; the rest is the contract.
wit_bindgen::generate!({ path: "../wit", world: "ironlark:host/server-mod",});
use std::sync::atomic::{AtomicUsize, Ordering};
use exports::ironlark::host::server_api::Guest;use ironlark::host::entity::{SpawnTransform, Vec3, control, spawn};use ironlark::host::gamemode::set_default_spawn;use ironlark::host::log::{Level, log};use ironlark::host::map_api::list_spawns;
struct Component;
/// Spread joiners over the map's spawn points instead of stacking them.static NEXT: AtomicUsize = AtomicUsize::new(0);
impl Guest for Component { async fn init() { // We place players, so the host must stop doing it. In init, because the // host holds joins until every server mod has loaded — do this later and // the first player is already spawned. set_default_spawn(false); log(Level::Info, "hellomode: init, we own spawn now"); }
async fn on_player_join(player_id: String) { let spawns = list_spawns().await; // never empty let point = &spawns[NEXT.fetch_add(1, Ordering::Relaxed) % spawns.len()]; let at = SpawnTransform { position: Vec3 { x: point.position.x, y: point.position.y, z: point.position.z, }, yaw: point.yaw, };
// Two steps, always: an entity exists, then someone drives it. let body = match spawn("core:character".into(), at).await { Ok(body) => body, Err(e) => { log(Level::Error, &format!("hellomode: spawn failed: {e}")); return; } }; if let Err(e) = control(player_id.clone(), &body).await { log(Level::Error, &format!("hellomode: control failed: {e}")); return; } log( Level::Info, &format!("hellomode: spawned and controlled a body for {player_id}"), ); }
// Everything below is required by the contract and does nothing here. A // component missing one of them does not instantiate at all.
async fn on_player_leave(_player_id: String) {}
async fn update(_dt: f32) {}
async fn on_interact( _player_id: String, _target: String, _hit_point: exports::ironlark::host::server_api::Vec3, _distance: f32, ) { }
async fn on_signal(_channel: String, _source: String, _payload: Vec<u8>) {}
async fn on_contact( _target: String, _other: exports::ironlark::host::server_api::ContactParty, _point: exports::ironlark::host::server_api::Vec3, _edge: exports::ironlark::host::server_api::ContactEdge, ) { }
async fn handle_rpc( _player_id: String, method: String, _args: Vec<u8>, ) -> Result<Vec<u8>, String> { Err(format!("unknown method: {method}")) }}
export!(Component);Note the shape of the two calls that matter: spawn then control, never one fused
call. The entity exists first; a controller is bound to it second. That separation is what
makes bodiless players, spectators and possession possible at all.
Build it
Section titled “Build it”cargo build --release # from the mod directoryYou should get target/wasm32-wasip2/release/hellomode_server.wasm.
Run it
Section titled “Run it”Your gamemode and the bundled core:freeroam are now both candidates, so the session refuses
to start until you name one — that refusal is the resolver working, not a bug:
Name yours in config/server.toml beside the game:
[session]gamemode = "tutorial:hellomode"Then host a session from the launcher as usual. The addon has to be in the content root the
game reads — assets/workshop/ beside the install — which is where you built it in the
first step.
What success looks like
Section titled “What success looks like”In logs/host.log:
workshop: 7 mod(s) — … tutorial:hellomode 0.1.0gamemode: tutorial:hellomodehellomode: init, we own spawn nowgamemode: set default-spawn = falseserver-mod loaded: tutorial:hellomodespawn: spawned 'core:character' body 354v0 at Vec3(0.0, 5.0, 12.0) (uncontrolled)hellomode: spawned and controlled a body for af74f667-…The body is announced uncontrolled and becomes controlled a line later — that is the two
steps, in the log.
You will also see has no client half, which is correct: this mod has no client half. Add
one when you need something on a player’s screen.
Where to go next
Section titled “Where to go next”- Rounds, phases and per-player HUD text: Gamemodes and Broadcast and RPC
- Zones and trigger volumes, for anything area-based: Contact events
- Every function you can call: Interface reference
- Before designing something ambitious: What you cannot build yet