Plugin audit
Packages and kits builds every gameplay system from the public API, so that a kit outside the engine could rebuild it. This page lists what stood in the way.
The following table lists, for each folder, what its systems use today that a game’s own code cannot reach through the public API, and what depends on it. Line numbers are at commit ebc716025 on 2026-09-30, and each entry names the kind of gap: a save field the schema fixes, a message every world registers, a call hardwired into the step or the world, a trait map private to the dump, the character’s spawn, or an import that has no exported equivalent. Every gap the first column names is closed by a layer at the end, so the plugin that replaces the folder uses only public doors.
| Folder | Uses what a game cannot | What depends on it |
|---|---|---|
| abilities | The engine.cast message every world registers (gameplay/runtime.ts:35). Private dump maps: AbilitiesTrait streams to its owner alone (gameplay/harness.ts:286), PendingCastTrait stays on the room (:316), TargetTrait holds a handle (:325). The character’s spawn grants abilities from the Player prop (components/Player.tsx:127-130, :307) and from CharacterSpawnTrait in the room (room/src/room.ts:1046-1047). Unexported helpers: the resource reserve and settle (gameplay/resources/resources.ts:239-269), the strike, line of sight and slow (gameplay/abilities/abilities.ts:124-237). |
The projectile (gameplay/weapons/projectile.ts:3-7); equipment grants and revokes (gameplay/inventory/equipment.ts:2); the room (room/src/room.ts:39, :1047); the dump; no CLI, MCP or devtools code, until the abilities inspector lands. Not a predicted system: a replay never runs it. |
| cooldowns | CooldownsTrait streams to its owner alone through the private map (gameplay/harness.ts:288), and the room sends owned traits only for a player’s character (room/src/room.ts:1779), so an NPC’s cooldowns reach no page. |
Abilities (gameplay/abilities/cast.ts:283-291), resources (gameplay/resources/resources.ts:198-200), item use (gameplay/inventory/use.ts:135-165). |
| resources | The fixed resources save field (schema/src/save.ts:85-87, read at gameplay/save.ts:37, restored at components/Player.tsx:250). The character’s spawn declares them from the Player prop (components/Player.tsx:131-134) and from CharacterSpawnTrait (room/src/room.ts:1039-1040). |
Abilities’ costs (gameplay/abilities/cast.ts:10-14), the save (gameplay/save.ts:4), the room (room/src/room.ts:40). |
| stats | The fixed stats save field (schema/src/save.ts:82, read at gameplay/save.ts:33-35, restored at components/Player.tsx:296 and room/src/room.ts:1041). The character’s spawn adds the trait with maxHealth (gameplay/character/actions.ts:54-60). The formula’s totals are unexported (gameplay/stats/totals.ts:12-32), and weapons import them (gameplay/weapons/traits.ts:7-13). |
Abilities, resources, weapons, inventory, health (gameplay/behaviours/systems.ts:302), movement (gameplay/character/movement.ts:116), the devtools’ stats rows (stores/devtools.ts:1603-1627, devtools/src/WorldPanels.tsx:33-48), the wiki tab (stores/devtoolsWiki.ts:223-247). |
| weapons | Shots ride fixed protocol types rather than a registered message (gameplay/replication/protocol.ts:58, :43, gameplay/replication/delta.ts:25), and the room hardwires the judge, the rewind and the shot results (room/src/room.ts:1512, :1809-1815, :1870). FiredShotsTrait and LatencyTrait stay on the room through the private map (gameplay/harness.ts:314-315). The character’s spawn adds HeldWeaponsTrait from the Player prop (components/Player.tsx:121-126) and the room adds LatencyTrait and FiredShotsTrait (room/src/room.ts:1057-1058). The crosshair module is unexported (gameplay/weapons/crosshair.ts:66-176). |
Abilities’ projectile and cover (gameplay/abilities/cast.ts:16-17); the room’s shot rules (room/src/shots.ts:167-240) and bot; spawnite play aim (cli/src/play.ts:2631); the devtools’ aim read (stores/devtools.ts:1825). |
| inventory | The fixed inventory and equipment save fields (schema/src/save.ts:90-91, read at gameplay/save.ts:38-39, restored at components/Player.tsx:299, :304). Item verbs ride a fixed protocol type (gameplay/replication/protocol.ts:63, applied at gameplay/replication/items.ts:29, sent by the frame loop at components/Frameloop.tsx:622). InventoryTrait and EquipmentTrait stream to their owner alone through the private map (gameplay/harness.ts:289-290). The character’s spawn declares the bag from the Player prop (components/Player.tsx:135-139) and in the room (room/src/room.ts:1050-1051). |
Pickups (gameplay/behaviours/systems.ts:13-15), the room’s item checks (room/src/room.ts:590-617), spawnite add item and place_loot (cli/src/items.ts:60, :121; mcp/src/tools/item/index.ts:13, :31), the wiki tab. |
| npc | The engine.dialog message every world registers (gameplay/runtime.ts:34). The conversation streams to its owner alone through the private map (gameplay/harness.ts:287) and through the dialog machine’s ownerOnly (gameplay/npc/dialog.ts:382); the routine and dialog machine traits are hidden from the dump (gameplay/npc/traits.ts:94, :121, :158). toMachineId is unexported (gameplay/npc/machineId.ts:8). |
Faction, which the projectile and abilities read (gameplay/weapons/projectile.ts:8, gameplay/abilities/traits.ts:4); add_npc and add_dialog (mcp/src/index.ts:140, :158) and their templates; the inspector’s NPC section (devtools/src/surfaces/InspectorPanel.tsx:140-148); DialogPanel (components/DialogPanel.tsx:45). |
| state | The step calls reconcileStateTags before its first system (gameplay/step.ts:474) and the world subscribes watchStateTags as it is made (gameplay/runtime.ts:41). ownerOnly on a machine is the one public route into the owned stream (gameplay/state/states.ts:198, gameplay/harness.ts:438-440). |
The round, the NPC runner and the conversation; the checkpoint; the room’s trait writes (room/src/room.ts:2112); the AI tree (gameplay/ai/tree.ts:246-248); the devtools (stores/devtools.ts:2460, :2488). |
| character | The fixed character save field (schema/src/save.ts:69-77, read at gameplay/save.ts:14-18). Join, input and walk ride fixed protocol types (gameplay/replication/protocol.ts:48-53). RespawnTrait stays on the room through the private map (gameplay/harness.ts:317). The room spawns a character from CharacterSpawnTrait’s fixed fields (gameplay/character/movement.ts:128-151, room/src/room.ts:1019-1072), and no join hook lets a plugin add to her. |
Twenty-four gameplay files, the room’s spawn and correction, the replay (gameplay/replication/replay.ts:107-120), the CLI’s dump names (cli/src/playtest.ts:617-623), the devtools. |
| track | copyClientInput copies the step’s input into each track mover inside the character’s own system (gameplay/step.ts:142-146). TrackTriggerMoversTrait holds handles through the private map (gameplay/harness.ts:326). |
The round’s laps (gameplay/world/round.ts:33-35), the track camera (gameplay/camera/trackCamera.ts:4-5), the AI tree (gameplay/ai/tree.ts:110-113). |
| world | The fixed levels save field (schema/src/save.ts:94, read at gameplay/world/levels.ts:149). The round’s score counts the coins addCoins credits while it plays (gameplay/world/round.ts), and no public call adds to it otherwise. |
spawnite simulate (cli/src/simulate.ts:286-287); the round in the dump (cli/src/playtest.ts:688-695). |
| behaviours | The engine.interact message every world registers (gameplay/runtime.ts:33). The fixed loot save field (schema/src/save.ts:97-99). The runner runs a closed list (gameplay/behaviours/systems.ts:391-403) and reads runsOn for that list alone (:412), so a game’s behaviour with runsOn set runs wherever the game’s own list puts it. The InteractedEvent event is never registered (gameplay/behaviours/traits.ts:76). The approach module is unexported (gameplay/behaviours/approach.ts). |
Every gameplay folder; the room (room/src/room.ts:1032, :1048, :1054); add_behaviour (mcp/src/index.ts:118); the devtools’ Add behaviour menu. |
| checkpoint | The physics codec is hardwired (gameplay/checkpoint/world.ts:103-109) from an unexported module (gameplay/physics/checkpoint.ts). |
The room’s continue (room/src/room.ts:2038-2161), spawnite replay (cli/src/replay.ts:564, :682). |
| the others | entity, replication, physics, map, ai, camera and clips are building blocks: what they use is the step itself, and the gaps are unexported modules (gameplay/replication/codec.ts, gameplay/map/colliderBake.ts, the physics shapes) that no plugin needs. |
Everything. |
Three findings cut across the table:
- Two lists, and they differ. A game’s page reads
<Game systems>and a game’s room reads the scene file’ssystemsexport, and nothing checks that they match (components/Game.tsx:190,room/src/scene.ts:343-366). Holdfast’s room runs its siege list while its page runs the engine’s default, because its app passes no list (games/holdfast/src/app/app.tsx:71-76).spawnite simulatereads a third source,src/systems.tsby convention (cli/src/simulate.ts:266-280), which misses holdfast’ssrc/siege/systems.ts. - A place bundles systems with order inside it. Depthfield splits
baseSystems.inputby position to put its dash between the input copy and the steer (games/depthfield/src/systems.ts:9-21), which the step’s own comment says a game avoids (gameplay/step.ts:226-228). The engine’s comments name three orderings inside a place,approachCastTargetsbeforesteerCharacters,runItemActivationsbeforerunBehaviours,castAbilitiesbeforeflyProjectiles, and one across places, the stats’ expiry first inmoveso every behaviour reads a stat without its spent buffs. - One player only. A game can register a message (
registerMessage, as holdfast’s signals do) and its systems can read it, but a game cannot stream a plain trait to one player only or keep one on the room:registerTraitwrites only the named and event maps (gameplay/harness.ts:354-356). The one public route is a state machine withownerOnly, which the dialog uses. Since then,defineTraittakesownerOnlyandserverOnly, as Choose who a trait reaches describes, so a game’s plain trait has both routes.