GMCP

From Unwritten Legends Wiki
Revision as of 19:01, 30 August 2025 by Dorandraco (talk | contribs) (reformatting)

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).