GMCP: Difference between revisions
Dorandraco (talk | contribs) Created page with "This document describes the GMCP (Generic MUD Communication Protocol) messages currently implemented by the Unwritten Legends server '''as observed in the 1.0 engine'''. It is intended for client authors (e.g., Mudlet packages) and for internal maintainers. == Transport & Negotiation == UL implements GMCP as a Telnet subnegotiation: * The server offers and accepts GMCP via Telnet '''DO/WILL GMCP'''. * After subnegotiation is active, the client typically sends <co..." |
Dorandraco (talk | contribs) mNo edit summary |
||
(One intermediate revision by the same user not shown) | |||
Line 1: | Line 1: | ||
This document describes the GMCP (Generic MUD Communication Protocol) messages currently implemented by the Unwritten Legends server '''as observed in the 1.0 engine'''. It is intended for client authors (e.g., Mudlet packages) and for internal maintainers. | This document describes the GMCP (Generic MUD Communication Protocol) messages currently implemented by the Unwritten Legends server '''as observed in the 1.0 engine'''. It is intended for client authors (e.g., Mudlet packages) and for internal maintainers. | ||
== Transport & Negotiation == | == Transport & Negotiation == | ||
Line 15: | Line 14: | ||
UL recognizes client <code>Core.Supports.Set/Add</code> and maintains a capability set but '''does not require''' capabilities to be declared to emit core messages. Custom UL messages are namespaced under <code>UL.*</code>. | UL recognizes client <code>Core.Supports.Set/Add</code> and maintains a capability set but '''does not require''' capabilities to be declared to emit core messages. Custom UL messages are namespaced under <code>UL.*</code>. | ||
=== Namespaces === | === Namespaces === | ||
Line 26: | Line 24: | ||
Versioning for the custom namespace is surfaced implicitly by the supported set (<code>"UL 1"</code>). If/when a breaking schema appears, we will add <code>"UL 2"</code> while leaving <code>UL 1</code> available for a deprecation window. | Versioning for the custom namespace is surfaced implicitly by the supported set (<code>"UL 1"</code>). If/when a breaking schema appears, we will add <code>"UL 2"</code> while leaving <code>UL 1</code> available for a deprecation window. | ||
== Emission Triggers & Frequency == | == Emission Triggers & Frequency == | ||
Line 37: | Line 34: | ||
'''Client guidance:''' handle idempotent updates and partial updates. Where keys are omitted on follow-up messages, merge into the client’s last known state. | '''Client guidance:''' handle idempotent updates and partial updates. Where keys are omitted on follow-up messages, merge into the client’s last known state. | ||
== Message Catalog == | == Message Catalog == | ||
Line 59: | Line 55: | ||
<li>'''When:''' once after <code>Core.Hello</code></li> | <li>'''When:''' once after <code>Core.Hello</code></li> | ||
<li>'''Schema:''' array of strings <code><Package> <major></code></li> | <li>'''Schema:''' array of strings <code><Package> <major></code></li> | ||
<li | <li>'''Example:''' <code>["Core 1", "Char 1", "Room 1", "External.Discord 1", "UL 1"]</code></li></ul> | ||
'''Also observed (client → server):''' <code>Core.Hello</code>, <code>Core.Supports.Set</code>, <code>Core.Supports.Add</code>. The server records client capabilities for future gating but does not currently require them to emit core messages. | |||
=== | === Room === | ||
==== Room.Info ==== | ==== Room.Info ==== | ||
Line 74: | Line 66: | ||
<li>'''Dir:''' server → client</li> | <li>'''Dir:''' server → client</li> | ||
<li>'''When:''' Login Snapshot; on room change; occasionally repeated for resilience</li> | <li>'''When:''' Login Snapshot; on room change; occasionally repeated for resilience</li> | ||
<li | <li>'''Schema:''' | ||
< | <pre>{ | ||
"area": string, | |||
"name": string, | |||
"exits": string[], | |||
"id": string | |||
}</pre></li> | |||
<li>'''Example:''' | |||
<li | <pre>{ | ||
< | "area": "City of Kaezar", | ||
"name": "Tempest Road at Avanil Way, Crossroads", | |||
"exits": [ "large stone building", "west", "south", "east", "north" ], | |||
"id": "8df6a052-9af2-5188-aff0-095e5d3d37a8" | |||
}</pre></li> | |||
</ul> | |||
Future (not yet emitted): <code>Room.Players</code>. | |||
=== | === Character Identity & State === | ||
==== Char.Info ==== | ==== Char.Info ==== | ||
Line 102: | Line 91: | ||
<li>'''Dir:''' server → client</li> | <li>'''Dir:''' server → client</li> | ||
<li>'''When:''' Login Snapshot; on identity changes (rare)</li> | <li>'''When:''' Login Snapshot; on identity changes (rare)</li> | ||
<li | <li>'''Schema:''' | ||
< | <pre>{ | ||
"pretitlename": string, | "pretitlename": string, // may be empty | ||
"name": string, // first name | "name": string, // first name | ||
"lastname": string, // surname | "lastname": string, // surname | ||
Line 118: | Line 107: | ||
"dominant_hand": string, // "left" | "right" | "dominant_hand": string, // "left" | "right" | ||
"level": number | "level": number | ||
}</li> | }</pre></li> | ||
<li | <li>'''Example:''' | ||
< | <pre>{ | ||
"pretitlename":"Sir", | "pretitlename":"Sir", | ||
"name":"Martaigne", | "name":"Martaigne", | ||
Line 135: | Line 124: | ||
"dominant_hand":"right", | "dominant_hand":"right", | ||
"level":100 | "level":100 | ||
}</li></ul> | }</pre></li></ul> | ||
==== Char.Status ==== | ==== Char.Status ==== | ||
Line 142: | Line 131: | ||
<li>'''Dir:''' server → client</li> | <li>'''Dir:''' server → client</li> | ||
<li>'''When:''' Login Snapshot; when status elements change</li> | <li>'''When:''' Login Snapshot; when status elements change</li> | ||
<li | <li>'''Schema:''' <code>{ "position": string, "stance": string }</code></li> | ||
<li><p>'''Example:''' <code>{ "position":"standing", "stance":"parry" }</code></p></li></ul> | <li><p>'''Example:''' <code>{ "position":"standing", "stance":"parry" }</code></p></li></ul> | ||
==== Char.Vitals ==== | ==== Char.Vitals ==== | ||
Line 151: | Line 140: | ||
<li>'''Dir:''' server → client</li> | <li>'''Dir:''' server → client</li> | ||
<li>'''When:''' Login Snapshot; Pulse; any change</li> | <li>'''When:''' Login Snapshot; Pulse; any change</li> | ||
<li | <li>'''Schema:''' | ||
< | <pre>{ | ||
"vitality": number, "vitality_max": number, | "vitality": number, "vitality_max": number, | ||
"essence": number, "essence_max": number, | "essence": number, "essence_max": number, | ||
"stamina": number, "stamina_max": number, | "stamina": number, "stamina_max": number, | ||
"willpower": number, "willpower_max": number | "willpower": number, "willpower_max": number | ||
}</li> | }</pre></li> | ||
<li | <li>'''Example:''' | ||
< | <pre>{ | ||
"vitality":320, | |||
"vitality_max":360, | |||
"essence":314, | |||
"essence_max":314, | |||
"stamina":310, | |||
"stamina_max":310, | |||
"willpower":218, | |||
"willpower_max":218 | |||
}</pre></li></ul> | |||
==== Char.Stats ==== | ==== Char.Stats ==== | ||
Line 166: | Line 164: | ||
<li>'''Dir:''' server → client</li> | <li>'''Dir:''' server → client</li> | ||
<li>'''When:''' Login Snapshot (full); Pulse (partial: often just <code>experience_total</code>); on changes</li> | <li>'''When:''' Login Snapshot (full); Pulse (partial: often just <code>experience_total</code>); on changes</li> | ||
<li | <li>'''Schema (full snapshot):''' | ||
< | <pre>{ | ||
"experience_total": number, | "experience_total": number, | ||
"experience_to_level": number, | "experience_to_level": number, | ||
Line 174: | Line 172: | ||
"arcana": number, | "arcana": number, | ||
"lessons": { "available": number, "max": number } | "lessons": { "available": number, "max": number } | ||
}</li> | }</pre></li> | ||
<li>'''Partial updates:''' UL may send only the keys that changed (e.g., <code>{ "experience_total": 217000943 }</code>). Clients should merge by key.</li> | <li>'''Partial updates:''' UL may send only the keys that changed (e.g., <code>{ "experience_total": 217000943 }</code>). Clients should merge by key.</li> | ||
<li | <li>'''Example (full):''' | ||
< | <pre>{ | ||
"experience_total":216265943, | "experience_total":216265943, | ||
"experience_to_level":4410000000, | "experience_to_level":4410000000, | ||
Line 184: | Line 182: | ||
"arcana":2265, | "arcana":2265, | ||
"lessons":{"available":4,"max":4} | "lessons":{"available":4,"max":4} | ||
}</li></ul> | }</pre></li></ul> | ||
=== UL (Unwritten Legends) — custom namespace === | |||
=== | |||
==== UL.Equipment.Set ==== | ==== UL.Equipment.Set ==== | ||
Line 196: | Line 191: | ||
<li>'''Dir:''' server → client</li> | <li>'''Dir:''' server → client</li> | ||
<li>'''When:''' Login Snapshot; on equipment/hand changes</li> | <li>'''When:''' Login Snapshot; on equipment/hand changes</li> | ||
<li | <li>'''Schema:''' <code>{ "slots": { "left": string, "right": string, ... } }</code> | ||
Slot keys are lowercase; value is an item short name or <code>"none"</code>.</li> | Slot keys are lowercase; value is an item short name or <code>"none"</code>.</li> | ||
<li | <li>'''Example:''' <code>{ "slots": { "left": "none", "right": "battle-worn t'elt claidheamh-mor" } }</code></li></ul> | ||
==== UL.Languages.Set ==== | ==== UL.Languages.Set ==== | ||
Line 207: | Line 201: | ||
<li>'''When:''' Login Snapshot; on language change</li> | <li>'''When:''' Login Snapshot; on language change</li> | ||
<li>'''Schema:''' <code>{"active": string, "known": string[]}</code> (all lowercase language ids)</li> | <li>'''Schema:''' <code>{"active": string, "known": string[]}</code> (all lowercase language ids)</li> | ||
<li | <li>'''Example:''' <code>{ "active": "common", "known": ["orcish","elvish","anjour", "common"] }</code></li></ul> | ||
==== UL.Spells.Set ==== | ==== UL.Spells.Set ==== | ||
Line 245: | Line 238: | ||
* '''Example:''' <code>{ "roundtime": 0, "stun": 0, "unconscious": 0 }</code> | * '''Example:''' <code>{ "roundtime": 0, "stun": 0, "unconscious": 0 }</code> | ||
=== External.Discord — presence [In Progress] === | |||
=== | |||
==== External.Discord.Info ==== | ==== External.Discord.Info ==== | ||
Line 263: | Line 253: | ||
<li>'''Dir:''' server → client</li> | <li>'''Dir:''' server → client</li> | ||
<li>'''When:''' Login Snapshot; on presence changes (details/state/party/time)</li> | <li>'''When:''' Login Snapshot; on presence changes (details/state/party/time)</li> | ||
<li | <li>'''Schema:''' | ||
< | <pre>{ | ||
"details": string, // e.g., "martaigne • Adventurer" | "details": string, // e.g., "martaigne • Adventurer" | ||
"state": string, // freeform (e.g., "Logging in...") | "state": string, // freeform (e.g., "Logging in...") | ||
Line 271: | Line 261: | ||
"partymax": number, | "partymax": number, | ||
"starttime": number // Unix epoch seconds | "starttime": number // Unix epoch seconds | ||
}</li> | }</pre></li> | ||
<li | <li>'''Example:''' | ||
< | <pre>{ | ||
"details":"martaigne • Adventurer", | "details":"martaigne • Adventurer", | ||
"state":"Logging in...", | "state":"Logging in...", | ||
Line 280: | Line 270: | ||
"partymax":1, | "partymax":1, | ||
"starttime":1756234455 | "starttime":1756234455 | ||
}</li></ul> | }</pre></li></ul> | ||
'''Observed (client → server):''' <code>External.Discord.Hello</code> with empty payload; UL treats support for <code>External.Discord</code> as sufficient to emit <code>Info</code>/<code>Status</code>. | |||
== Handshake Transcript (illustrative) == | |||
== | |||
<pre>GMCP <- Core.Hello { "client":"Mudlet", "version":"4.19.1" } | <pre>GMCP <- Core.Hello { "client":"Mudlet", "version":"4.19.1" } | ||
Line 296: | Line 283: | ||
... (Login Snapshot follows: Room.Info, Char.*, UL.*, External.Discord.*)</pre> | ... (Login Snapshot follows: Room.Info, Char.*, UL.*, External.Discord.*)</pre> | ||
== Client Expectations & Best Practices == | |||
== | |||
* Treat repeated messages as '''idempotent'''; ignore duplicates or update your HUD accordingly. | * Treat repeated messages as '''idempotent'''; ignore duplicates or update your HUD accordingly. | ||
Line 306: | Line 291: | ||
* Time values in <code>External.Discord.Status.starttime</code> are '''Unix epoch seconds'''. | * Time values in <code>External.Discord.Status.starttime</code> are '''Unix epoch seconds'''. | ||
== Future Additions (not yet emitted) == | |||
== | |||
* <code>Char.Affects.Set/Add/Remove</code> | * <code>Char.Affects.Set/Add/Remove</code> | ||
Line 315: | Line 297: | ||
* <code>UL.Info { version, schema }</code> (alternative to advertising via <code>Core.Hello</code>) | * <code>UL.Info { version, schema }</code> (alternative to advertising via <code>Core.Hello</code>) | ||
== Change Log == | |||
== | |||
* '''1.0''' — Initial public spec for engine v1.0. Includes Core/Char/Room/External.Discord, and UL namespace (<code>Equipment/Languages/Spells/MartialArt(s)/Timers</code>). | * '''1.0''' — Initial public spec for engine v1.0. Includes Core/Char/Room/External.Discord, and UL namespace (<code>Equipment/Languages/Spells/MartialArt(s)/Timers</code>). | ||
Latest revision as of 19:01, 30 August 2025
This document describes the GMCP (Generic MUD Communication Protocol) messages currently implemented by the Unwritten Legends server as observed in the 1.0 engine. It is intended for client authors (e.g., Mudlet packages) and for internal maintainers.
Transport & Negotiation
UL implements GMCP as a Telnet subnegotiation:
- The server offers and accepts GMCP via Telnet DO/WILL GMCP.
- After subnegotiation is active, the client typically sends
Core.Hello
and itsCore.Supports.Set/Add
list. UL tracks the client’s advertised packages. - The server replies with:
Core.Hello
(server identity/version)Core.Supports.Set
advertising what UL can emit.
Current advertised packages (server → client): ["Core 1", "Char 1", "Room 1", "External.Discord 1", "UL 1"]
UL recognizes client Core.Supports.Set/Add
and maintains a capability set but does not require capabilities to be declared to emit core messages. Custom UL messages are namespaced under UL.*
.
Namespaces
- Core.* – handshake and feature advertisement.
- Char.* – character identity, state, vitals, stats.
- Room.* – current room/area summary.
- External.Discord.* – rich presence metadata for Discord bridge.
- UL.* – Unwritten Legends–specific messages (equipment, languages, spells, martial arts, timers, etc.).
Versioning for the custom namespace is surfaced implicitly by the supported set ("UL 1"
). If/when a breaking schema appears, we will add "UL 2"
while leaving UL 1
available for a deprecation window.
Emission Triggers & Frequency
On login/character entry: the server emits a snapshot (marked Login Snapshot below).
On pulse: UL’s game loop runs a Pulse (about 5 s). Certain values (e.g., experience absorption, vitality regen) produce periodic GMCP updates even if unchanged elsewhere. Expect duplicates; clients should de-dup by content or simply accept repeats.
On change: Some messages are resent when values change (e.g., equipment, active language, room).
Client guidance: handle idempotent updates and partial updates. Where keys are omitted on follow-up messages, merge into the client’s last known state.
Message Catalog
Each entry lists Topic, Direction, When, Schema, and an Example.
Core
Core.Hello
- Dir: server → client
- When: after GMCP negotiation and after receiving client hello (once per session)
- Schema:
{ "client": string, "version": string }
- Example:
{ "client": "Unwritten Legends", "version": "1.0.0.0" }
Core.Supports.Set
- Dir: server → client
- When: once after
Core.Hello
- Schema: array of strings
<Package> <major>
- Example:
["Core 1", "Char 1", "Room 1", "External.Discord 1", "UL 1"]
Also observed (client → server): Core.Hello
, Core.Supports.Set
, Core.Supports.Add
. The server records client capabilities for future gating but does not currently require them to emit core messages.
Room
Room.Info
- Dir: server → client
- When: Login Snapshot; on room change; occasionally repeated for resilience
- Schema:
{ "area": string, "name": string, "exits": string[], "id": string }
- Example:
{ "area": "City of Kaezar", "name": "Tempest Road at Avanil Way, Crossroads", "exits": [ "large stone building", "west", "south", "east", "north" ], "id": "8df6a052-9af2-5188-aff0-095e5d3d37a8" }
Future (not yet emitted): Room.Players
.
Character Identity & State
Char.Info
- Dir: server → client
- When: Login Snapshot; on identity changes (rare)
- Schema:
{ "pretitlename": string, // may be empty "name": string, // first name "lastname": string, // surname "truename": string, "posttitle": string, // may be empty, includes punctuation "race": string, // lowercase "subrace": string, // lowercase "gender": string, // lowercase (e.g., "male", "female", "neuter") "age": number, // years "profession": string, // lowercase "deity": string, // lowercase "patronmoon": string, // lowercase "dominant_hand": string, // "left" | "right" "level": number }
- Example:
{ "pretitlename":"Sir", "name":"Martaigne", "lastname":"Shardleigh", "truename":"Martaigne", "posttitle":", Plydia's Scourge", "race":"human", "subrace":"kivian", "gender":"male", "age":57, "profession":"cleric", "deity":"thine", "patronmoon":"tallow", "dominant_hand":"right", "level":100 }
Char.Status
- Dir: server → client
- When: Login Snapshot; when status elements change
- Schema:
{ "position": string, "stance": string }
Example:
{ "position":"standing", "stance":"parry" }
Char.Vitals
- Dir: server → client
- When: Login Snapshot; Pulse; any change
- Schema:
{ "vitality": number, "vitality_max": number, "essence": number, "essence_max": number, "stamina": number, "stamina_max": number, "willpower": number, "willpower_max": number }
- Example:
{ "vitality":320, "vitality_max":360, "essence":314, "essence_max":314, "stamina":310, "stamina_max":310, "willpower":218, "willpower_max":218 }
Char.Stats
- Dir: server → client
- When: Login Snapshot (full); Pulse (partial: often just
experience_total
); on changes - Schema (full snapshot):
{ "experience_total": number, "experience_to_level": number, "fame": { "current": number, "lifetime": number }, "skillpoints": number, "arcana": number, "lessons": { "available": number, "max": number } }
- Partial updates: UL may send only the keys that changed (e.g.,
{ "experience_total": 217000943 }
). Clients should merge by key. - Example (full):
{ "experience_total":216265943, "experience_to_level":4410000000, "fame":{"current":102650,"lifetime":193400}, "skillpoints":635, "arcana":2265, "lessons":{"available":4,"max":4} }
UL (Unwritten Legends) — custom namespace
UL.Equipment.Set
- Dir: server → client
- When: Login Snapshot; on equipment/hand changes
- Schema:
{ "slots": { "left": string, "right": string, ... } }
Slot keys are lowercase; value is an item short name or"none"
. - Example:
{ "slots": { "left": "none", "right": "battle-worn t'elt claidheamh-mor" } }
UL.Languages.Set
- Dir: server → client
- When: Login Snapshot; on language change
- Schema:
{"active": string, "known": string[]}
(all lowercase language ids) - Example:
{ "active": "common", "known": ["orcish","elvish","anjour", "common"] }
UL.Spells.Set
- Dir: server → client
- When: Login Snapshot; when circles known change
- Schema:
{ "circles": string[] }
(string constants as used server-side) - Example:
{ "circles": ["VIA_FIDE_NIVI", "VIA_GLAS", "VIA_TEG"] }
UL.Spells.PreparedSpell
- Dir: server → client
- When: When spell is prepared or released
- Schema:
{ "spelllongname": string, "spellshortname": string }
- Example:
{ spelllongname = "Fire Cantrip", spellshortname = "vf01" }
UL.MartialArt.Active
- Dir: server → client
- When: Login Snapshot; when active style changes
- Schema:
{ "style": string, "moves": string[] }
- Example:
{ "style":"Qai Sun Crane", "moves":["crescentkick","spinkick","roundhouse"] }
UL.MartialArts.Set
- Dir: server → client
- When: Login Snapshot; when learned styles change
- Schema:
{ "styles": string[] }
- Example:
{ "styles":["Assault","Qai Sun Bear","Berserk","Brawling"] }
UL.Timers
- Dir: server → client
- When: Login Snapshot; Pulse; on timer changes
- Schema:
{ "roundtime": number, "stun": number, "unconscious": number }
(seconds) - Example:
{ "roundtime": 0, "stun": 0, "unconscious": 0 }
External.Discord — presence [In Progress]
External.Discord.Info
- Dir: server → client
- When: Login Snapshot (if client supports
External.Discord
); may be resent on config change - Schema:
{ "applicationid": string, "inviteurl": string, "gamename": string }
- Example:
{ "applicationid":"1409606830350401667", "inviteurl":"https://discord.gg/yourinvite", "gamename":"Unwritten Legends" }
- Known Issues: inviteurl is currently disabled from send as the Discord server is not public
External.Discord.Status
- Dir: server → client
- When: Login Snapshot; on presence changes (details/state/party/time)
- Schema:
{ "details": string, // e.g., "martaigne • Adventurer" "state": string, // freeform (e.g., "Logging in...") "largeimage": string, // key for Discord rich presence image "partysize": number, "partymax": number, "starttime": number // Unix epoch seconds }
- Example:
{ "details":"martaigne • Adventurer", "state":"Logging in...", "largeimage":"server-icon", "partysize":1, "partymax":1, "starttime":1756234455 }
Observed (client → server): External.Discord.Hello
with empty payload; UL treats support for External.Discord
as sufficient to emit Info
/Status
.
Handshake Transcript (illustrative)
GMCP <- Core.Hello { "client":"Mudlet", "version":"4.19.1" } GMCP <- Core.Supports.Set [ "Char 1", "Char.Skills 1", "Char.Items 1", "Room 1", "IRE.Rift 1", "IRE.Composer 1", "External.Discord 1", "Client.Media 1", "Char.Login 1" ] GMCP <- External.Discord.Hello GMCP -> Core.Hello { "client":"Unwritten Legends", "version":"1.0.0.0" } GMCP -> Core.Supports.Set ["Core 1","Char 1","Room 1","External.Discord 1","UL 1"] ... (Login Snapshot follows: Room.Info, Char.*, UL.*, External.Discord.*)
Client Expectations & Best Practices
- Treat repeated messages as idempotent; ignore duplicates or update your HUD accordingly.
- Expect partial updates (e.g.,
Char.Stats
with onlyexperience_total
). Merge by key. - Unknown keys are forward-compatible; ignore them safely.
- For
UL.*
messages, check for"UL 1"
inCore.Supports.*
if you want to be strict—UL currently emits them unconditionally to capable clients. - Time values in
External.Discord.Status.starttime
are Unix epoch seconds.
Future Additions (not yet emitted)
Char.Affects.Set/Add/Remove
- Incremental variants:
UL.Equipment.Add/Remove
, etc. UL.Info { version, schema }
(alternative to advertising viaCore.Hello
)
Change Log
- 1.0 — Initial public spec for engine v1.0. Includes Core/Char/Room/External.Discord, and UL namespace (
Equipment/Languages/Spells/MartialArt(s)/Timers
).