GMCP: Difference between revisions

From Unwritten Legends Wiki
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..."
 
reformatting
Line 52: Line 52:
<li>'''Schema:''' <code>{ "client": string, "version": string }</code></li>
<li>'''Schema:''' <code>{ "client": string, "version": string }</code></li>
<li>'''Example:''' <code>{ "client": "Unwritten Legends", "version": "1.0.0.0" }</code></li></ul>
<li>'''Example:''' <code>{ "client": "Unwritten Legends", "version": "1.0.0.0" }</code></li></ul>


==== Core.Supports.Set ====
==== Core.Supports.Set ====
Line 59: Line 60:
<li>'''When:''' once after <code>Core.Hello</code></li>
<li>'''When:''' once after <code>Core.Hello</code></li>
<li>'''Schema:''' array of strings <code>&lt;Package&gt; &lt;major&gt;</code></li>
<li>'''Schema:''' array of strings <code>&lt;Package&gt; &lt;major&gt;</code></li>
<li><p>'''Example:'''</p>
<li>'''Example:''' <code>["Core 1", "Char 1", "Room 1", "External.Discord 1", "UL 1"]</code></li></ul>
<source lang="json">["Core 1", "Char 1", "Room 1", "External.Discord 1", "UL 1"]</li></ul>


<blockquote>'''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.
'''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.
</blockquote>


-----


=== 4.2 Room ===
=== Room ===


==== Room.Info ====
==== Room.Info ====
Line 74: Line 72:
<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><p>'''Schema:'''</p>
<li>'''Schema:'''
<source lang="json">{ "area": string, "name": string }</li>
<pre>{
<li></li>
  "area": string,
<li>or</li>
  "name": string,
<li><source lang="json">{ "exits": { string, ... } }</li>
  "exits": string[],
<li></li>
  "id": string
<li>or</li>
}</pre></li>
<li><source lang="json">{ "id": { string } }</li>
<li>'''Example:'''
<li><p>'''Example:'''</p>
<pre>{
<source lang="json">{ "area": "City of Kaezar", "name": "Tempest Road at Avanil Way, Crossroads" }</li>
  "area": "City of Kaezar",
<li><p>or</p>
  "name": "Tempest Road at Avanil Way, Crossroads",
<source lang="json">{ "exits": { "large stone building", "west", "south", "east", "north" } }</li>
  "exits": [ "large stone building", "west", "south", "east", "north" ],
<li></li>
  "id": "8df6a052-9af2-5188-aff0-095e5d3d37a8"
<li>or</li>
}</pre></li>
<li><source lang="json">{ "id": { "8df6a052-9af2-5188-aff0-095e5d3d37a8" } }</li></ul>
</ul>


<blockquote>Future (not yet emitted): <code>Room.Players</code>.
Future (not yet emitted): <code>Room.Players</code>.
</blockquote>


-----


=== 4.3 Character Identity &amp; State ===
=== Character Identity &amp; State ===


==== Char.Info ====
==== Char.Info ====
Line 102: Line 98:
<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><p>'''Schema:'''</p>
<li>'''Schema:'''
<source lang="json">{
<pre>{
   "pretitlename": string,     // may be empty
   "pretitlename": string,     // may be empty
   "name": string,              // first name
   "name": string,              // first name
   "lastname": string,          // surname
   "lastname": string,          // surname
Line 118: Line 114:
   "dominant_hand": string,    // "left" | "right"
   "dominant_hand": string,    // "left" | "right"
   "level": number
   "level": number
}</li>
}</pre></li>
<li><p>'''Example:'''</p>
<li>'''Example:'''
<source lang="json">{
<pre>{
   "pretitlename":"Sir",
   "pretitlename":"Sir",
   "name":"Martaigne",
   "name":"Martaigne",
Line 135: Line 131:
   "dominant_hand":"right",
   "dominant_hand":"right",
   "level":100
   "level":100
}</li></ul>
}</pre></li></ul>
 


