Host meta layer
The part of the host a game author observes: how the engine drives the game's Wasm core and routes what the core reports, and the engine-owned meta layer on top of it — the participant registry, the gameplay rules the engine owns (rounds, scoring, teams, map rotation, spectators), the managers and the modules. A game parameterises these through gameConfig; it cannot replace them.
Core calls and events
The engine drives the game's Wasm core and routes what the core reports (the ABI itself is in plugin-api.md):
- Lifecycle. The map is loaded with
load_map(it arrives already scaled, so the core loads it withscale: 1). A human joins throughspawn_actorand leaves throughremove_actor; a scripted participant (bot) usesspawn_scripted_actor/remove_scripted_actor; a player data change callsreset_actor. On a layered (2.5D) map a respawn point may name its level ([x, y, angle, level]) and the engine passes it on withset_actor_level(gameId, level)— the call is feature-detected, an older core without it derives the level from the geometry. - Input. Player input reaches the core through
apply_input; the core confirms the sequence number in the frame's player block. - Events. After each
stepthe engine drainstake_events()and routes the standard dictionary itself:panelSet/panelActiveupdate the panel (fieldis the game's own panel-schema key),deathgoes to the kill report (scores, round outcome),shakeshakes the camera.customis the only type with game-specific meaning: it is handed as is to the optionalHostPlugin.onCoreEvent(data, { panel, vimp }). The core works on numeric ids (u32), the meta layer on string keys — the engine converts at that boundary. - Frames. The snapshot is packed with
pack_body/pack_frame; the first frame of a joining player is built from the core'splayers_data().
Meta layer
The engine's meta layer runs the match on top of the core's events. Code a plugin adds to the host (command handlers, modules) must use isomorphic APIs only — see overview.md.
The map list a room rotates through is the catalog's current one at the moment the room starts.
Participants
Humans and scripted participants (bots) share one registry and one numeric id space; they are told apart by the isScripted/isNetworked getters, not by the shape of the id. Every participant has gameId, name, model, team, teamId and status; a scripted participant's name is scripted.namePrefix plus its id from the game config.
Engine-owned gameplay rules
These rules live in the engine and cannot be replaced; a game parameterises them through gameConfig or implements an alternative entirely inside its WASM core. The per-manager details are below.
- Rounds. A round ends when a team is wiped (every participant, humans and scripted, not alive) and survivors remain in at most one other team; with three or more teams it goes on until only one team has anyone alive. The winner is the team that still has survivors, however the last player died (an enemy kill, a team kill, self-destruction, the environment, a killer who has already left) — every
deathevent runs the wipe check. A wipe with no survivors anywhere (both last players died at once, or a single-team game) ends the round with no winner: every player gets thedefeatcue (spectatorsvictory) and a round-end message with no winning team. The round timer expiring does not end the round with a result — it starts a new one with no score change. The next round starts aftertimers.roundRestartDelay. A game that is not round-based is closest to one long round (a largeroundTime, respawns in the core) or a single team, in which no wipe can occur. - Scoring. Killing an enemy: killer
score +1and rank points+1, victimdeaths +1andstatus = 'dead'. A team kill: killerscore −1and points−1, victimdeaths +1. A suicide: victimdeaths +1only. A killer who already left: no frag, the victim still dies and the round still resolves. Each wiped team's head getsdeaths +1once per round, and at round end the surviving team's head getsscore +1(none on a draw). The±1rule is written synchronously with the kill report and there is no hook to change it. A dead player becomes a spectator watching the killer until the next round. - Sound cues fired here:
fragto the killer,deathto the victim,victoryto the winning team and to spectators (on any outcome),defeatto everyone else at round end. Exactly five engine events exist (roundStart,victory,defeat,frag,death);gameConfig.soundCuesmaps them to the names in the game's sound config, cued sounds bypass the world voice limit, an unmapped cue is not sent, and{}disables them. - Teams.
gameConfig.teamsmaps team names to numeric ids and includes the spectator team; undernoSpectators: trueit holds exactly one team and a joining human goes straight into it (no vote). One playing team is a valid configuration. Team capacity isrespawns[team].lengthon the current map. Joining a full team callsscripted.removeOneForHuman(team); when it returnsfalsethe player getsTEAMS_TEAM_FULLand stays put. Switching team withintimers.teamChangeGracePeriodof the round start is free; later the switcher dies and spectates until the next round. Dropping below 2 active humans resets the statistics and immediately starts a new round — unless the game declaresendlessRound: true, which switches off every engine-initiated round restart (this rule, the team wipe and the round timer);/nrand map changes still restart it. - Map rotation.
timers.mapTimeexpiring starts a system vote offeringmapsInVotemaps from the catalog; a player may also propose a map through the vote menu (a single proposal is applied directly); the switch waitstimers.mapChangeDelay. A map change re-creates the scripted participants, resets the panel and the stat table, clears the votes and flushes the rank and state to the platform. - Spectators. Dead players and members of the spectator team follow another participant (the camera tracks the killer after a death);
spectatorKeys(nextPlayer/prevPlayer, from key set0) cycle the watched player; the cursor auto-hides after 3 s without mouse movement. - Game informs. The codes are fixed by the engine (
vimp-engine/src/config/gameCodes.js):winnerTeam→ index0ofgameInform.list,roundStart→1,gameOver→2. The indexes are positional — reordering the array changes the meaning; code1also triggers the panel/logo animation on the client. Technical informs (techInformList, with engine defaults) are indexed as listed in plugin-api.md; placeholders{0},{1}come from the params array. - Initial vote.
gameConfig.initialVote(usually'teamChange') is the vote sent to a player right after their first frame;nulldrops them straight into the game. - Player profile.
rankis an engine-owned integer in a cross-game format rendered by the platform's lobby;stateis the game's own JSON blob seeded fromplayerState.defaultState, read and written throughonCoreEvent'svimpobject and flushed on round end, map change and departure. Both are namespaced per(user, gameId); because the host is an untrusted browser, both are technically forgeable — a known limit of the P2P model. - Kicks. There is no kick vote and no
/banendpoint. Protection from a bad host is the engine's host migration — the automatic triggers (overload, hidden tab, network lag) and the/changehostvote, which the platform counts, not the host (lobby mode only). None of it passes through the plugin, and the game's vote menu gets no item for it. Thresholds and close codes: game-config.md and snapshot.md.
Managers and modules a game configures
A game does not replace the engine's managers and modules; it drives them through gameConfig and the plugin hooks:
| Component | What the game supplies | Behaviour visible to the game |
|---|---|---|
RoundManager | teams, map respawns, scripted, timers.*, endlessRound | rounds, teams and map rotation exactly as in the rules above |
CommandProcessor | chat commands through HostPlugin.chatCommands (registerCommand(name, handler)) | the engine has no commands of its own; a handler is handler(ctx, gameId, args); an unknown command answers "Command not found"; when several humans are active a command may go through a vote |
VoteCoordinator, Vote | game votes (createVote), their texts | topic cooldown timeBlockedVote, lifetime voteTime; more than 7 options paginate; a tie is resolved by a random pick; a vote created during an active one waits in a queue |
Panel | panel.fields, activeKey | per-user HUD; only changes are sent, once per snapshot tick; authoritative values (health, ammo) live in the core and reach the panel through its events |
Stat | the stat schema (columns, aggregation, teams) | scoreboard rows and team totals; a delta per tick, the full state on join |
Chat | HostPlugin.systemMessages (the game's code groups) and, on the client, their texts | the engine groups s/v/m/c have English defaults; a game code without a text is dropped silently; every system message is a code, data goes in params |
TimerManager | timers.* | the game loop, round and map timers, RTT pings, idle checks, deferred round restart and map change |
RTTManager | rtt.*, idleKickTimeout | latency tracking and kicks, see snapshot.md |
PlayerDataSync | playerState.defaultState | per-participant rank/state, see plugin-api.md |
← Previous: Rust Engine Core (vimp-engine-core) · Next: Client runtime for game authors →