forums wiki bugs items changes map login play now

GMCP

GMCP (Generic MUD Communication Protocol)

Aabahran supports GMCP over telnet option 201. GMCP allows MUD clients to receive structured data as JSON alongside the normal text stream. This enables modern client features like vitals gauges, buff bars, combat HUDs, party frames, chat tabs, automappers, and clickable room panels.

Telnet Negotiation

GMCP uses telnet option 201 (0xC9). When you connect, the server sends:

IAC WILL GMCP        (FF FB C9)

Your client should respond with:

IAC DO GMCP          (FF FD C9)

Once negotiated, GMCP messages are exchanged inside telnet subnegotiations:

IAC SB GMCP <payload> IAC SE
(FF FA C9 ... FF F0)

The payload is a UTF-8 string in the format:

Package.Name {json}

For example:

Char.Vitals {"hp":340,"maxhp":500,"mana":200,"maxmana":200,"move":150,"maxmove":150}

IAC State Machine

To extract GMCP messages from the telnet stream, your client needs to track IAC sequences. Here is a minimal state machine:

State Byte Action
0 (normal) FF (IAC) Go to state 1
0 (normal) anything else Append to text output
1 (got IAC) FF Escaped 0xFF literal, append to text, go to state 0
1 (got IAC) FB/FC/FD/FE (WILL/WONT/DO/DONT) Save verb, go to state 2
1 (got IAC) FA (SB) Start subnegotiation buffer, go to state 3
1 (got IAC) anything else Ignore (GA, NOP, etc.), go to state 0
2 (got verb) any byte This is the option number. If WILL 0xC9, respond DO 0xC9. Go to state 0
3 (in subneg) FF (IAC) Go to state 4
3 (in subneg) anything else Append to subneg buffer
4 (IAC in subneg) F0 (SE) Subneg complete. If first byte is 0xC9, the rest is a GMCP message. Go to state 0
4 (IAC in subneg) FF Escaped 0xFF inside subneg, append to buffer, go to state 3
4 (IAC in subneg) anything else Protocol error, go to state 0

When you receive a complete subneg where the first byte is 0xC9 (GMCP), strip that byte and split the remaining string at the first space. Everything before the space is the package name, everything after is the JSON body.

GMCP Packages

Char.Vitals

Current and maximum values for hit points, mana, and movement.

When sent: Every prompt update (after each command)

{
  "hp": 340,
  "maxhp": 500,
  "mana": 200,
  "maxmana": 200,
  "move": 150,
  "maxmove": 150
}

Usage: Build a vitals gauge or health bar. Divide hp by maxhp for a percentage. Consider color thresholds (red below 25%, yellow below 50%, green above).


Char.Status

Character identity and progression info.

When sent: Login, level change

{
  "name": "Tharok",
  "level": 42,
  "race": "human",
  "class": "warrior"
}

Usage: Display character identity in your UI header.


Char.Affects

All active spell and skill effects on the character. Hidden affects (sneak, noquit, etc.) are filtered out.

When sent: Login, and whenever an affect is added or removed

{
  "affects": [
    {
      "name": "armor",
      "duration": 12,
      "level": 30,
      "location": "armor class",
      "modifier": -20
    },
    {
      "name": "bless",
      "duration": -1,
      "level": 40,
      "location": "hit roll",
      "modifier": 2
    }
  ]
}
Field Type Description
name string Spell or skill name
duration integer Ticks remaining. -1 means permanent
level integer Level at which the affect was applied
location string What stat is modified (see table below)
modifier integer How much the stat is changed

Location values: "none", "strength", "dexterity", "intelligence", "wisdom", "constitution", "armor class", "hit roll", "damage roll", "hp", "mana", "moves", "save vs spell", "save vs affliction", "save vs malediction", "save vs mental", "save vs breath", "luck", "spell level", "hp regen", "mana regen", "move regen", "spell cost", "ac vs pierce", "ac vs bash", "ac vs slash", "ac vs exotic", "move cost"

Usage: Build a buff bar or debuff tracker. Show icons or text for each active affect. Use duration for countdown timers. Flash or highlight affects that are about to expire (duration of 1 or 2).

Note: The full affects list is sent every time any single affect changes. This means you can simply replace your entire buff bar state each time rather than tracking individual add/remove events.


Char.Combat

Information about the current combat target.

When sent: Every prompt update, and when combat ends

When fighting:

{
  "target": "a black dragon",
  "condition": "pretty hurt",
  "hp_pct": 15
}

When not fighting:

