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};
}