MoralDecay implements the Generic MUD Communication Protocol (GMCP, telnet option 201) to push structured, out-of-band game data to clients — vitals, room information, combat targets, chat, and GUI/map auto-configuration for Mudlet. This document is intended for players and developers building GMCP-enabled clients or scripts.
Core.Supports.Set. The server sends its fixed set of packages to any connection that successfully negotiates GMCP, regardless of what the client claims to support. There is no per-package opt-in/opt-out on the wire, aside from the GUI auto-push toggle noted in §3.
playdecay.com:3003) — full GMCP support, telnet-negotiated normally by the driver. This is the only path GMCP-enabled clients should use.mud-websockify (public port 2023) → mud-telnet-proxy (internal, localhost-only port 3004) → driver (port 3003). The internal proxy is a small purpose-built script that negotiates just enough telnet (NAWS, TTYPE, ECHO) for the xterm.js terminal to work correctly in a browser. It does not recognize the GMCP telnet option (201) and actively refuses (WONT/DONT) any option it doesn't explicitly know, and it discards the payload of any IAC SB … IAC SE subnegotiation block for unrecognized options — exactly the framing GMCP uses. As a result, GMCP packets never reach the browser client, by design.
127.0.0.1 only and is not reachable from outside the server — it isn't an alternate port you can connect a client to; it's purely an internal hop behind the public websocket endpoint (port 2023). If you're building a GMCP-aware client, connect directly to port 3003 instead.
Core.Hello {"name": "MoralDecay", "version": "1.0"}
Core.Hello is sent; nothing else follows immediately.Room.Info, Char.Vitals, Char.Stats, Char.Status, Char.Limbs, Client.Map, Client.GUI) is sent once login completes and the player actually enters the game.Core.Hello. Initialize your UI from the actual Room.Info/Char.Vitals packets whenever they arrive, not from the negotiation handshake itself.
All packages below are push-only; the server does not respond to client-initiated requests for room/character data (see §4 for the one exception).
{"name": "MoralDecay", "version": "1.0"}
Sent once, at GMCP negotiation.
{ "num": 123456789, "name": "A dusty crossroads", "area": "Candera", "terrain": "indoors", "exits": {"n": 234567890, "e": 345678901}, "coord": {"x": 3, "y": -2, "z": 0}, "room_type": "shop" }
Sent on login and every room move.
num — a stable 31-bit hash of the room's file path. Clone/instance suffixes (#123) are stripped before hashing, so all instances of the same room template share the same num — it cannot distinguish two live instances of one room.exits — keys are abbreviated directions (n s e w u d ne nw se sw), or the raw exit-command string otherwise (e.g. "enter"). Values are the destination room's num hash — an integer, not a name or path. Build your own id → data cache from prior Room.Info packets to resolve these.coord and room_type are optional — present only for rooms with map-coordinate data or explicit typing. Most rooms omit them.{"hp": 87, "maxhp": 100, "mp": 42, "maxmp": 60}
Sent on login, heartbeat healing ticks, mana change, and every hit taken.
hp is a percentage (0–100), not an absolute hit-point value — this is deliberate; real HP totals are not exposed over GMCP. maxhp is always literally 100; treat hp purely as a percent bar.move/maxmove field — MoralDecay has no separate stamina/movement stat.{"str": "strong", "dex": "agile", "con": "hardy", "int": "clever", "wis": "wise", "cha": "charming"}
All six values are adjective strings, not numbers, matching the wording in score. Sent only at login/reconnect — stat changes from leveling, death penalties, or gear are not currently re-pushed, so this can go stale mid-session. Reconnect to refresh.
{ "name": "Excelsior", "level": 42, "class": "warrior", "race": "human", "xp": 15000, "align": "Good", "silver": 3200, "wiz_level": 0, "wizard": false }
Sent at login and on XP gain/loss (including party XP splits).
xp is spendable XP only, not lifetime/total XP earned.align is a display string ("Good", "Neutral", "Evil", etc.), not a numeric value.silver is total money carried, in silver coins.align and silver are only refreshed opportunistically alongside an XP event — not independently hooked — so they can lag until the next XP tick or reconnect.{"head": "is in excellent condition", "left arm": "is wounded", "right leg": "is LOST"}
One key per body part, keyed by the limb's name (varies by race — no fixed schema). Values are free-text condition phrases matching diagnose/health output. Sent at login, on heartbeat, and on every hit taken.
{ "name": "a snarling wolf", "race": "wolf", "condition": "is badly hurt and bleeding", "lost_limbs": ["left ear"], "equipment": "It is wearing a studded collar." }
Sent on examine, when your main attacker changes, and when attackers are added/removed during combat. Cleared with {"name": ""} when the target is lost or combat ends.
condition is one of eight fuzzy tiers matching what examine shows — no numeric HP is ever included for targets (contrast with your own Char.Vitals, which does send a numeric percentage).equipment is a single pre-formatted text blob (raw list_equipment() output, embedded newlines as \n), not a structured array — parse it as text for per-item data.{"chan": "tell", "player": "excelsior", "msg": "hey there", "direction": "in", "targets": ["someoneelse"]}
Sent for shout, tell, party, wiz, and aristo channel traffic.
chan is one of shout, tell, party, wiz, aristo.direction (in/out) and targets (array of recipient names) are only present on tells.wiz packets are only sent to wizards; aristo packets only to players holding the aristocracy item. Ineligible players simply never receive these — there's no explicit "channel unavailable" signal.{"url": "https://www.playdecay.com/downloads/gui/newbie_map.dat"}
Points Mudlet at a newbie-area map file for auto-load. Sent once at login.
{"version": "1.0.7", "url": "https://www.playdecay.com/downloads/gui/MoralDecay-GUI.mpackage"}
Points Mudlet at the official GUI package for auto-install/update; Mudlet re-fetches only when version changes. Sent once at login.
Client.GUI does not necessarily mean anything is broken.
External.Discord.Info {"inviteurl": "", "applicationid": "1511789438747803698", "largeimagehash": "", "largeimagename": "moraldecay", "smallimagehash": "", "smallimagename": "", "buttons": []} External.Discord.Status {"game": "MoralDecay", "user": "excelsior", "details": "excelsior", "partysize": 0, "partymax": 0, "starttime": 1785600000}
Sent in response to the client sending External.Discord.Hello (§4).
partysize/partymax are always 0 — not currently populated despite the fields existing.starttime is a Unix timestamp of login, intended for Discord Rich Presence "elapsed time" display.Status is deferred until the player file is loaded, then sent afterward.Only one inbound GMCP message is recognized:
External.Discord.Hello
Triggers the server to reply with External.Discord.Info followed by External.Discord.Status (§3). Any other client-sent GMCP package or message — including Core.Supports.Set — is silently ignored.
Char.Stats and parts of Char.Status (align, silver) can go stale mid-session; a reconnect forces a fresh snapshot.Core.Supports.Set list to inspect — build clients assuming the fixed package set documented above.Room.Info.num cannot distinguish multiple live instances of the same room.Target.Info and Char.Vitals intentionally differ in precision (fuzzy text vs. numeric percent) — by design, not a bug.Client.GUI/Client.Map may never arrive for a given player if they've opted out — don't treat their absence as an error.| Package | Direction | Trigger |
|---|---|---|
Core.Hello | S → C | GMCP negotiation |
Room.Info | S → C | Login, room move |
Char.Vitals | S → C | Login, heartbeat, hits, mana change |
Char.Stats | S → C | Login/reconnect only |
Char.Status | S → C | Login, XP change |
Char.Limbs | S → C | Login, heartbeat, hits |
Target.Info | S → C | Examine, attacker change, combat end |
Comm.Channel | S → C | Channel traffic |
Client.Map | S → C | Login (if enabled) |
Client.GUI | S → C | Login (if enabled) |
External.Discord.Info/Status | S → C | In reply to External.Discord.Hello |
External.Discord.Hello | C → S | Sent by client to request Discord data |