{}
Field Type Description
target string Name of your opponent as you see them
condition string Descriptive condition string
hp_pct integer Target's health as a percentage (0-100+)

Condition strings: "excellent", "a few scratches", "small wounds", "quite a few wounds", "big nasty wounds", "pretty hurt", "awful", "bleeding to death"

Usage: Build a target frame showing enemy name and health bar. Use hp_pct for a precise gauge and condition for a text label. When the object is empty {}, hide the combat display.


Char.Worth

Economic and progression data.

When sent: Every prompt update, login

{
  "gold": 1500,
  "bank": 25000,
  "exp": 48230,
  "tnl": 12770,
  "trains": 3,
  "practices": 15
}
Field Type Description
gold integer Gold carried
bank integer Gold in the bank
exp integer Total experience points
tnl integer Experience remaining until next level
trains integer Training sessions available
practices integer Practice sessions available

Usage: Display wealth and progression in a stats panel. Show an XP bar using tnl relative to your level's total requirement.


Group.Info

Information about your group members.

When sent: Every prompt update (while grouped), group add/remove, login

When grouped:

{
  "leader": "Kaelith",
  "members": [
    {
      "name": "Kaelith",
      "level": 40,
      "class": "warrior",
      "hp_pct": 95,
      "mana_pct": 60,
      "move_pct": 100,
      "tnl": 12770
    },
    {
      "name": "Lyria",
      "level": 38,
      "class": "healer",
      "hp_pct": 72,
      "mana_pct": 30,
      "move_pct": 88,
      "tnl": 5000
    }
  ]
}

When not grouped:

{}
Field Type Description
leader string Group leader's name
members array List of group member objects
members[].name string Member name
members[].level integer Member level
members[].class string Class name
members[].hp_pct integer HP percentage (0-100)
members[].mana_pct integer Mana percentage (0-100)
members[].move_pct integer Movement percentage (0-100)
members[].tnl integer Experience to next level

Usage: Build party frames with health/mana bars for each group member. When the object is empty {}, hide the group display. The leader is always included in the members list.


World.Time

In-game time and weather conditions.

When sent: Login, and every in-game hour (weather tick)

{
  "hour": 14,
  "day": 5,
  "month": 3,
  "year": 842,
  "sunlight": "day",
  "sky": "raining"
}
Field Type Description
hour integer Current hour of the in-game day
day integer Day of the month
month integer Month of the year
year integer Year
sunlight string Light level
sky string Weather condition

Sunlight values: "dark", "rise", "light", "set"

Sky values: "cloudless", "cloudy", "raining", "lightning"

Usage: Display a day/night indicator and weather icon. Adjust your UI theme or map colors based on time of day. Track in-game calendar for roleplay purposes.


Comm.Channel

Chat channel messages delivered to your character.

When sent: Whenever a channel message is received

{
  "channel": "yell",
  "speaker": "Valdric",
  "text": "Anyone seen a healer near Seringale?"
}
Field Type Description
channel string Channel identifier
speaker string Who sent the message
text string Message content (color codes stripped)

Channel values: "pray", "cabal", "clan", "faction", "newbie", "yell", "say", "tell", "reply"

Usage: Route messages to separate chat tabs based on channel. Build a chat log with speaker names and timestamps. Apply different colors per channel. You also receive your own messages on the channel so your client can log them.

Note: Both speaker and text have MUD color codes stripped and special characters JSON-escaped. The text is the original (untranslated) message content.


Room.Info

Room identity, area, terrain, and exits.

When sent: Room look (movement, look command)

{
  "num": 3001,
  "name": "The Temple of Midgaard",
  "area": "Midgaard",
  "terrain": "city",
  "exits": {
    "north": 3002,
    "south": 3000,
    "east": 3003
  }
}
Field Type Description
num integer Room vnum
name string Room title (color codes stripped)
area string Area name
terrain string Sector type (see table below)
exits object Direction name to destination vnum

Terrain types:

Value Terrain
inside Indoor room
city City streets
field Open field / grassland
forest Forest / woods
hills Hilly terrain
mountain Mountains
water_swim Shallow / swimmable water
water_noswim Deep water (requires boat/fly)
swamp Swamp / bog
air Open air (requires fly)
desert Desert
lava Lava / volcanic
snow Snow / frozen terrain

Exit directions: north, south, east, west, up, down

Usage: Display room name, area, and terrain in your UI. Build an automapper using vnum + exits to track connections between rooms. Color-code terrain types on your map.


Room.Chars

Characters visible in the current room (excluding yourself).

When sent: Room look (same time as Room.Info)

