This guide helps automated coding agents and contributors navigate the codebase fast and implement changes safely.
- Orchestrator:
engine/core/engine.service.ts- Wires modules, registers commands, subscribes to events, routes chat and room notifications.
- Networking (telnet):
engine/modules/networking/telnet-server.ts: sockets, prompt, session manager, command parser.command-parser.ts: registers core commands; emits events likeplayer.move,EventTypes.PLAYER_MESSAGE.ansi.ts:ColorSchemeandprompt().session.ts: session lifecycle and I/O.
- Web bridge:
server/networking/websocket.gateway.ts: Socket.IO plumbing.web-client.service.ts: mirrors engine events to web clients; executes commands for SPA users.
- World:
engine/modules/world/world-manager.ts- Rooms, players/NPCs in rooms,
getRoomDescription(roomId, viewerSessionId?).
- Rooms, players/NPCs in rooms,
- Persistence:
engine/modules/persistence/player-manager.ts- Active players, lookups by session/username/playerId.
- Dialogue:
engine/modules/dialogue/dialogue-commands.ts,dialogue-manager.ts,canned-branching-provider.ts.
- Web client (SPA):
clients/index.html,clients/client.js.
- Events
- Use the
EventSystemwithnew GameEvent(eventType, source, target?, data?). - World room events use
targetas the player sessionId:'world.room.entered'and'world.room.left'.
- Use the
- Chat routing
EventTypes.PLAYER_MESSAGEis handled centrally inEngineService.type: 'say'→ room‑local;type: 'global'→ global broadcast. Don’t broadcast from TelnetServer.
- Room descriptions
- Call
worldManager.getRoomDescription(roomId, viewerSessionId)to show usernames and omit the viewer.
- Call
- Dialogue sub‑prompt
- The
CommandParsertracks dialogue mode per session (enterDialogueMode/exitDialogueMode/isInDialogueMode). - Telnet prompt shows
Dialogue>in mode;talk <npc>enters mode;leaveexits. - Web bridge emits “[Dialogue mode enabled/disabled]” and SPA shows a badge.
- The
- In
engine/modules/networking/command-parser.ts, callthis.registerCommand({ command, aliases, description, handler }). - Use
this.eventSystem.emit(new GameEvent('your.event', sessionId, undefined, { ... }))to decouple. - Return a user string; TelnetServer prints it and re‑prompts automatically.
Example handler body:
handler: async (sessionId, args) => {
this.eventSystem.emit(new GameEvent('feature.do', sessionId, undefined, { args }));
return ColorScheme.success('Requested.');
}If the command is feature‑scoped and you need EngineService, register via engine.service.ts using registerNetworkCommand during module init.
- Subscribe in the module that owns behavior (e.g., EngineService or WebClientService):
this.eventSystem.on('feature.done', (event) => {
const sessionId = String(event.source);
this.sendMessageToSession(sessionId, 'Done', 'system');
});Avoid cross‑layer side‑effects in TelnetServer; prefer EngineService for routing, and WebClientService for web emissions.
- Movement/rooms: Command in
command-parser.ts→ emit'player.move'→ handle in EngineService/WebClientService → callworldManager.movePlayer()andgetRoomDescription(). - Chat: Only emit
EventTypes.PLAYER_MESSAGEfrom commands. EngineService handles fan‑out (room/global). - Dialogue: Use
DialogueCommandHandlers. For UX, leverage CommandParser’s dialogue mode andleavecommand. - Web UI: Mirror behavior in
server/networking/web-client.service.tsand, if needed, updateclients/client.js.
npm run build
npm run server:devQuick checks:
- Telnet: Verify prompt changes and room notifications; try
say,chat,talk <npc>,leave. - Web: Open http://localhost:3000/, authenticate, verify the same flows and dialogue badge.
- Build:
npm run build(no TS errors) - Unit tests:
npm run test(if you added tests) - Lint:
npm run lint - Smoke: Start dev server, quick manual command checks
- Do not broadcast
PLAYER_MESSAGEin TelnetServer; EngineService owns routing. - For room events, ensure
event.targetis the sessionId of the moving player. - Always pass
viewerSessionIdtogetRoomDescriptionto get usernames and hide the viewer.
New event emission:
this.eventSystem.emit(new GameEvent('module.action', sessionId, targetId, { payload }));Send to a telnet session:
engine.sendMessageToSession(sessionId, 'Text', 'system');Broadcast excluding sender:
engine.broadcastMessage('Hello', 'broadcast', sessionId);That’s it—stay event‑driven, keep routing in EngineService/WebClientService, and favor small, testable handlers.
Content lives under engine/modules/world/content/:
engine/modules/world/content/
├─ world.json # World config (start room, meta)
├─ sectors/ # Areas with rooms and exits
│ ├─ town.json
│ ├─ forest.json
│ └─ castle.json
├─ npcs/ # Individual NPC definitions (JSON)
│ ├─ blacksmith.json
│ ├─ gate-guard.json
│ └─ ...
└─ dialogue/ # Dialogue trees + npc mapping
├─ npc-mappings.json # Map NPC ids → dialogue tree ids
├─ blacksmith.yaml
├─ guard.yaml
└─ ...
Files: engine/modules/world/content/sectors/*.json
- Structure (example keys; inspect existing sector files like
sectors/town.json):id,name,descriptionrooms: array of rooms withid,name,description,exits,itemsexits: objects like{ direction: "north", toRoomId: "eldoria:town_square", verbs: ["north"], ... }items: sector-level item definitions (objects withid,name, etc.)
- Steps:
- Copy an existing sector JSON as a template.
- Add/edit rooms and exits. Keep room ids namespaced:
namespace:roomId(e.g.,eldoria:town_square). - Ensure graph connectivity (each exit’s destination exists; consider return exits).
- If adding a new default spawn room, set the env var
MUD_DEFAULT_ROOM_IDto that room id.
- Verify:
- Build, start server,
lookin the room, try moving usingnorth/east/....
- Build, start server,
Files: engine/modules/world/content/npcs/*.json
- Minimal format (see world README for full schema):
{
"id": "blacksmith",
"name": "a sturdy blacksmith",
"description": "Soot-stained apron...",
"shortDescription": "blacksmith",
"dialogueProvider": "canned-branching",
"behaviors": ["merchant"],
"stats": { "level": 6, "health": 110 },
"spawnData": { "spawnRoomId": "town:blacksmith" }
}- Steps:
- Create or edit the JSON under
content/npcs/. - Ensure
spawnRoomIdpoints to an existing room id. - Optionally set
multiUserBehaviorand spawn/despawn conditions.
- Create or edit the JSON under
- Verify:
- Start server, move to the room; you should see the NPC in
lookoutput.
- Start server, move to the room; you should see the NPC in
Files: engine/modules/world/content/dialogue/
- Dialogue tree files: YAML (
*.yaml) or JSON with nodes/choices. - Map NPC ids to tree ids in
npc-mappings.json:
{
"blacksmith": "blacksmith",
"gate-guard": "guard"
}- Steps:
- Create or edit
blacksmith.yaml(tree id should match the file’s top-levelid). - Add mapping in
npc-mappings.json:"<npcId>": "<treeId>". - Ensure the NPC JSON sets
dialogueProvider(e.g.,canned-branching).
- Create or edit
- Verify:
- In game:
talk blacksmith→ see choices; use numbers/text;leaveto exit.
- In game:
id: blacksmith
name: Blacksmith Dialogue
version: 1.0.0
startNodeId: greeting
variables: { greeted: false }
nodes:
greeting:
id: greeting
npcMessage: |
Eh? What do you want?
choices:
- id: "1"
text: "Can you repair my gear?"
nextNodeId: repair
repair:
id: repair
npcMessage: "Let me see what you’ve got."
isEnd: trueTips:
- Use
variablesfor local conversation state. - Mark terminal nodes with
isEnd: true. - Conditions/actions are supported; see
engine/modules/dialogue/README.mdfor full reference.
In this repo, items are defined at the sector level and referenced by id in each room:
- Define items in the sector’s top-level
itemsarray (objects withid,name,description, etc.). Seesectors/town.jsonfor examples likewooden_cup,pipe,town_fountain. - Reference items in a room by id via the room’s
itemsarray, e.g."items": ["town_fountain"]. - Verify with
lookin the room.
File: engine/modules/world/content/world.json
- Lists which sector and npc files to load. It does not set the starting room.
- The preferred starting room is configured via the
MUD_DEFAULT_ROOM_IDenvironment variable (e.g.,eldoria:tavern). If unset/invalid, the first loaded room is used.
See also: engine/modules/world/README.md and docs/content-generation-guide.md for full schema and advanced topics.
npm run buildnpm run server:dev- Web client: http://localhost:3000/
- Auth,
look, move through exits, confirm NPC presence and dialogue
- Auth,
- Telnet: verify say/chat scoping and room enter/leave notifications