# Forsaken Lands GMCP Specification
Client-side reference for what the server emits on the wire. Field names, types, emit timing, and escaping rules.
## Contents
1. [Protocol basics](#protocol-basics)
2. [Handshake and client identification](#handshake-and-client-identification)
3. [Package reference](#package-reference)
- [Char.Vitals](#charvitals)
- [Char.Status](#charstatus)
- [Char.Worth](#charworth)
- [Char.Affects](#charaffects)
- [Char.Combat](#charcombat)
- [Room.Info](#roominfo)
- [Room.Chars](#roomchars)
- [Room.Items](#roomitems)
- [Map.Tiles](#maptiles)
- [Group.Info](#groupinfo)
- [World.Time](#worldtime)
- [World.Moons](#worldmoons)
- [Comm.Channel](#commchannel)
4. [Client to server messages](#client-to-server-messages)
5. [Scripting recipes](#scripting-recipes)
6. [Caveats and gotchas](#caveats-and-gotchas)
---
## Protocol basics
GMCP rides on TELNET option 201 (0xC9). Each message looks like:
```
IAC SB 201 <package-name> <space> <json-payload> IAC SE
```
Example:
```
IAC SB 201 Char.Vitals {"hp":850,"maxhp":900,...} IAC SE
```
Package names are dotted namespaces. Payloads are JSON (object, array, or primitive). The server emits actively. There is no subscription step beyond enabling the option.
### Enabling GMCP
The server sends `IAC WILL 201` at connect. Your client replies `IAC DO 201`. Mudlet, Mudlet-Web, tintin++, Blightmud, and MUSHclient handle this automatically. The built-in web client always negotiates it.
There is no `Core.Supports.Set` step. Once GMCP is on, every package fires at its appropriate trigger. Drop the ones you don't care about on the client side.
### Packet ordering
GMCP messages interleave with normal telnet output. A single `look` produces a burst:
```
Room.Info {...}
Room.Chars [...]
Room.Items [...]
Map.Tiles {...}
<then the plain-text room description>
```
Buffer packets by name and keep the latest value for each one. Older values are stale.
### String escaping
Payloads are valid JSON (`"`, `\\`, `\n`, `\t`, etc.). Backtick color codes are stripped server-side before any string goes into a JSON field, so you get clean text. The reset `` `` ``, single-char codes like `` `6 ``, and 256-color forms `` `(255) `` and `` `[123] `` are all removed. Room names, character names, and channel text arrive pre-stripped.
---
## Handshake and client identification
### Server to client
Sent right after telnet negotiation:
```
IAC WILL 201 (server)
IAC DO 201 (client, your job)
```
### Client to server
Three optional packages the server watches for.
#### `Core.Hello`
Standard GMCP fingerprint. Send once, right after you accept the option.
```json
Core.Hello {"client":"Mudlet","version":"4.17.2"}
```
Fields: `client` (string), `version` (string). Stored server-side for stats. No functional consequence.
#### `Client.Fingerprint.Report`
Free-form JSON. Forsaken Lands accepts it and stores it per-connection. Skip it if you don't need it.
#### `Proxy.Ident`
Used only by internal web proxies to forward the real client IP. Accepted only from RFC1918/loopback source IPs. Public clients don't need to send it.
---
## Package reference
### `Char.Vitals`
Drives HP, mana, and movement bars.
**Shape:**
```json
Char.Vitals {"hp":850,"maxhp":900,"mana":760,"maxmana":820,"move":250,"maxmove":250}
```
**Fields:**
| Field | Type | Notes |
|---|---|---|
| `hp` | int | current hit points |
| `maxhp` | int | max hit points with buffs |
| `mana` | int | current mana |
| `maxmana` | int | max mana with buffs |
| `move` | int | current movement |
| `maxmove` | int | max movement |
**Emitted on:** every prompt fire.
---
### `Char.Status`
Slow-changing identity fields.
**Shape:**
```json
Char.Status {"name":"Gibble","level":50,"race":"gnome","class":"Psi"}
```
**Fields:**
| Field | Type | Notes |
|---|---|---|
| `name` | string | character name |
| `level` | int | character level |
| `race` | string | race table name, lower-case short key |
| `class` | string | class table name (display casing preserved) |
**Emitted on:** login, level gain.
---
### `Char.Worth`
Currency and soft progression counters.
**Shape:**
```json
Char.Worth {"gold":12345,"bank":99000,"exp":812400,"tnl":45085,"trains":2,"practices":0,"cps":1713,"rps":9,"cabal":"Knight"}
```
**Fields:**
| Field | Type | Notes |
|---|---|---|
| `gold` | int | carried gold |
| `bank` | int | banked gold |
| `exp` | int | lifetime earned experience |
| `tnl` | int | experience remaining to next level (0 if capped) |
| `trains` | int | unspent training sessions |
| `practices` | int | unspent practice sessions |
| `cps` | int | cabal points |
| `rps` | int | roleplay points |
| `cabal` | string | current cabal name, or `"none"` if uncabaled |
**Emitted on:** every prompt and login.
---
### `Char.Affects`
Every active spell, skill, and song. Same list the in-game `affects` command shows.
**Shape:**
```json
Char.Affects {"affects":[
{"kind":"spell","name":"bless","duration":6,"level":50,"location":"hitroll","modifier":4},
{"kind":"spell","name":"armor","duration":44,"level":50,"location":"ac","modifier":-20},
{"kind":"song","name":"bagatelle of bravado","duration":8,"level":50,"location":"damroll","modifier":3}
]}
```
**Fields per affect:**
| Field | Type | Notes |
|---|---|---|
| `kind` | string | `"spell"` or `"song"`. Group on this. Don't trust name alone (see below). |
| `name` | string | stripped of color codes |
| `duration` | int | ticks remaining (a tick is roughly 30 seconds to a minute of real time). `-1` means permanent. |
| `level` | int | level the affect was cast at |
| `location` | string | stat it modifies (`"hitroll"`, `"damroll"`, `"ac"`, `"str"`, `"mana"`, `"none"`, etc.) |
| `modifier` | int | magnitude (negative is better for AC, etc.) |
The payload is the full state, not a diff. Replace your local list each time.
**Emitted on:** login, every prompt, and any add/remove/expire.
**Why `kind` matters:** spells and songs use disjoint internal indexes. Without `kind`, a spell at sn 18 (control weather) and a song at index 18 collide on numeric ID. Always split on `kind` before rendering.
---
### `Char.Combat`
Set when fighting. Empty object when not.
**Shape (fighting):**
```json
Char.Combat {"target":"Durim","condition":"quite a few wounds","hp_pct":54}
```
**Shape (not fighting):**
```json
Char.Combat {}
```
**Fields:**
| Field | Type | Notes |
|---|---|---|
| `target` | string | name of the char you're fighting (stripped) |
| `condition` | string | flavor text: `"excellent"`, `"a few scratches"`, `"small wounds"`, `"quite a few wounds"`, `"big nasty wounds"`, `"pretty hurt"`, `"awful"`, `"bleeding to death"` |
| `hp_pct` | int | 0 to 100; clamps to 0 on death |
**Emitted on:** prompt. The empty object fires once when combat ends, so use it as the explicit "tear down combat HUD" signal.
---
### `Room.Info`
The room you're standing in.
**Shape:**
```json
Room.Info {
"num": 16601,
"name": "The Academy Courtyard",
"area": "Val Miran",
"terrain": "city",
"exits": {"north":16600,"east":16602,"west":16603}
}
```
**Fields:**
| Field | Type | Notes |
|---|---|---|
| `num` | int | room vnum |
| `name` | string | stripped of color codes |
| `area` | string | area name (zone) |
| `terrain` | string | one of: `inside`, `city`, `field`, `forest`, `hills`, `mountain`, `water_swim`, `water_noswim`, `swamp`, `air`, `desert`, `lava`, `snow` |
| `exits` | object | direction-name to destination room vnum. Includes `up`/`down` when present. |
**Emitted on:** every look at your current room (movement, force-look, etc).
---
### `Room.Chars`
Everyone else visible in your room.
**Shape:**
```json
Room.Chars [
{"name":"Durim","npc":false},
{"name":"a city guard","npc":true}
]
```
**Fields per entry:**
| Field | Type | Notes |
|---|---|---|
| `name` | string | as you see them (handles invis/disguise/PERS()) |
| `npc` | bool | `true` for mobs, `false` for PCs |
You are omitted. People you can't see are omitted.
**Emitted on:** every look at the current room.
---
### `Room.Items`
Objects on the ground.
**Shape:**
```json
Room.Items [
{"name":"a worn leather boot","type":"armor"},
{"name":"a pile of gold coins","type":"money"}
]
```
**Fields per entry:**
| Field | Type | Notes |
|---|---|---|
| `name` | string | short description, color-stripped |
| `type` | string | item type: `weapon`, `armor`, `container`, `food`, `potion`, `wand`, `scroll`, `staff`, `money`, `light`, `corpse_npc`, `corpse_pc`, etc. |
Items you can't see are omitted (e.g. invisible without detect-invis).
**Emitted on:** every look at the current room.
---
### `Map.Tiles`
BFS-generated minimap around the player. Lets clients render a tactical floor view plus arbitrary vertical depth.
**Top-level shape:**
```json
Map.Tiles {
"r": 7,
"g": [ [ cell|null, cell|null, ... ], ... ],
"a": [ zlayer_room, ... ],
"b": [ zlayer_room, ... ],
"zr": [ zroom, ... ],
"areas": { "<area_vnum>": {"name":"…","color":"#rrggbb"}, … },
"t": " ... @e ...| ..."
}
```
**Top-level fields:**
| Field | Type | Notes |
|---|---|---|
| `r` | int | radius of the BFS (4 small, 7 default, 10 large). Grid is `(2r+1) x (2r+1)`. |
| `g` | 2D array | rows by columns, current floor only. Positions outside the BFS reach are `null`. Player is always at `[r][r]`. |
| `a` | array | legacy: rooms reached via *up* exits, `\|z\|=1` only with a 1-cardinal sample. Stable for older clients. New clients can ignore. |
| `b` | array | legacy mirror of `a` for *down* exits. |
| `zr` | array | full multi-Z BFS results, off-floor cells only (`z != 0`). Each entry is the same per-cell payload as `g` plus explicit `x`/`y`/`z`. The cardinal-first BFS prefers same-floor paths on tie. A `zr` entry may share its `(x,y)` with a populated `g[y][x]` when a room sits directly above or below it (a stacked floor). The client decides whether to render off-floor cells as a separate layer, a toggle, or in-place. Arbitrary depth, capped only by the radius hop count. |
| `areas` | object | keyed by area vnum. Includes every zone present in `g` or `zr`. Value is `{name, color}`. `color` is a stable `#rrggbb` hashed from the vnum. |
| `t` | string | pre-rendered ASCII fallback grid (`|`-separated rows). `0-9` are sector digits, `a-c` are desert/lava/snow, `@` is you, space is empty. Current floor only. |
#### Per-cell (`g[y][x]`) shape
```json
{
"s": 1,
"e": "neswud",
"l": 2,
"h": 1,
"ar": 30,
"f": "s$b",
"d": {"n":"locked","s":"closed"},
"ex": {"n":16600,"e":16602,"w":16603,"u":16800,"d":16900}
}
```
| Field | Type | Notes |
|---|---|---|
| `s` | int | sector index (0-12, same order as `Room.Info` terrain enum). |
| `e` | string | exit letters from `n e s w u d`. Uppercase means the exit leads somewhere the map couldn't include (off-grid or teleport-style). |
| `l` | int | light level 0-4. `0` dark+night, `1` dark, `2` indoors, `3` outdoors+night, `4` outdoors+day. |
| `h` | int | present and `1` only on the cell you're standing in. Omitted everywhere else. |
| `ar` | int | area vnum. Cross-reference with top-level `areas` for name and color. |
| `f` | string | flag chars from `s` (safe/no-PK), `$` (shop), `b` (bank), `t` (trainer), `h` (healer). Omitted when empty. |
| `d` | object | door state dict. Present only when the room has at least one visible door. Keys are direction letters, values are `"open"`, `"closed"`, `"locked"`, or `"hidden"` (imms only). |
| `ex` | object | exit destinations as direction-letter to vnum. Omitted if no exits. |
Player-identity (`p`) per cell is intentionally not emitted. A passive PK radar over GMCP would erode the scan/where gameplay loop, so it was dropped. Use `scan`, `scan pk`, or `where` for nearby players. `Room.Chars` covers your current room.
#### Per-cell in `a` / `b` (legacy z-layers)
```json
{"x":7,"y":7,"s":0,"e":"u","vnum":2051}
```
Rooms reached by going up or down from a grid cell at `(x,y)`. Single-step (`|z|=1`) only and don't carry the extended cell fields (no `d`, `f`, `l`, etc.). Older clients still rely on this shape. Prefer `zr` for new code.
#### Per-room in `zr`
```json
{
"x": 5, "y": 7, "z": -2,
"s": 0,
"e": "neud",
"l": 1,
"ar": 30,
"f": "s",
"d": {"u":"closed"},
"ex": {"n":3041,"e":3042,"u":3010,"d":3120}
}
```
| Field | Type | Notes |
|---|---|---|
| `x`, `y` | int | grid coordinate, same space as `g[y][x]`. May coincide with a populated `g` cell when a room is stacked directly above or below. |
| `z` | int | floor delta from the player. Positive is above (`1` = one floor up), negative is below. Arbitrary depth, capped only by the radius. |
| `s`, `e`, `l`, `ar`, `f`, `d`, `ex` | as above | identical semantics to `g[y][x]`. `h` never appears in `zr` (player is always in `g`). |
#### Secret exits
Non-imms never see secret passages or closed-and-secret doors. Those exits are absent from `e`, `ex`, and `d`. Imms see them with `d` value `"hidden"`.
**Emitted on:** every look at the current room.
**Backwards compatibility:** the original minimal fields (`r`, `g[y][x].s`, `g[y][x].e`, `g[y][x].h`, `t`) are unchanged. Old clients that only read those keep working.
---
### `Group.Info`
Your group roster. Empty (`{}`) when solo.
**Shape (grouped):**
```json
Group.Info {
"leader": "Durim",
"members": [
{"name":"Durim","level":50,"class":"Dkn","hp_pct":78,"mana_pct":55,"move_pct":93,"tnl":1250},
{"name":"Gibble","level":50,"class":"Psi","hp_pct":91,"mana_pct":40,"move_pct":88,"tnl":0}
]
}
```
**Shape (solo):**
```json
Group.Info {}
```
**Top-level fields:**
- `leader` (string): leader's visible name.
- `members` (array): every member including you. Members with `NOWHO` are omitted.
**Per-member fields:**
| Field | Type | Notes |
|---|---|---|
| `name` | string | as you see them |
| `level` | int | 1 to 60 |
| `class` | string | short class key (e.g. `Dkn`, `Psi`, `War`) for PCs, `"mob"` for charmies |
| `hp_pct`, `mana_pct`, `move_pct` | int | 0 to 100 |
| `tnl` | int | exp to next level. `0` for NPCs and capped chars. |
**Emitted on:** prompt, login, group join/leave.
---
### `World.Time`
In-game clock and weather.
**Shape:**
```json
World.Time {"hour":14,"day":12,"month":3,"year":2026,"sunlight":"light","sky":"cloudy"}
```
**Fields:**
| Field | Type | Notes |
|---|---|---|
| `hour` | int | 0-23 game-time |
| `day` | int | day of month |
| `month` | int | 0-17 (see game calendar) |
| `year` | int | game year |
| `sunlight` | string | `"dark"`, `"rise"`, `"light"`, `"set"` |
| `sky` | string | `"cloudless"`, `"cloudy"`, `"raining"`, `"lightning"` |
**Emitted on:** login. Also broadcast to every GMCP-enabled connection on each weather tick (roughly every two minutes real time).
---
### `World.Moons`
Phase state for the three moons (Lysenties, Nercuros, Dyphrities). The MUD's astronomy intentionally exposes phase but not fullness or time-to-full, so client scripts can't time alignment-driven procs to the tick.
**Shape:**
```json
World.Moons {
"moons": [
{"name":"Lysenties","active":true,"phase":4,"phase_name":"full"},
{"name":"Nercuros","active":true,"phase":2,"phase_name":"first half"},
{"name":"Dyphrities","active":false,"phase":0,"phase_name":"new"}
],
"eclipse": false,
"triad": false,
"near_alignment": false
}
```
**Top-level fields:**
| Field | Type | Notes |
|---|---|---|
| `moons` | array | one entry per moon, in fixed order Lysenties / Nercuros / Dyphrities. |
| `eclipse` | bool | true while a solar or lunar eclipse is active. All moon-driven scalars collapse to zero during eclipse. |
| `triad` | bool | true when all three moons are full simultaneously. |
| `near_alignment` | bool | true when phases are close enough to trigger near-alignment buffs/effects. |
**Per-moon fields:**
| Field | Type | Notes |
|---|---|---|
| `name` | string | display name (`"Lysenties"`, `"Nercuros"`, `"Dyphrities"`) |
| `active` | bool | false if the moon is currently dormant (rare; treat as inactive for any moon-derived effects). |
| `phase` | int | 0-7 phase index. `0` new, `1` waxing, `2` first half, `3` gibbous waxing, `4` full, `5` gibbous waning, `6` second half, `7` waning. |
| `phase_name` | string | display string for the phase. |
**Emitted on:** login. Also broadcast to every GMCP-enabled connection when any phase index flips, any `active` flag toggles, or any of the three world-state booleans changes. The broadcaster compares against a small static cache so re-firing on every weather tick when nothing changed costs nothing on the wire.
---
### `Comm.Channel`
One message per chat-like event you hear.
**Base shape (all channels):**
```json
Comm.Channel {"channel":"say","speaker":"Durim","text":"Hello there."}
```
**Extended shape (say, yell, tell, gtell add language and direction metadata):**
```json
Comm.Channel {
"channel":"say",
"speaker":"Durim",
"text":"xak zee nog",
"language":"foreign",
"understood":false
}
```
```json
Comm.Channel {
"channel":"tell",
"speaker":"Gibble",
"text":"meet at the altar",
"language":"common",
"understood":true,
"direction":"received"
}
```
**Base fields (always present):**
| Field | Type | Notes |
|---|---|---|
| `channel` | string | see table below |
| `speaker` | string | visible name. For `tell`, this is the sender (you only receive a `tell` packet as the recipient). |
| `text` | string | color-stripped. Garbled per-listener on language-aware channels. |
**Optional fields (say, yell, tell, gtell):**
| Field | Type | Notes |
|---|---|---|
| `language` | string | language name (`"common"`, `"drow"`, `"orcish"`, etc.) when the listener understood. `"foreign"` when they didn't. The actual foreign language never leaks. |
| `understood` | bool | `true` means `text` is the original. `false` means `text` is phoneme-garbled to match the terminal's foreign-tongue rendering. |
| `direction` | string | `"received"` on tell packets at the recipient. Outgoing tells do not echo to the sender's GMCP, so `"sent"` never appears in practice. |
**Channel values:**
| Value | Source | Language-aware | Direction field |
|---|---|---|---|
| `say` | `say` | yes | no |
| `yell` | `yell` | yes | no |
| `tell` | `tell`, `reply`, language-specific tells (`tarchaic`, `tmogwei`, etc.) | yes | yes (recipient side only) |
| `gtell` | `gtell` and group-tell projection | yes | no |
| `pray` | `pray` | no | no |
| `newbie` | `newbie` | no | no |
| `cabal` | `cabal chat` | no | no |
| `clan` | `clan chat` | no | no |
| `faction` | `faction chat` | no | no |
| `immortal` | `immtalk` (imms only) | no | no |
| `imp` | `imptalk` (IMP+ only) | no | no |
**Tell delivery:** only the recipient receives a `Comm.Channel`. The sender sees their own outgoing line in plain text but no GMCP echo. This was a deliberate change: when tells echoed both ways, naive triggers treated the echo as if the recipient had just sent a tell back. If your client needs a record of outgoing tells, parse them off the terminal line.
**Language semantics:**
- The speaker's own packet (for say, yell, gtell) is always `understood:true` with the actual language name.
- Each listener's packet is evaluated separately via the same `can_hear_lang` logic the terminal uses (skill % roll, `com_lan` affect, `IS_IMMORTAL`, illithid race, divine language).
- When `understood:false`, `text` is phoneme-garbled per the speaker's language to match the terminal output, and `language` is `"foreign"` rather than the real language name.
**Imm/imp chat:** broadcast to everyone with the right trust whose channel toggle is on. No language filtering. No `direction` field.
**Emitted on:** each matching channel message.
**RNG note:** language comprehension is a per-call dice roll for partial-skill listeners (skill 0 or 100 are deterministic). GMCP and terminal output evaluate independently, so a middle-skill listener can occasionally see them disagree on a single message. Treat the GMCP value as authoritative for client-side rendering.
---
## Client to server messages
Besides `Core.Hello`, `Client.Fingerprint.Report`, and `Proxy.Ident` (covered above), the server doesn't accept gameplay commands over GMCP. Drive input through the normal text command stream. GMCP is an output channel for state.
---
## Scripting recipes
### Mudlet, HP/mana bars from Char.Vitals
```lua
registerAnonymousEventHandler("gmcp.Char.Vitals", function()
local v = gmcp.Char.Vitals
hpBar:setValue(v.hp, v.maxhp)
manaBar:setValue(v.mana, v.maxmana)
moveBar:setValue(v.move, v.maxmove)
end)
```
### Mudlet, combat target widget
```lua
registerAnonymousEventHandler("gmcp.Char.Combat", function()
local c = gmcp.Char.Combat
if not c or not c.target then
combatHUD:hide()
else
combatHUD:show()
combatName:echo(c.target)
combatBar:setValue(c.hp_pct, 100)
end
end)
```
### Mudlet, songs separate from spells
```lua
registerAnonymousEventHandler("gmcp.Char.Affects", function()
local spells, songs = {}, {}
for _, a in ipairs(gmcp.Char.Affects.affects or {}) do
if a.kind == "song" then
table.insert(songs, a)
else
table.insert(spells, a)
end
end
renderSpells(spells)
renderSongs(songs)
end)
```
### Mudlet, minimap with zone tints
```lua
registerAnonymousEventHandler("gmcp.Map.Tiles", function()
local m = gmcp.Map.Tiles
local r = m.r
for y = 1, #m.g do
for x = 1, #m.g[y] do
local cell = m.g[y][x]
if cell == vim.NIL then cell = nil end -- Mudlet uses vim.NIL for JSON null
if cell then
local tint = m.areas and m.areas[tostring(cell.ar)] and m.areas[tostring(cell.ar)].color or "#444"
drawCell(x, y, cell.s, cell.e, tint, cell.l, cell.f, cell.h)
else
drawCell(x, y, nil)
end
end
end
-- Off-floor rooms (towers, dungeons with stacked floors)
for _, z in ipairs(m.zr or {}) do
drawZRoom(z.x, z.y, z.z, z.s, z.e, z.ar)
end
end)
```
### Mudlet, World.Moons phase tracker
```lua
registerAnonymousEventHandler("gmcp.World.Moons", function()
local m = gmcp.World.Moons
for _, moon in ipairs(m.moons) do
moonWidget[moon.name]:setPhase(moon.phase, moon.phase_name, moon.active)
end
eclipseIndicator:setVisible(m.eclipse)
triadIndicator:setVisible(m.triad)
end)
```
### tintin++, chat log split by channel
```
#action {Comm.Channel %*} {
#var CHAN %1[channel];
#var WHO %1[speaker];
#var MSG %1[text];
#line log chat-$CHAN.txt <$WHO>: $MSG
}
```
### Generic, room-change trigger
`Room.Info` fires once per look. Stash `room.num` to detect movement:
```
prevRoom = nil
onRoomInfo = function(info)
if info.num ~= prevRoom then
prevRoom = info.num
onEnterRoom(info)
end
end
```
---
## Caveats and gotchas
### Ordering
Prompt-driven packages (`Char.Vitals`, `Char.Worth`, `Char.Combat`, `Group.Info`) fire together every prompt. Don't assume cross-package atomicity. Treat each as independent state.
### Stale packets
- `Char.Combat {}` is the explicit "combat ended" signal. Don't wait for a timeout.
- `Group.Info {}` is the explicit "solo" signal.
- `Char.Affects` always sends the full list. Replace, don't merge.
### Map timing
`Map.Tiles` is the largest packet (up to about 80KB at radius 10 with the full payload). Debounce heavy redraw work. Don't redraw per pixel.
### Size limits
- `Char.Affects`: up to 8KB. Truncates silently on overflow.
- `Map.Tiles`: up to 96KB.
- `Group.Info`, `Room.Chars`, `Room.Items`: up to 8KB.
- `Comm.Channel` text field: up to about 1KB after JSON-escape.
### Invisibility
`Room.Chars` and `Group.Info` apply the normal `can_see()` check. Your client only sees what your character sees.
### Color stripping
Every string field is passed through `json_escape_strip_color`. You get plain text with quotes and backslashes JSON-escaped. If you want colored names, reconstruct from class/race/title in your client CSS.
### Secret exits
Non-imms: secret passages and closed-and-secret doors are absent from `Map.Tiles`. Imms see them with `d` value `"hidden"`.
### Reconnection
After copyover/hotboot the server re-emits the login package set. Don't assume any package's content survives across disconnects.
### When GMCP goes silent
If you stop receiving packets:
1. Confirm `IAC DO 201` was sent.
2. Confirm your terminal isn't stripping IAC sequences (some unix `tail` setups do).
3. `Core.Hello` isn't required, but sending it keeps server-side fingerprint and telemetry useful.
---
## Source of truth
Everything here is what FL actually emits. If a field disagrees with the wire, the code is right and the doc is stale. File an issue or update this doc directly.