The mental model
One Colyseus room, a handful of focused systems, four behavior registries, an injectable ruleset, and a content store — that's the whole server. This page walks through how they fit together.
Package boundaries
| Package | Owns |
|---|---|
@ms6/shared | Enums, packet type constants, payload types, content record types, the pure rule functions + Ruleset interface, the generic Registry class. Imported by every other package — nothing in here imports from server/client/editor. |
@ms6/server | The Colyseus room, gameplay systems, content storage, REST API, auth. |
@ms6/client | The Phaser game — rendering, input, movement prediction, UI panels. |
@ms6/editor | The standalone admin web app. Talks to the server over plain REST (packages/server/src/routes/*), never over Colyseus packets. |
The room and its systems
GameRoom (packages/server/src/rooms/GameRoom.ts) is
deliberately thin: Colyseus lifecycle hooks (onCreate/
onAuth/onJoin/onLeave), the message
routing table, and the game tick loop. Every actual gameplay rule lives in
a System class under packages/server/src/systems/:
| System | Owns |
|---|---|
PlayerSystem | Movement, chat dispatch, inventory/equipment, item use, character hydration/save |
CombatSystem | Melee attacks, spell casting, damage application, death/respawn, experience |
NpcSystem | NPC spawning, AI tick, death/loot |
MapSystem | Map load/send, warping, tile triggers, ground items |
ShopSystem, TradeSystem, PartySystem, GuildSystem | Exactly what they say |
ChatSystem, AdminSystem | Map/whisper/emote chat; staff warp/kick/ban/set-access commands |
Every system's constructor takes the room itself: new PlayerSystem(room).
GameRoom implements the EngineContext interface
(packages/server/src/engine/EngineContext.ts), which is the
documented contract of what a system is allowed to depend on — session
lookups, broadcast helpers, cross-system getters, and the live
ruleset — rather than the concrete class's full surface area.
Session bookkeeping
SessionRegistry (packages/server/src/engine/SessionRegistry.ts)
owns the actual player-session map, the per-map index used for map-scoped
broadcasts, and the exact-name index used for whispers and admin lookups.
GameRoom composes one and delegates to it — this is what used
to be three hand-kept Maps directly on the room.
The four behavior registries
This is the actual extensibility story. Four gameplay dispatch points that
used to be closed switch statements are now lookups into a
Registry<number, Handler> (packages/shared/src/registry.ts):
| Registry | Keyed by | Replaces the switch in |
|---|---|---|
npcBehaviorRegistry | NpcBehavior | NpcSystem.tick() |
itemEffectRegistry | ItemType | PlayerSystem.handleUseItem() |
spellEffectRegistry | SpellType | CombatSystem.handleUseSpell() |
tileTriggerRegistry | TileType | MapSystem.handleTileTrigger() |
All four live under packages/server/src/engine/behaviors/.
defaults.ts registers the engine's built-in behaviors;
registerDefaultBehaviors() is called once from
index.ts before the server starts listening. Full details
and a worked example: Behaviors & Rules.
The ruleset
Damage formulas, the experience curve, and vital regeneration are not
free functions systems call directly — they go through
room.ruleset, a Ruleset object
(packages/shared/src/rules/ruleset.ts) set once at server
bootstrap. defaultRuleset reproduces the engine's historic
numbers exactly; a game can supply its own via createRuleset().
Details: Custom Ruleset.
Content storage
ContentManager (packages/server/src/content/ContentManager.ts)
is a module-level singleton every system reads from directly —
ContentManager.getItem(id),
ContentManager.getNpc(id), and so on. Internally, items,
NPCs, spells, and shops are each backed by a generic
ContentStore<T, Row> (content/ContentStore.ts)
parameterized by a small descriptor (content/descriptors.ts)
that knows how to translate between a Prisma row and the in-memory record
shape — this is what collapsed four nearly-identical load/seed/save
blocks into one generic implementation. Maps, classes, and server config
stay hand-rolled since they don't fit that same shape. See
New Content Types for how to add a
sixth content type on this pattern.
Content edits reach live players immediately
The standalone editor writes over REST (routes/content.ts),
which saves straight into ContentManager's in-memory cache —
every system reads content fresh, so a balance change takes effect
immediately with no server restart. For item/NPC/spell/shop/server-config
saves specifically, the route also pushes a live update packet to every
connected client via engine/liveRoom.ts, so an
already-connected player's own cached copies (item icons, etc.) refresh
without reconnecting. Map saves are the one exception — broadcasting a
map indiscriminately would corrupt the client state of players currently
on a different map, so a map edit takes effect for a player the
next time they enter that specific map.
Client side
NetworkManager (packages/client/src/network/NetworkManager.ts)
is the client's packet dispatcher: it wraps the Colyseus room connection
and fans every incoming message out to whatever handlers registered for
that packet type via NetworkManager.on(type, handler) — the
same registry pattern as the server's behavior registries, just for the
wire protocol instead of gameplay dispatch.
GameScene (packages/client/src/scenes/GameScene.ts)
is where most of those handlers are registered; it also owns local-player
movement prediction/reconciliation and coordinates the
MapRenderer/EntityRenderer rendering helpers.
See Client UI & Effects for how
to add to it.