Mirage Source 6
Architecture

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

PackageOwns
@ms6/sharedEnums, 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/serverThe Colyseus room, gameplay systems, content storage, REST API, auth.
@ms6/clientThe Phaser game — rendering, input, movement prediction, UI panels.
@ms6/editorThe 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/:

SystemOwns
PlayerSystemMovement, chat dispatch, inventory/equipment, item use, character hydration/save
CombatSystemMelee attacks, spell casting, damage application, death/respawn, experience
NpcSystemNPC spawning, AI tick, death/loot
MapSystemMap load/send, warping, tile triggers, ground items
ShopSystem, TradeSystem, PartySystem, GuildSystemExactly what they say
ChatSystem, AdminSystemMap/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):

RegistryKeyed byReplaces the switch in
npcBehaviorRegistryNpcBehaviorNpcSystem.tick()
itemEffectRegistryItemTypePlayerSystem.handleUseItem()
spellEffectRegistrySpellTypeCombatSystem.handleUseSpell()
tileTriggerRegistryTileTypeMapSystem.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.