==== Char.Status ====
==== Char.Status ====
Line 142: Line 139:
<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><p>'''Schema:'''</p>
<li>'''Schema:''' <code>{ "position": string, "stance": string }</code></li>
<source lang="json">{ "position": string, "stance": string }</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 148:
<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><p>'''Schema:'''</p>
<li>'''Schema:'''
<source lang="json">{
<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><p>'''Example:'''</p>
<li>'''Example:'''
<source lang="json">{ "vitality":320, "vitality_max":360, "essence":314, "essence_max":314, "stamina":310, "stamina_max":310, "willpower":218, "willpower_max":218 }</li></ul>
<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 173:
<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><p>'''Schema (full snapshot):'''</p>
<li>'''Schema (full snapshot):'''
<source lang="json">{
<pre>{
   "experience_total": number,
   "experience_total": number,
   "experience_to_level": number,
   "experience_to_level": number,
Line 174: Line 181:
   "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><p>'''Example (full):'''</p>
<li>'''Example (full):'''
<source lang="json">{
<pre>{
   "experience_total":216265943,
   "experience_total":216265943,
   "experience_to_level":4410000000,
   "experience_to_level":4410000000,
Line 184: Line 191:
   "arcana":2265,
   "arcana":2265,
   "lessons":{"available":4,"max":4}
   "lessons":{"available":4,"max":4}
}</li></ul>
}</pre></li></ul>




-----
=== UL (Unwritten Legends) — custom namespace ===
 
=== 4.4 UL (Unwritten Legends) — custom namespace ===


==== UL.Equipment.Set ====
==== UL.Equipment.Set ====
Line 196: Line 201:
<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><p>'''Schema:'''</p>
<li>'''Schema:''' <code>{ "slots": { "left": string, "right": string, ... } }</code>
<source lang="json">{ "slots": { "left": string, "right": string, ... } }
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><p>'''Example:''' <code>{ "slots": { "left": "none", "right": "battle-worn t'elt claidheamh-mor" } }</code></p></li></ul>
<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 211:
<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><p>'''Example:'''</p>
<li>'''Example:''' <code>{ "active": "common", "known": ["orcish","elvish","anjour", "common"] }</code></li></ul>
<source lang="json">{ "active": "common", "known": ["orcish","elvish","anjour", "common"] }</li></ul>


==== UL.Spells.Set ====
==== UL.Spells.Set ====
Line 246: Line 249:




-----
=== External.Discord — presence [In Progress] ===
 
=== 4.5 External.Discord — presence [In Progress] ===


==== External.Discord.Info ====
==== External.Discord.Info ====
Line 263: Line 264:
<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><p>'''Schema:'''</p>
<li>'''Schema:'''
<source lang="json">{
<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 272:
   "partymax": number,
   "partymax": number,
   "starttime": number      // Unix epoch seconds
   "starttime": number      // Unix epoch seconds
}</li>
}</pre></li>
<li><p>'''Example:'''</p>
<li>'''Example:'''
<source lang="json">{
<pre>{
   "details":"martaigne • Adventurer",
   "details":"martaigne • Adventurer",
   "state":"Logging in...",
   "state":"Logging in...",
Line 280: Line 281:
   "partymax":1,
   "partymax":1,
   "starttime":1756234455
   "starttime":1756234455
}</li></ul>
}</pre></li></ul>


<blockquote>'''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>.
'''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>.
</blockquote>


-----


== 5) Handshake Transcript (illustrative) ==
== Handshake Transcript (illustrative) ==


<pre>GMCP &lt;- Core.Hello { "client":"Mudlet", "version":"4.19.1" }
<pre>GMCP &lt;- Core.Hello { "client":"Mudlet", "version":"4.19.1" }
Line 296: Line 295:
... (Login Snapshot follows: Room.Info, Char.*, UL.*, External.Discord.*)</pre>
... (Login Snapshot follows: Room.Info, Char.*, UL.*, External.Discord.*)</pre>


-----


== 6) Client Expectations &amp; Best Practices ==
== Client Expectations &amp; 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 307: Line 305:




-----
== Future Additions (not yet emitted) ==
 
== 7) Future Additions (not yet emitted) ==


* <code>Char.Affects.Set/Add/Remove</code>
* <code>Char.Affects.Set/Add/Remove</code>
Line 316: Line 312:




-----
== Change Log ==
 
== 8) 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>).
-----
''Questions or schema proposals?'' Open an issue, or add examples at the bottom of this doc and tag maintainers.

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 its Core.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 only experience_total). Merge by key.
  • Unknown keys are forward-compatible; ignore them safely.
  • For UL.* messages, check for "UL 1" in Core.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 via Core.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).