[
  {"name": "a city guard", "npc": true},
  {"name": "Valdric", "npc": false}
]
Field Type Description
name string Character name as you see them (color codes stripped)
npc boolean true for NPCs/mobs, false for players

Usage: Build a room occupants panel. Distinguish NPCs from players with different icons or colors. Make names clickable for look <name> or kill <name> commands.


Room.Items

Objects visible on the ground in the current room.

When sent: Room look (same time as Room.Info)

[
  {"name": "a gleaming longsword", "type": "weapon"},
  {"name": "a leather satchel", "type": "container"}
]
Field Type Description
name string Item short description (color codes stripped)
type string Item type

Item types: "light", "scroll", "wand", "staff", "weapon", "treasure", "armor", "potion", "clothing", "furniture", "trash", "container", "drink", "key", "food", "money", "boat", "corpse", "fountain", "pill", "map", "gem", "jewelry", and others

Usage: Build a room items panel. Make items clickable for get <name> or look <name>. Use different icons based on item type (sword for weapons, shield for armor, etc.).


Map.Tiles

A grid of tiles representing the surrounding area for minimap rendering.

When sent: Room look (same time as Room.Info)

{
  "r": 4,
  "g": [
    [null, null, {"s":3,"e":"ns"}, null, null, null, null, null, null],
    [null, null, {"s":3,"e":"nse"}, {"s":2,"e":"ew"}, null, null, null, null, null],
    [null, null, {"s":1,"e":"nsew","h":1}, {"s":1,"e":"ew"}, {"s":2,"e":"w"}, null, null, null, null]
  ]
}
Field Type Description
r integer Radius of the map (player is at center)
g array 2D grid of cells, size = (2 * r + 1) x (2 * r + 1)

Each cell in the grid is either null (unexplored / no room) or an object:

Field Type Description
s integer Sector type (0-12, see table below)
e string Exit characters: n e s w u d
h integer Present and set to 1 if this is the player's current room

Sector type IDs (numeric, matching the terrain strings from Room.Info):

ID Terrain Suggested Color
0 inside Warm brown #5a4e3c
1 city Gray #787369
2 field Green #3c6928
3 forest Dark green #1c4616
4 hills Earth brown #695532
5 mountain Cold gray #55555f
6 water (swim) Light blue #284b87
7 water (deep) Dark blue #14235a
8 swamp Murky green #374628
9 air Sky blue #6473a5
10 desert Sand #af9150
11 lava Orange-red #af3214
12 snow Light gray #afb9c3

Exit characters encode which directions have exits from that room:

Char Direction Grid offset
n north y - 1
e east x + 1
s south y + 1
w west x - 1
u up (no grid representation)
d down (no grid representation)

Uppercase exit characters (e.g., N, E) indicate exits that lead to non-adjacent rooms (the destination is more than one tile away on the grid, such as a portal or a long corridor).

Usage: Render a minimap. The player is always at the center of the grid (r, r). Draw each non-null cell as a colored tile based on sector type. Draw corridors between adjacent rooms using the exit characters. Highlight the player's room (where h is 1).


Module Summary

Package When Sent Purpose
Char.Vitals Every prompt HP, mana, move bars
Char.Status Login, level up Name, level, race, class
Char.Affects Login, affect change Buff/debuff bar
Char.Combat Every prompt Target health bar
Char.Worth Every prompt, login Gold, exp, trains, practices
Group.Info Prompt (grouped), group change, login Party frames
World.Time Login, weather tick Day/night, weather
Comm.Channel Channel message Chat tabs
Room.Info Room look Room name, area, exits
Room.Chars Room look NPCs and players in room
Room.Items Room look Items on the ground
Map.Tiles Room look Minimap grid

Echo Toggle (Password Prompts)

The server uses standard telnet echo negotiation for password prompts:

IAC WILL ECHO   (FF FB 01)   -> Server will handle echo (client should HIDE input)
IAC WONT ECHO   (FF FC 01)   -> Server stops echoing (client should SHOW input)

When IAC WILL ECHO is received, switch your input field to password mode (mask characters). When IAC WONT ECHO is received, switch back to normal text input.

ANSI Color Codes

The server sends two types of ANSI color sequences:

Type Wire format Example ESC byte present?
16-color [0;31m Red text No, bare CSI, no 0x1B prefix
256-color \x1b[38;5;166m Orange text Yes, standard CSI
Reset [0m Reset all No
Clear screen [2J Clear No

If your client uses a standard terminal emulator (like xterm.js), you need to fix up the bare 16-color sequences by prepending 0x1B (ESC) before any [ that starts a CSI sequence but isn't already preceded by ESC.

Fixup algorithm: Scan the byte stream. When you encounter [ (0x5B) that is NOT preceded by 0x1B, look ahead: if the bytes after [ are digits and/or semicolons followed by a letter (0x40-0x7E), it's a bare CSI sequence, prepend 0x1B. This is safe because literal brackets in prose (e.g., [Exits: north]) won't have digits immediately after the [.

Example: Minimal GMCP Client in JavaScript

// Telnet constants
const IAC = 0xFF, SB = 0xFA, SE = 0xF0;
const WILL = 0xFB, WONT = 0xFC, DO = 0xFD, DONT = 0xFE;
const GMCP = 0xC9; // 201

// State machine
let iacState = 0, iacVerb = 0;
let subneg = [];
let textBuf = [];

function processBytes(data) {
    for (let i = 0; i < data.length; i++) {
        const b = data[i];

        switch (iacState) {
        case 0: // normal
            if (b === IAC) iacState = 1;
            else textBuf.push(b);
            break;

        case 1: // got IAC
            if (b === IAC) { textBuf.push(0xFF); iacState = 0; }
            else if (b === WILL || b === WONT || b === DO || b === DONT)
                { iacVerb = b; iacState = 2; }
            else if (b === SB) { subneg = []; iacState = 3; }
            else iacState = 0;
            break;

        case 2: // got IAC + verb, b = option
            if (iacVerb === WILL && b === GMCP) {
                // Respond: DO GMCP
                send(new Uint8Array([IAC, DO, GMCP]));
            }
            if (b === 0x01) { // TELOPT_ECHO
                if (iacVerb === WILL) setPasswordMode(true);
                else if (iacVerb === WONT) setPasswordMode(false);
            }
            iacState = 0;
            break;

        case 3: // inside subneg
            if (b === IAC) iacState = 4;
            else subneg.push(b);
            break;

        case 4: // IAC inside subneg
            if (b === SE) {
                handleSubneg(subneg);
                iacState = 0;
            } else if (b === IAC) {
                subneg.push(0xFF);
                iacState = 3;
            } else iacState = 0;
            break;
        }
    }

    // Flush text to terminal
    if (textBuf.length > 0) {
        writeToTerminal(fixupANSI(new Uint8Array(textBuf)));
        textBuf = [];
    }
}

function handleSubneg(data) {
    if (data.length < 1 || data[0] !== GMCP) return;

    // Decode: strip option byte, split "Package.Name {json}"
    const payload = new TextDecoder().decode(new Uint8Array(data.slice(1)));
    const spaceIdx = payload.indexOf(' ');
    if (spaceIdx < 0) return;

    const pkg = payload.substring(0, spaceIdx);
    const json = JSON.parse(payload.substring(spaceIdx + 1));

    switch (pkg) {
    case 'Char.Vitals':
        updateVitals(json);
        break;
    case 'Char.Status':
        updateStatus(json);
        break;
    case 'Char.Affects':
        updateAffects(json.affects);
        break;
    case 'Char.Combat':
        updateCombat(json);
        break;
    case 'Char.Worth':
        updateWorth(json);
        break;
    case 'Group.Info':
        updateGroup(json);
        break;
    case 'World.Time':
        updateTime(json);
        break;
    case 'Comm.Channel':
        appendChat(json.channel, json.speaker, json.text);
        break;
    case 'Room.Info':
        updateRoom(json);
        break;
    case 'Room.Chars':
        updateRoomChars(json);
        break;
    case 'Room.Items':
        updateRoomItems(json);
        break;
    case 'Map.Tiles':
        renderMap(json);
        break;
    }
}

Mudlet Example

Mudlet has built-in GMCP support. No telnet parsing is needed, just register event handlers:

-- Vitals
function onGMCPVitals()
    local v = gmcp.Char.Vitals
    myHP = v.hp
    myMaxHP = v.maxhp
    -- update your gauge
end
registerAnonymousEventHandler("gmcp.Char.Vitals", "onGMCPVitals")

-- Status
function onGMCPStatus()
    local s = gmcp.Char.Status
    -- s.name, s.level, s.race, s.class
end
registerAnonymousEventHandler("gmcp.Char.Status", "onGMCPStatus")

-- Affects (buff bar)
function onGMCPAffects()
    local a = gmcp.Char.Affects
    for _, aff in ipairs(a.affects) do
        -- aff.name, aff.duration, aff.level, aff.location, aff.modifier
    end
end
registerAnonymousEventHandler("gmcp.Char.Affects", "onGMCPAffects")

-- Combat target
function onGMCPCombat()
    local c = gmcp.Char.Combat
    if c.target then
        -- fighting: c.target, c.condition, c.hp_pct
    else
        -- not fighting (empty object)
    end
end
registerAnonymousEventHandler("gmcp.Char.Combat", "onGMCPCombat")

-- Worth (gold, exp, etc.)
function onGMCPWorth()
    local w = gmcp.Char.Worth
    -- w.gold, w.bank, w.exp, w.tnl, w.trains, w.practices
end
registerAnonymousEventHandler("gmcp.Char.Worth", "onGMCPWorth")

-- Group
function onGMCPGroup()
    local g = gmcp.Group.Info
    if g.leader then
        -- grouped: g.leader, g.members[]
        for _, m in ipairs(g.members) do
            -- m.name, m.level, m.class, m.hp_pct, m.mana_pct, m.move_pct, m.tnl
        end
    else
        -- not grouped (empty object)
    end
end
registerAnonymousEventHandler("gmcp.Group.Info", "onGMCPGroup")

-- World time and weather
function onGMCPTime()
    local t = gmcp.World.Time
    -- t.hour, t.day, t.month, t.year, t.sunlight, t.sky
end
registerAnonymousEventHandler("gmcp.World.Time", "onGMCPTime")

-- Chat channels
function onGMCPChannel()
    local c = gmcp.Comm.Channel
    -- c.channel, c.speaker, c.text
    cecho(string.format("\n[%s] %s: %s", c.channel, c.speaker, c.text))
end
registerAnonymousEventHandler("gmcp.Comm.Channel", "onGMCPChannel")

-- Room info
function onGMCPRoom()
    local r = gmcp.Room.Info
    -- r.num, r.name, r.area, r.terrain, r.exits
    centerview(r.num)  -- update Mudlet mapper
end
registerAnonymousEventHandler("gmcp.Room.Info", "onGMCPRoom")

-- Room characters
function onGMCPRoomChars()
    local chars = gmcp.Room.Chars
    for _, c in ipairs(chars) do
        -- c.name, c.npc (boolean)
    end
end
registerAnonymousEventHandler("gmcp.Room.Chars", "onGMCPRoomChars")

-- Room items
function onGMCPRoomItems()
    local items = gmcp.Room.Items
    for _, item in ipairs(items) do
        -- item.name, item.type
    end
end
registerAnonymousEventHandler("gmcp.Room.Items", "onGMCPRoomItems")

-- Map tiles
function onGMCPMap()
    local m = gmcp.Map.Tiles
    -- m.r = radius, m.g = grid of cells
end
registerAnonymousEventHandler("gmcp.Map.Tiles", "onGMCPMap")

TinTin++ Example

TinTin++ supports GMCP via the #event command:

#event {IAC WILL GMCP} {
    #send {$IAC $DO $GMCP\};
}

#event {IAC SB GMCP Char.Vitals IAC SE} {
    #var {vitals} {%0};
    #showme {HP: $vitals[hp]/$vitals[maxhp]  Mana: $vitals[mana]/$vitals[maxmana]};
}

#event {IAC SB GMCP Char.Affects IAC SE} {
    #var {affects} {%0};
}

#event {IAC SB GMCP Char.Combat IAC SE} {
    #var {combat} {%0};
}

#event {IAC SB GMCP Char.Worth IAC SE} {
    #var {worth} {%0};
    #showme {Gold: $worth[gold]  Exp: $worth[exp]  TNL: $worth[tnl]};
}

#event {IAC SB GMCP Group.Info IAC SE} {
    #var {group} {%0};
}

#event {IAC SB GMCP World.Time IAC SE} {
    #var {time} {%0};
    #showme {Time: $time[hour]:00  Sky: $time[sky]};
}

#event {IAC SB GMCP Comm.Channel IAC SE} {
    #var {chan} {%0};
    #showme {[$chan[channel]] $chan[speaker]: $chan[text]};
}

#event {IAC SB GMCP Room.Info IAC SE} {
    #var {room} {%0};
    #showme {Room: $room[name] ($room[terrain])};
}

#event {IAC SB GMCP Room.Chars IAC SE} {
    #var {roomchars} {%0};
}

#event {IAC SB GMCP Room.Items IAC SE} {
    #var {roomitems} {%0};
}

#event {IAC SB GMCP Map.Tiles IAC SE} {
    #var {map} {%0};
}
Last edited by Erelei