Aabahran

Diff: GMCP

Comparing revision 687 (Erelei, ) to revision 694 (Erelei, )
1 1 # Forsaken Lands GMCP Specification
2 2
3 3 Client-side reference for what the server emits on the wire. Field names, types, emit timing, and escaping rules.
4 -
5 - ## Contents
6 4
7 - 1. [Protocol basics](#protocol-basics)
8 - 2. [Handshake and client identification](#handshake-and-client-identification)
9 - 3. [Package reference](#package-reference)
10 - - [Char.Vitals](#charvitals)
11 - - [Char.Status](#charstatus)
12 - - [Char.Worth](#charworth)
13 - - [Char.Affects](#charaffects)
14 - - [Char.Combat](#charcombat)
15 - - [Room.Info](#roominfo)
16 - - [Room.Chars](#roomchars)
17 - - [Room.Items](#roomitems)
18 - - [Map.Tiles](#maptiles)
19 - - [Group.Info](#groupinfo)
20 - - [World.Time](#worldtime)
21 - - [World.Moons](#worldmoons)
22 - - [Comm.Channel](#commchannel)
23 - 4. [Client to server messages](#client-to-server-messages)
24 - 5. [Scripting recipes](#scripting-recipes)
25 - 6. [Caveats and gotchas](#caveats-and-gotchas)
5 + {toc}
26 6
27 7 ---
28 8
29 9 ## Protocol basics
30 10
31 - GMCP rides on TELNET option 201 (0xC9). Each message looks like:
11 + GMCP rides on TELNET option 201 (`0xC9`). Every message is:
32 12
33 13 ```
34 14 IAC SB 201 <package-name> <space> <json-payload> IAC SE
15 + (FF FA C9 ... FF F0)
35 16 ```
36 17
37 18 Example:
19 +
38 20 ```
39 21 IAC SB 201 Char.Vitals {"hp":850,"maxhp":900,...} IAC SE
40 22 ```
41 23
42 - 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.
24 + Package names use dotted namespaces. The payload is UTF-8 JSON (object, array, or primitive). The server emits actively. There is no subscription step beyond enabling the option.
43 25
44 26 ### Enabling GMCP
45 27
46 - 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.
28 + The server sends `IAC WILL 201` (`FF FB C9`). Your client responds `IAC DO 201` (`FF FD C9`). Mudlet, tintin++, Blightmud, and MUSHclient do this automatically. The built-in web client always negotiates it.
47 29
48 - 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.
30 + There is no `Core.Supports.Set` step. Once GMCP is on, every package fires at its trigger. Discard packets you don't want on the client side.
49 31
50 32 ### Packet ordering
51 33
52 - GMCP messages interleave with normal telnet output. A single `look` produces a burst:
34 + GMCP messages interleave with normal telnet output. A single `look` produces a burst like `Room.Info {...}`, `Room.Chars [...]`, `Room.Items [...]`, `Map.Tiles {...}`, followed by the plain-text room description.
35 +
36 + Buffer packets by name and keep only the latest value for each. Older values are stale.
37 +
38 + ### String escaping
39 +
40 + Payloads are valid JSON (`"`, `\\`, `\n`, `\t`, etc. are escaped). The server strips backtick color codes before any string enters a JSON field. The reset code, single-character codes like `` `6 ``, and 256-color forms like `` `(255) `` and `` `[123] `` are all removed. Room names, character names, and channel text arrive clean.
41 +
42 + ### IAC state machine
43 +
44 + A raw telnet client has to pull GMCP out of the byte stream itself. This minimal state machine does it:
45 +
46 + | State | Byte | Action |
47 + |---|---|---|
48 + | 0 (normal) | `FF` (IAC) | go to state 1 |
49 + | 0 (normal) | anything else | append to text output |
50 + | 1 (got IAC) | `FF` | escaped `0xFF` literal. Append to text, go to state 0 |
51 + | 1 (got IAC) | `FB`/`FC`/`FD`/`FE` (WILL/WONT/DO/DONT) | save verb, go to state 2 |
52 + | 1 (got IAC) | `FA` (SB) | start subneg buffer, go to state 3 |
53 + | 1 (got IAC) | anything else | ignore (GA, NOP, etc.), go to state 0 |
54 + | 2 (got verb) | any byte | option number. If `WILL 0xC9`, reply `DO 0xC9`. Go to state 0 |
55 + | 3 (in subneg) | `FF` (IAC) | go to state 4 |
56 + | 3 (in subneg) | anything else | append to subneg buffer |
57 + | 4 (IAC in subneg) | `F0` (SE) | subneg complete. If the first byte is `0xC9`, the rest is GMCP. Go to state 0 |
58 + | 4 (IAC in subneg) | `FF` | escaped `0xFF`. Append to buffer, go to state 3 |
59 + | 4 (IAC in subneg) | anything else | protocol error, go to state 0 |
60 +
61 + On a complete subneg whose first byte is `0xC9`, drop that byte and split the rest at the first space. The left side is the package name. The right side is the JSON body.
62 +
63 + ### Echo toggle (password prompts)
64 +
65 + Password prompts use standard telnet echo negotiation:
53 66
54 67 ```
55 - Room.Info {...}
56 - Room.Chars [...]
57 - Room.Items [...]
58 - Map.Tiles {...}
59 - <then the plain-text room description>
68 + IAC WILL ECHO (FF FB 01) server will echo, client should HIDE input
69 + IAC WONT ECHO (FF FC 01) server stops echoing, client should SHOW input
60 70 ```
61 71
62 - Buffer packets by name and keep the latest value for each one. Older values are stale.
72 + On `IAC WILL ECHO`, switch your input to password mode. On `IAC WONT ECHO`, switch back.
63 73
64 - ### String escaping
74 + ### ANSI color codes
65 75
66 - 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.
76 + The server sends two kinds of ANSI sequences:
77 +
78 + | Type | Wire format | Example | ESC byte present? |
79 + |---|---|---|---|
80 + | 16-color | `[0;31m` | red text | **no**, bare CSI with no `0x1B` prefix |
81 + | 256-color | `\x1b[38;5;166m` | orange text | **yes**, standard CSI |
82 + | reset | `[0m` | reset all | **no** |
83 + | clear screen | `[2J` | clear | **no** |
84 +
85 + A standard terminal emulator (xterm.js, etc.) needs the bare sequences fixed up by prepending `0x1B`.
86 +
87 + **Fixup:** scan the stream. When you hit `[` (`0x5B`) not preceded by `0x1B`, look ahead. If digits and/or semicolons follow and end in a final byte (`0x40` to `0x7E`), it's a bare CSI. Prepend `0x1B`. Literal brackets in prose like `[Exits: north]` don't start with a digit, so they're left alone.
67 88
68 89 ---
69 90
70 91 ## Handshake and client identification
71 92
72 93 ### Server to client
73 94
74 - Sent right after telnet negotiation:
95 + Right after telnet negotiation:
75 96 ```
76 97 IAC WILL 201 (server)
77 98 IAC DO 201 (client, your job)
78 99 ```
79 100
80 101 ### Client to server
81 102
82 - Three optional packages the server watches for.
103 + Three optional packages the server listens for.
83 104
84 105 #### `Core.Hello`
85 - Standard GMCP fingerprint. Send once, right after you accept the option.
106 +
107 + Standard GMCP fingerprint. Send once right after accepting the option:
86 108 ```json
87 109 Core.Hello {"client":"Mudlet","version":"4.17.2"}
88 110 ```
89 - Fields: `client` (string), `version` (string). Stored server-side for stats. No functional consequence.
111 +
112 + **Fields:**
113 + | Field | Type | Notes |
114 + |---|---|---|
115 + | `client` | string | client name |
116 + | `version` | string | client version |
117 +
118 + Stored for statistics. No gameplay effect.
90 119
91 120 #### `Client.Fingerprint.Report`
92 - Free-form JSON. Forsaken Lands accepts it and stores it per-connection. Skip it if you don't need it.
121 +
122 + Free-form JSON. Accepted and stored per connection. Optional.
93 123
94 124 #### `Proxy.Ident`
95 - 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.
125 +
126 + Only for internal web proxies forwarding the real client IP. Accepted only from RFC1918 or loopback source addresses. Public clients don't send it.
96 127
97 128 ---
98 129
99 130 ## Package reference
131 +
132 + The packages the server emits to connected clients.
100 133
101 134 ### `Char.Vitals`
102 135
103 136 Drives HP, mana, and movement bars.
104 137
105 138 **Shape:**
106 139 ```json
107 140 Char.Vitals {"hp":850,"maxhp":900,"mana":760,"maxmana":820,"move":250,"maxmove":250}
108 141 ```
109 142
110 143 **Fields:**
111 144 | Field | Type | Notes |
112 145 |---|---|---|
113 146 | `hp` | int | current hit points |
114 147 | `maxhp` | int | max hit points with buffs |
115 148 | `mana` | int | current mana |
116 149 | `maxmana` | int | max mana with buffs |
117 150 | `move` | int | current movement |
118 151 | `maxmove` | int | max movement |
119 152
120 153 **Emitted on:** every prompt fire.
154 +
155 + **Usage:** `hp / maxhp` gives a percentage. Common thresholds are red below 25%, yellow below 50%, green above.
121 156
122 157 ---
123 158
124 159 ### `Char.Status`
125 160
126 161 Slow-changing identity fields.
127 162
128 163 **Shape:**
129 164 ```json
130 165 Char.Status {"name":"Gibble","level":50,"race":"gnome","class":"Psi"}
131 166 ```
132 167
133 168 **Fields:**
134 169 | Field | Type | Notes |
135 170 |---|---|---|
136 171 | `name` | string | character name |
137 172 | `level` | int | character level |
138 173 | `race` | string | race table name, lower-case short key |
139 174 | `class` | string | class table name (display casing preserved) |
140 175
141 176 **Emitted on:** login, level gain.
142 177
143 178 ---
144 179
145 180 ### `Char.Worth`
146 181
147 182 Currency and soft progression counters.
148 183
149 184 **Shape:**
150 185 ```json
151 186 Char.Worth {"gold":12345,"bank":99000,"exp":812400,"tnl":45085,"trains":2,"practices":0,"cps":1713,"rps":9,"cabal":"Knight"}
152 187 ```
153 188
154 189 **Fields:**
155 190 | Field | Type | Notes |
156 191 |---|---|---|
157 192 | `gold` | int | carried gold |
158 193 | `bank` | int | banked gold |
159 194 | `exp` | int | lifetime earned experience |
160 195 | `tnl` | int | experience remaining to next level (0 if capped) |
161 196 | `trains` | int | unspent training sessions |
162 197 | `practices` | int | unspent practice sessions |
163 198 | `cps` | int | cabal points |
164 199 | `rps` | int | roleplay points |
165 200 | `cabal` | string | current cabal name, or `"none"` if uncabaled |
166 201
167 202 **Emitted on:** every prompt and login.
168 203
169 204 ---
170 205
171 206 ### `Char.Affects`
172 207
173 - Every active spell, skill, and song. Same list the in-game `affects` command shows.
208 + Every active spell, skill, and song. Same list the in-game `affects` command shows. Hidden affects (sneak, noquit, etc.) are filtered out.
174 209
175 210 **Shape:**
176 211 ```json
177 212 Char.Affects {"affects":[
178 213 {"kind":"spell","name":"bless","duration":6,"level":50,"location":"hitroll","modifier":4},
179 214 {"kind":"spell","name":"armor","duration":44,"level":50,"location":"ac","modifier":-20},
180 215 {"kind":"song","name":"bagatelle of bravado","duration":8,"level":50,"location":"damroll","modifier":3}
181 216 ]}
182 217 ```
183 218
184 219 **Fields per affect:**
185 220 | Field | Type | Notes |
186 221 |---|---|---|
187 222 | `kind` | string | `"spell"` or `"song"`. Group on this. Don't trust name alone (see below). |
188 223 | `name` | string | stripped of color codes |
189 224 | `duration` | int | ticks remaining (a tick is roughly 30 seconds to a minute of real time). `-1` means permanent. |
190 225 | `level` | int | level the affect was cast at |
191 226 | `location` | string | stat it modifies (`"hitroll"`, `"damroll"`, `"ac"`, `"str"`, `"mana"`, `"none"`, etc.) |
192 227 | `modifier` | int | magnitude (negative is better for AC, etc.) |
193 228
194 229 The payload is the full state, not a diff. Replace your local list each time.
195 230
196 231 **Emitted on:** login, every prompt, and any add/remove/expire.
197 232
198 233 **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.
234 +
235 + **Usage:** buff and debuff bars. Use `duration` for countdowns and highlight affects at 1 or 2 ticks left.
199 236
200 237 ---
201 238
202 239 ### `Char.Combat`
203 240
204 241 Set when fighting. Empty object when not.
205 242
206 243 **Shape (fighting):**
207 244 ```json
208 245 Char.Combat {"target":"Durim","condition":"quite a few wounds","hp_pct":54}
209 246 ```
210 247
211 248 **Shape (not fighting):**
212 249 ```json
213 250 Char.Combat {}
214 251 ```
215 252
216 253 **Fields:**
217 254 | Field | Type | Notes |
218 255 |---|---|---|
219 256 | `target` | string | name of the char you're fighting (stripped) |
220 257 | `condition` | string | flavor text: `"excellent"`, `"a few scratches"`, `"small wounds"`, `"quite a few wounds"`, `"big nasty wounds"`, `"pretty hurt"`, `"awful"`, `"bleeding to death"` |
221 258 | `hp_pct` | int | 0 to 100; clamps to 0 on death |
222 259
223 260 **Emitted on:** prompt. The empty object fires once when combat ends, so use it as the explicit "tear down combat HUD" signal.
224 261
225 262 ---
226 263
227 264 ### `Room.Info`
228 265
229 266 The room you're standing in.
230 267
231 268 **Shape:**
232 269 ```json
233 270 Room.Info {
234 271 "num": 16601,
235 272 "name": "The Academy Courtyard",
236 273 "area": "Val Miran",
237 274 "terrain": "city",
275 + "sector": 1,
276 + "region": 0,
277 + "climate": "Temperate",
238 278 "exits": {"north":16600,"east":16602,"west":16603}
239 279 }
240 280 ```
241 281
242 282 **Fields:**
243 283 | Field | Type | Notes |
244 284 |---|---|---|
245 285 | `num` | int | room vnum |
246 286 | `name` | string | stripped of color codes |
247 287 | `area` | string | area name (zone) |
248 - | `terrain` | string | one of: `inside`, `city`, `field`, `forest`, `hills`, `mountain`, `water_swim`, `water_noswim`, `swamp`, `air`, `desert`, `lava`, `snow` |
249 - | `exits` | object | direction-name to destination room vnum. Includes `up`/`down` when present. |
288 + | `terrain` | string | sector name, see terrain table below |
289 + | `sector` | int | sector index 0 to 12, or `-1` when unknown. Same index `Map.Tiles` sends in `s`, so a mapper can color a room without matching on the name. |
290 + | `region` | int | climate region index, see region table below |
291 + | `climate` | string | climate region name for the same index |
292 + | `exits` | object | direction-name to destination room vnum. Directions are `north`, `south`, `east`, `west`, `up`, `down`. |
293 +
294 + **Terrain / sector values:**
295 + | `sector` | `terrain` | Meaning |
296 + |---|---|---|
297 + | 0 | `inside` | indoor room |
298 + | 1 | `city` | city streets |
299 + | 2 | `field` | open field, grassland |
300 + | 3 | `forest` | forest, woods |
301 + | 4 | `hills` | hilly terrain |
302 + | 5 | `mountain` | mountains |
303 + | 6 | `water_swim` | shallow, swimmable water |
304 + | 7 | `water_noswim` | deep water (needs boat or fly) |
305 + | 8 | `swamp` | swamp, bog |
306 + | 9 | `air` | open air (needs fly) |
307 + | 10 | `desert` | desert |
308 + | 11 | `lava` | lava, volcanic |
309 + | 12 | `snow` | snow, frozen terrain |
310 +
311 + **Region values:**
312 + | `region` | `climate` |
313 + |---|---|
314 + | 0 | Temperate |
315 + | 1 | Coastal North |
316 + | 2 | Coastal South |
317 + | 3 | Desert |
318 + | 4 | Tundra |
319 + | 5 | Mountain North |
320 + | 6 | Mountain South |
321 + | 7 | Mountain East |
250 322
251 323 **Emitted on:** every look at your current room (movement, force-look, etc).
324 +
325 + **Usage:** automappers key on `num` and link rooms through `exits`.
252 326
253 327 ---
254 328
255 329 ### `Room.Chars`
256 330
257 331 Everyone else visible in your room.
258 332
259 333 **Shape:**
260 334 ```json
261 335 Room.Chars [
262 336 {"name":"Durim","npc":false},
263 337 {"name":"a city guard","npc":true}
264 338 ]
265 339 ```
266 340
267 341 **Fields per entry:**
268 342 | Field | Type | Notes |
269 343 |---|---|---|
270 344 | `name` | string | as you see them (handles invis/disguise/PERS()) |
271 345 | `npc` | bool | `true` for mobs, `false` for PCs |
272 346
273 347 You are omitted. People you can't see are omitted.
274 348
275 349 **Emitted on:** every look at the current room.
350 +
351 + **Usage:** occupant panels. Make names clickable for `look <name>` or `kill <name>`.
276 352
277 353 ---
278 354
279 355 ### `Room.Items`
280 356
281 357 Objects on the ground.
282 358
283 359 **Shape:**
284 360 ```json
285 361 Room.Items [
286 362 {"name":"a worn leather boot","type":"armor"},
287 363 {"name":"a pile of gold coins","type":"money"}
288 364 ]
289 365 ```
290 366
291 367 **Fields per entry:**
292 368 | Field | Type | Notes |
293 369 |---|---|---|
294 370 | `name` | string | short description, color-stripped |
295 - | `type` | string | item type: `weapon`, `armor`, `container`, `food`, `potion`, `wand`, `scroll`, `staff`, `money`, `light`, `corpse_npc`, `corpse_pc`, etc. |
371 + | `type` | string | item type: `light`, `scroll`, `wand`, `staff`, `weapon`, `treasure`, `armor`, `potion`, `clothing`, `furniture`, `trash`, `container`, `drink`, `key`, `food`, `money`, `boat`, `corpse_npc`, `corpse_pc`, `fountain`, `pill`, `map`, `gem`, `jewelry`, etc. |
296 372
297 373 Items you can't see are omitted (e.g. invisible without detect-invis).
298 374
299 375 **Emitted on:** every look at the current room.
376 +
377 + **Usage:** item panels. Make items clickable for `get <name>` or `look <name>`.
300 378
301 379 ---
302 380
303 381 ### `Map.Tiles`
304 382
305 383 BFS-generated minimap around the player. Lets clients render a tactical floor view plus arbitrary vertical depth.
306 384
307 385 **Top-level shape:**
308 386 ```json
309 387 Map.Tiles {
310 388 "r": 7,
311 389 "g": [ [ cell|null, cell|null, ... ], ... ],
312 390 "a": [ zlayer_room, ... ],
313 391 "b": [ zlayer_room, ... ],
314 392 "zr": [ zroom, ... ],
315 393 "areas": { "<area_vnum>": {"name":"…","color":"#rrggbb"}, … },
316 394 "t": " ... @e ...| ..."
317 395 }
318 396 ```
319 397
320 398 **Top-level fields:**
321 399 | Field | Type | Notes |
322 400 |---|---|---|
323 401 | `r` | int | radius of the BFS (4 small, 7 default, 10 large). Grid is `(2r+1) x (2r+1)`. |
324 402 | `g` | 2D array | rows by columns, current floor only. Positions outside the BFS reach are `null`. Player is always at `[r][r]`. |
325 403 | `a` | array | legacy: rooms reached via *up* exits, `\|z\|=1` only with a 1-cardinal sample. Stable for older clients. New clients can ignore. |
326 404 | `b` | array | legacy mirror of `a` for *down* exits. |
327 405 | `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. |
328 406 | `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. |
329 - | `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. |
407 + | `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. |
330 408
331 409 #### Per-cell (`g[y][x]`) shape
332 410
333 411 ```json
334 412 {
335 413 "s": 1,
336 414 "e": "neswud",
337 415 "l": 2,
338 416 "h": 1,
339 417 "ar": 30,
340 418 "f": "s$b",
341 419 "d": {"n":"locked","s":"closed"},
342 420 "ex": {"n":16600,"e":16602,"w":16603,"u":16800,"d":16900}
343 421 }
344 422 ```
345 423
346 424 | Field | Type | Notes |
347 425 |---|---|---|
348 426 | `s` | int | sector index (0-12, same order as `Room.Info` terrain enum). |
349 427 | `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). |
350 428 | `l` | int | light level 0-4. `0` dark+night, `1` dark, `2` indoors, `3` outdoors+night, `4` outdoors+day. |
351 429 | `h` | int | present and `1` only on the cell you're standing in. Omitted everywhere else. |
352 430 | `ar` | int | area vnum. Cross-reference with top-level `areas` for name and color. |
353 431 | `f` | string | flag chars from `s` (safe/no-PK), `$` (shop), `b` (bank), `t` (trainer), `h` (healer). Omitted when empty. |
354 432 | `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). |
355 433 | `ex` | object | exit destinations as direction-letter to vnum. Omitted if no exits. |
356 434
357 435 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.
436 +
437 + #### Sector colors and exit offsets
438 +
439 + Suggested tile colors per sector index:
440 +
441 + | `s` | Terrain | Suggested color |
442 + |---|---|---|
443 + | 0 | inside | warm brown `#5a4e3c` |
444 + | 1 | city | gray `#787369` |
445 + | 2 | field | green `#3c6928` |
446 + | 3 | forest | dark green `#1c4616` |
447 + | 4 | hills | earth brown `#695532` |
448 + | 5 | mountain | cold gray `#55555f` |
449 + | 6 | water (swim) | light blue `#284b87` |
450 + | 7 | water (deep) | dark blue `#14235a` |
451 + | 8 | swamp | murky green `#374628` |
452 + | 9 | air | sky blue `#6473a5` |
453 + | 10 | desert | sand `#af9150` |
454 + | 11 | lava | orange red `#af3214` |
455 + | 12 | snow | light gray `#afb9c3` |
456 +
457 + Exit letters map to grid neighbors like this:
458 +
459 + | Letter | Direction | Grid offset |
460 + |---|---|---|
461 + | `n` | north | `y - 1` |
462 + | `e` | east | `x + 1` |
463 + | `s` | south | `y + 1` |
464 + | `w` | west | `x - 1` |
465 + | `u` | up | none on `g`, see `zr` |
466 + | `d` | down | none on `g`, see `zr` |
358 467
359 468 #### Per-cell in `a` / `b` (legacy z-layers)
360 469
361 470 ```json
362 471 {"x":7,"y":7,"s":0,"e":"u","vnum":2051}
363 472 ```
364 473
365 474 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.
366 475
367 476 #### Per-room in `zr`
368 477
369 478 ```json
370 479 {
371 480 "x": 5, "y": 7, "z": -2,
372 481 "s": 0,
373 482 "e": "neud",
374 483 "l": 1,
375 484 "ar": 30,
376 485 "f": "s",
377 486 "d": {"u":"closed"},
378 487 "ex": {"n":3041,"e":3042,"u":3010,"d":3120}
379 488 }
380 489 ```
381 490
382 491 | Field | Type | Notes |
383 492 |---|---|---|
384 493 | `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. |
385 494 | `z` | int | floor delta from the player. Positive is above (`1` = one floor up), negative is below. Arbitrary depth, capped only by the radius. |
386 495 | `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`). |
387 496
388 497 #### Secret exits
389 498
390 499 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"`.
391 500
392 501 **Emitted on:** every look at the current room.
393 502
394 503 **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.
395 504
396 505 ---
397 506
398 507 ### `Group.Info`
399 508
400 - Your group roster. Empty (`{}`) when solo.
509 + Your group roster.
401 510
402 511 **Shape (grouped):**
403 512 ```json
404 513 Group.Info {
405 514 "leader": "Durim",
406 515 "members": [
407 516 {"id":1769388810,"name":"Durim","level":50,"class":"Dkn","hp_pct":78,"mana_pct":55,"move_pct":93,"tnl":1250},
408 517 {"id":1769401002,"name":"Gibble","level":50,"class":"Psi","hp_pct":91,"mana_pct":40,"move_pct":88,"tnl":0}
409 518 ]
410 519 }
411 520 ```
412 521
413 522 **Shape (solo):**
414 523 ```json
415 524 Group.Info {}
416 525 ```
417 526
418 527 **Top-level fields:**
419 - - `leader` (string): leader's name.
420 - - `members` (array): every member including you and any charmed followers. Members with `NOWHO` are omitted.
528 + | Field | Type | Notes |
529 + |---|---|---|
530 + | `leader` | string | leader's name |
531 + | `members` | array | every member including you and any charmed followers. The leader is always included. Members with `NOWHO` are omitted. |
421 532
422 533 **Per-member fields:**
423 534 | Field | Type | Notes |
424 535 |---|---|---|
425 536 | `id` | int | stable per-character identity. Key your roster on this, not `name`. Two members can share a `name` (e.g. identical summons); `id` never collides. |
426 537 | `name` | string | member's real name (PCs) or short description (charmies). Not filtered by visibility, so blindness does not turn group members into `someone`. |
427 538 | `level` | int | 1 to 60 |
428 539 | `class` | string | short class key (e.g. `Dkn`, `Psi`, `War`) for PCs, `"mob"` for charmies |
429 540 | `hp_pct`, `mana_pct`, `move_pct` | int | 0 to 100 |
430 541 | `tnl` | int | exp to next level. `0` for NPCs and capped chars. |
431 542
432 543 **Emitted on:** prompt, login, group join/leave.
433 544
434 545 ---
435 546
436 547 ### `World.Time`
437 548
438 - In-game clock and weather.
549 + Game clock and weather.
439 550
440 551 **Shape:**
441 552 ```json
442 553 World.Time {"hour":14,"day":12,"month":3,"year":2026,"sunlight":"light","sky":"cloudy"}
443 554 ```
444 555
445 556 **Fields:**
446 557 | Field | Type | Notes |
447 558 |---|---|---|
448 559 | `hour` | int | 0-23 game-time |
449 560 | `day` | int | day of month |
450 561 | `month` | int | 0-17 (see game calendar) |
451 562 | `year` | int | game year |
452 563 | `sunlight` | string | `"dark"`, `"rise"`, `"light"`, `"set"` |
453 564 | `sky` | string | `"cloudless"`, `"cloudy"`, `"raining"`, `"lightning"` |
454 565
455 566 **Emitted on:** login. Also broadcast to every GMCP-enabled connection on each weather tick (roughly every two minutes real time).
456 567
457 568 ---
458 569
459 570 ### `World.Moons`
460 571
461 - 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.
572 + Moon phases and alignment state.
462 573
463 574 **Shape:**
464 575 ```json
465 576 World.Moons {
466 577 "moons": [
467 578 {"name":"Lysenties","active":true,"phase":4,"phase_name":"full"},
468 579 {"name":"Nercuros","active":true,"phase":2,"phase_name":"first half"},
469 580 {"name":"Dyphrities","active":false,"phase":0,"phase_name":"new"}
470 581 ],
471 582 "eclipse": false,
472 583 "triad": false,
473 584 "near_alignment": false
474 585 }
475 586 ```
476 587
477 588 **Top-level fields:**
478 589 | Field | Type | Notes |
479 590 |---|---|---|
480 591 | `moons` | array | one entry per moon, in fixed order Lysenties / Nercuros / Dyphrities. |
481 592 | `eclipse` | bool | true while a solar or lunar eclipse is active. All moon-driven scalars collapse to zero during eclipse. |
482 593 | `triad` | bool | true when all three moons are full simultaneously. |
483 594 | `near_alignment` | bool | true when phases are close enough to trigger near-alignment buffs/effects. |
484 595
485 596 **Per-moon fields:**
486 597 | Field | Type | Notes |
487 598 |---|---|---|
488 599 | `name` | string | display name (`"Lysenties"`, `"Nercuros"`, `"Dyphrities"`) |
489 600 | `active` | bool | false if the moon is currently dormant (rare; treat as inactive for any moon-derived effects). |
490 601 | `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. |
491 602 | `phase_name` | string | display string for the phase. |
492 603
493 604 **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.
494 605
495 606 ---
496 607
497 608 ### `Comm.Channel`
498 609
499 - One message per chat-like event you hear.
610 + Channel messages delivered to your character.
500 611
501 612 **Base shape (all channels):**
502 613 ```json
503 614 Comm.Channel {"channel":"say","speaker":"Durim","text":"Hello there."}
504 615 ```
505 616
506 617 **Extended shape (say, yell, tell, gtell add language and direction metadata):**
507 618 ```json
508 619 Comm.Channel {
509 620 "channel":"say",
510 621 "speaker":"Durim",
511 622 "text":"xak zee nog",
512 623 "language":"foreign",
513 624 "understood":false
514 625 }
515 626 ```
516 627 ```json
517 628 Comm.Channel {
518 629 "channel":"tell",
519 630 "speaker":"Gibble",
520 631 "text":"meet at the altar",
521 632 "language":"common",
522 633 "understood":true,
523 634 "direction":"received"
524 635 }
525 636 ```
526 637
527 638 **Base fields (always present):**
528 639 | Field | Type | Notes |
529 640 |---|---|---|
530 641 | `channel` | string | see table below |
531 642 | `speaker` | string | visible name. For `tell`, this is the sender (you only receive a `tell` packet as the recipient). |
532 643 | `text` | string | color-stripped. Garbled per-listener on language-aware channels. |
533 644
534 645 **Optional fields (say, yell, tell, gtell):**
535 646 | Field | Type | Notes |
536 647 |---|---|---|
537 648 | `language` | string | language name (`"common"`, `"drow"`, `"orcish"`, etc.) when the listener understood. `"foreign"` when they didn't. The actual foreign language never leaks. |
538 649 | `understood` | bool | `true` means `text` is the original. `false` means `text` is phoneme-garbled to match the terminal's foreign-tongue rendering. |
539 650 | `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. |
540 651
541 652 **Channel values:**
542 653
543 654 | Value | Source | Language-aware | Direction field |
544 655 |---|---|---|---|
545 656 | `say` | `say` | yes | no |
546 657 | `yell` | `yell` | yes | no |
547 658 | `tell` | `tell`, `reply`, language-specific tells (`tarchaic`, `tmogwei`, etc.) | yes | yes (recipient side only) |
548 659 | `gtell` | `gtell` and group-tell projection | yes | no |
549 660 | `pray` | `pray` | no | no |
550 661 | `newbie` | `newbie` | no | no |
551 662 | `cabal` | `cabal chat` | no | no |
552 663 | `clan` | `clan chat` | no | no |
553 664 | `faction` | `faction chat` | no | no |
554 665 | `immortal` | `immtalk` (imms only) | no | no |
555 666 | `imp` | `imptalk` (IMP+ only) | no | no |
556 667
557 668 **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.
558 669
559 670 **Language semantics:**
560 671 - The speaker's own packet (for say, yell, gtell) is always `understood:true` with the actual language name.
561 672 - 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).
562 673 - 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.
563 674
564 675 **Imm/imp chat:** broadcast to everyone with the right trust whose channel toggle is on. No language filtering. No `direction` field.
565 676
566 677 **Emitted on:** each matching channel message.
567 678
568 679 **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.
680 +
681 + **Usage:** route messages to chat tabs by `channel` and color each channel differently.
682 +
683 + ---
684 +
685 + ### Package summary
686 +
687 + | Package | Emitted on | Typical use |
688 + |---|---|---|
689 + | `Char.Vitals` | every prompt | HP, mana, move bars |
690 + | `Char.Status` | login, level gain | name, level, race, class |
691 + | `Char.Worth` | every prompt, login | gold, exp, trains, practices, cabal |
692 + | `Char.Affects` | login, every prompt, affect change | buff and debuff bars |
693 + | `Char.Combat` | prompt, combat end | target health bar |
694 + | `Room.Info` | room look | room name, area, terrain, exits |
695 + | `Room.Chars` | room look | NPCs and players in room |
696 + | `Room.Items` | room look | items on the ground |
697 + | `Map.Tiles` | room look | minimap |
698 + | `Group.Info` | prompt, login, group join/leave | party frames |
699 + | `World.Time` | login, weather tick | day/night, weather |
700 + | `World.Moons` | login, moon state change | moon phase tracker |
701 + | `Comm.Channel` | channel message | chat tabs |
569 702
570 703 ---
571 704
572 705 ## Client to server messages
573 706
574 707 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.
575 708
576 709 ---
577 710
578 711 ## Scripting recipes
712 +
713 + ### JavaScript, minimal raw telnet GMCP client
714 +
715 + Implements the [IAC state machine](#iac-state-machine), echo toggle, and package dispatch. `send`, `writeToTerminal`, `fixupANSI`, `setPasswordMode`, and the `update*` functions are yours to supply.
716 +
717 + ```javascript
718 + // Telnet constants
719 + const IAC = 0xFF, SB = 0xFA, SE = 0xF0;
720 + const WILL = 0xFB, WONT = 0xFC, DO = 0xFD, DONT = 0xFE;
721 + const GMCP = 0xC9; // 201
722 + const TELOPT_ECHO = 0x01;
723 +
724 + // State machine
725 + let iacState = 0, iacVerb = 0;
726 + let subneg = [];
727 + let textBuf = [];
728 +
729 + function processBytes(data) {
730 + for (let i = 0; i < data.length; i++) {
731 + const b = data[i];
732 +
733 + switch (iacState) {
734 + case 0: // normal
735 + if (b === IAC) iacState = 1;
736 + else textBuf.push(b);
737 + break;
738 +
739 + case 1: // got IAC
740 + if (b === IAC) { textBuf.push(0xFF); iacState = 0; }
741 + else if (b === WILL || b === WONT || b === DO || b === DONT)
742 + { iacVerb = b; iacState = 2; }
743 + else if (b === SB) { subneg = []; iacState = 3; }
744 + else iacState = 0;
745 + break;
746 +
747 + case 2: // got IAC + verb, b = option
748 + if (iacVerb === WILL && b === GMCP) {
749 + send(new Uint8Array([IAC, DO, GMCP]));
750 + }
751 + if (b === TELOPT_ECHO) {
752 + if (iacVerb === WILL) setPasswordMode(true);
753 + else if (iacVerb === WONT) setPasswordMode(false);
754 + }
755 + iacState = 0;
756 + break;
757 +
758 + case 3: // inside subneg
759 + if (b === IAC) iacState = 4;
760 + else subneg.push(b);
761 + break;
762 +
763 + case 4: // IAC inside subneg
764 + if (b === SE) {
765 + handleSubneg(subneg);
766 + iacState = 0;
767 + } else if (b === IAC) {
768 + subneg.push(0xFF);
769 + iacState = 3;
770 + } else iacState = 0;
771 + break;
772 + }
773 + }
774 +
775 + // Flush text to terminal
776 + if (textBuf.length > 0) {
777 + writeToTerminal(fixupANSI(new Uint8Array(textBuf)));
778 + textBuf = [];
779 + }
780 + }
781 +
782 + function handleSubneg(data) {
783 + if (data.length < 1 || data[0] !== GMCP) return;
784 +
785 + // Strip option byte, split "Package.Name {json}"
786 + const payload = new TextDecoder().decode(new Uint8Array(data.slice(1)));
787 + const spaceIdx = payload.indexOf(' ');
788 + if (spaceIdx < 0) return;
789 +
790 + const pkg = payload.substring(0, spaceIdx);
791 + const json = JSON.parse(payload.substring(spaceIdx + 1));
792 +
793 + switch (pkg) {
794 + case 'Char.Vitals': updateVitals(json); break;
795 + case 'Char.Status': updateStatus(json); break;
796 + case 'Char.Worth': updateWorth(json); break;
797 + case 'Char.Affects': updateAffects(json.affects); break;
798 + case 'Char.Combat': updateCombat(json); break;
799 + case 'Room.Info': updateRoom(json); break;
800 + case 'Room.Chars': updateRoomChars(json); break;
801 + case 'Room.Items': updateRoomItems(json); break;
802 + case 'Map.Tiles': renderMap(json); break;
803 + case 'Group.Info': updateGroup(json); break;
804 + case 'World.Time': updateTime(json); break;
805 + case 'World.Moons': updateMoons(json); break;
806 + case 'Comm.Channel': appendChat(json.channel, json.speaker, json.text); break;
807 + }
808 + }
809 + ```
810 +
811 + ### Mudlet, handler for every package
812 +
813 + Mudlet parses GMCP natively. Register event handlers and read from the `gmcp` table.
814 +
815 + ```lua
816 + -- Vitals
817 + registerAnonymousEventHandler("gmcp.Char.Vitals", function()
818 + local v = gmcp.Char.Vitals
819 + -- v.hp, v.maxhp, v.mana, v.maxmana, v.move, v.maxmove
820 + end)
821 +
822 + -- Status
823 + registerAnonymousEventHandler("gmcp.Char.Status", function()
824 + local s = gmcp.Char.Status
825 + -- s.name, s.level, s.race, s.class
826 + end)
827 +
828 + -- Worth
829 + registerAnonymousEventHandler("gmcp.Char.Worth", function()
830 + local w = gmcp.Char.Worth
831 + -- w.gold, w.bank, w.exp, w.tnl, w.trains, w.practices, w.cps, w.rps, w.cabal
832 + end)
833 +
834 + -- Affects
835 + registerAnonymousEventHandler("gmcp.Char.Affects", function()
836 + for _, aff in ipairs(gmcp.Char.Affects.affects or {}) do
837 + -- aff.kind, aff.name, aff.duration, aff.level, aff.location, aff.modifier
838 + end
839 + end)
840 +
841 + -- Combat
842 + registerAnonymousEventHandler("gmcp.Char.Combat", function()
843 + local c = gmcp.Char.Combat
844 + if c.target then
845 + -- fighting: c.target, c.condition, c.hp_pct
846 + else
847 + -- combat ended (empty object)
848 + end
849 + end)
850 +
851 + -- Room info
852 + registerAnonymousEventHandler("gmcp.Room.Info", function()
853 + local r = gmcp.Room.Info
854 + -- r.num, r.name, r.area, r.terrain, r.sector, r.region, r.climate, r.exits
855 + centerview(r.num) -- update Mudlet mapper
856 + end)
857 +
858 + -- Room characters
859 + registerAnonymousEventHandler("gmcp.Room.Chars", function()
860 + for _, c in ipairs(gmcp.Room.Chars) do
861 + -- c.name, c.npc
862 + end
863 + end)
864 +
865 + -- Room items
866 + registerAnonymousEventHandler("gmcp.Room.Items", function()
867 + for _, item in ipairs(gmcp.Room.Items) do
868 + -- item.name, item.type
869 + end
870 + end)
871 +
872 + -- Map tiles
873 + registerAnonymousEventHandler("gmcp.Map.Tiles", function()
874 + local m = gmcp.Map.Tiles
875 + -- m.r, m.g, m.zr, m.areas, m.t
876 + end)
877 +
878 + -- Group
879 + registerAnonymousEventHandler("gmcp.Group.Info", function()
880 + local g = gmcp.Group.Info
881 + if g.leader then
882 + for _, m in ipairs(g.members) do
883 + -- m.id, m.name, m.level, m.class, m.hp_pct, m.mana_pct, m.move_pct, m.tnl
884 + end
885 + else
886 + -- solo (empty object)
887 + end
888 + end)
889 +
890 + -- Time and weather
891 + registerAnonymousEventHandler("gmcp.World.Time", function()
892 + local t = gmcp.World.Time
893 + -- t.hour, t.day, t.month, t.year, t.sunlight, t.sky
894 + end)
895 +
896 + -- Moons
897 + registerAnonymousEventHandler("gmcp.World.Moons", function()
898 + local m = gmcp.World.Moons
899 + -- m.moons, m.eclipse, m.triad, m.near_alignment
900 + end)
901 +
902 + -- Chat
903 + registerAnonymousEventHandler("gmcp.Comm.Channel", function()
904 + local c = gmcp.Comm.Channel
905 + cecho(string.format("\n[%s] %s: %s", c.channel, c.speaker, c.text))
906 + end)
907 + ```
579 908
580 909 ### Mudlet, HP/mana bars from Char.Vitals
581 910
582 911 ```lua
583 912 registerAnonymousEventHandler("gmcp.Char.Vitals", function()
584 913 local v = gmcp.Char.Vitals
585 914 hpBar:setValue(v.hp, v.maxhp)
586 915 manaBar:setValue(v.mana, v.maxmana)
587 916 moveBar:setValue(v.move, v.maxmove)
588 917 end)
589 918 ```
590 919
591 920 ### Mudlet, combat target widget
592 921
593 922 ```lua
594 923 registerAnonymousEventHandler("gmcp.Char.Combat", function()
595 924 local c = gmcp.Char.Combat
596 925 if not c or not c.target then
597 926 combatHUD:hide()
598 927 else
599 928 combatHUD:show()
600 929 combatName:echo(c.target)
601 930 combatBar:setValue(c.hp_pct, 100)
602 931 end
603 932 end)
604 933 ```
605 934
606 935 ### Mudlet, songs separate from spells
607 936
608 937 ```lua
609 938 registerAnonymousEventHandler("gmcp.Char.Affects", function()
610 939 local spells, songs = {}, {}
611 940 for _, a in ipairs(gmcp.Char.Affects.affects or {}) do
612 941 if a.kind == "song" then
613 942 table.insert(songs, a)
614 943 else
615 944 table.insert(spells, a)
616 945 end
617 946 end
618 947 renderSpells(spells)
619 948 renderSongs(songs)
620 949 end)
621 950 ```
622 951
623 952 ### Mudlet, minimap with zone tints
624 953
625 954 ```lua
626 955 registerAnonymousEventHandler("gmcp.Map.Tiles", function()
627 956 local m = gmcp.Map.Tiles
628 957 local r = m.r
629 958 for y = 1, #m.g do
630 959 for x = 1, #m.g[y] do
631 960 local cell = m.g[y][x]
632 961 if cell == vim.NIL then cell = nil end -- Mudlet uses vim.NIL for JSON null
633 962 if cell then
634 963 local tint = m.areas and m.areas[tostring(cell.ar)] and m.areas[tostring(cell.ar)].color or "#444"
635 964 drawCell(x, y, cell.s, cell.e, tint, cell.l, cell.f, cell.h)
636 965 else
637 966 drawCell(x, y, nil)
638 967 end
639 968 end
640 969 end
641 970 -- Off-floor rooms (towers, dungeons with stacked floors)
642 971 for _, z in ipairs(m.zr or {}) do
643 972 drawZRoom(z.x, z.y, z.z, z.s, z.e, z.ar)
644 973 end
645 974 end)
646 975 ```
647 976
648 977 ### Mudlet, World.Moons phase tracker
649 978
650 979 ```lua
651 980 registerAnonymousEventHandler("gmcp.World.Moons", function()
652 981 local m = gmcp.World.Moons
653 982 for _, moon in ipairs(m.moons) do
654 983 moonWidget[moon.name]:setPhase(moon.phase, moon.phase_name, moon.active)
655 984 end
656 985 eclipseIndicator:setVisible(m.eclipse)
657 986 triadIndicator:setVisible(m.triad)
658 987 end)
988 + ```
989 +
990 + ### tintin++, event handler for every package
991 +
992 + tintin++ exposes GMCP through `#event`.
993 +
994 + ```
995 + #event {IAC WILL GMCP} {
996 + #send {$IAC $DO $GMCP\};
997 + }
998 +
999 + #event {IAC SB GMCP Char.Vitals IAC SE} {
1000 + #var {vitals} {%0};
1001 + #showme {HP: $vitals[hp]/$vitals[maxhp] Mana: $vitals[mana]/$vitals[maxmana]};
1002 + }
1003 +
1004 + #event {IAC SB GMCP Char.Status IAC SE} { #var {status} {%0}; }
1005 + #event {IAC SB GMCP Char.Affects IAC SE} { #var {affects} {%0}; }
1006 + #event {IAC SB GMCP Char.Combat IAC SE} { #var {combat} {%0}; }
1007 +
1008 + #event {IAC SB GMCP Char.Worth IAC SE} {
1009 + #var {worth} {%0};
1010 + #showme {Gold: $worth[gold] Exp: $worth[exp] TNL: $worth[tnl]};
1011 + }
1012 +
1013 + #event {IAC SB GMCP Room.Info IAC SE} {
1014 + #var {room} {%0};
1015 + #showme {Room: $room[name] ($room[terrain])};
1016 + }
1017 +
1018 + #event {IAC SB GMCP Room.Chars IAC SE} { #var {roomchars} {%0}; }
1019 + #event {IAC SB GMCP Room.Items IAC SE} { #var {roomitems} {%0}; }
1020 + #event {IAC SB GMCP Map.Tiles IAC SE} { #var {map} {%0}; }
1021 + #event {IAC SB GMCP Group.Info IAC SE} { #var {group} {%0}; }
1022 +
1023 + #event {IAC SB GMCP World.Time IAC SE} {
1024 + #var {time} {%0};
1025 + #showme {Time: $time[hour]:00 Sky: $time[sky]};
1026 + }
1027 +
1028 + #event {IAC SB GMCP World.Moons IAC SE} { #var {moons} {%0}; }
1029 +
1030 + #event {IAC SB GMCP Comm.Channel IAC SE} {
1031 + #var {chan} {%0};
1032 + #showme {[$chan[channel]] $chan[speaker]: $chan[text]};
1033 + }
659 1034 ```
660 1035
661 1036 ### tintin++, chat log split by channel
662 1037
663 1038 ```
664 1039 #action {Comm.Channel %*} {
665 1040 #var CHAN %1[channel];
666 1041 #var WHO %1[speaker];
667 1042 #var MSG %1[text];
668 1043 #line log chat-$CHAN.txt <$WHO>: $MSG
669 1044 }
670 1045 ```
671 1046
672 1047 ### Generic, room-change trigger
673 1048
674 1049 `Room.Info` fires once per look. Stash `room.num` to detect movement:
675 1050
676 1051 ```
677 1052 prevRoom = nil
678 1053 onRoomInfo = function(info)
679 1054 if info.num ~= prevRoom then
680 1055 prevRoom = info.num
681 1056 onEnterRoom(info)
682 1057 end
683 1058 end
684 1059 ```
685 1060
686 1061 ---
687 1062
688 1063 ## Caveats and gotchas
689 1064
690 1065 ### Ordering
691 1066 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.
692 1067
693 1068 ### Stale packets
694 1069 - `Char.Combat {}` is the explicit "combat ended" signal. Don't wait for a timeout.
695 1070 - `Group.Info {}` is the explicit "solo" signal.
696 1071 - `Char.Affects` always sends the full list. Replace, don't merge.
697 1072
698 1073 ### Map timing
699 1074 `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.
700 1075
701 1076 ### Size limits
702 1077 - `Char.Affects`: up to 8KB. Truncates silently on overflow.
703 1078 - `Map.Tiles`: up to 96KB.
704 1079 - `Group.Info`, `Room.Chars`, `Room.Items`: up to 8KB.
705 1080 - `Comm.Channel` text field: up to about 1KB after JSON-escape.
706 1081
707 1082 ### Invisibility
708 1083 `Room.Chars` applies the normal `can_see()` check. Your client only sees what your character sees. `Group.Info` is the exception: it always names your own group members (you are grouped with them, so their identity is not hidden), so blindness does not collapse the roster to `someone`.
709 1084
710 1085 ### Color stripping
711 1086 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.
1087 +
1088 + ### Bare ANSI sequences
1089 + 16-color codes arrive without the ESC byte. Terminal emulators need the [fixup](#ansi-color-codes) or they print `[0;31m` as literal text.
712 1090
713 1091 ### Secret exits
714 1092 Non-imms: secret passages and closed-and-secret doors are absent from `Map.Tiles`. Imms see them with `d` value `"hidden"`.
715 1093
716 1094 ### Reconnection
717 1095 After copyover/hotboot the server re-emits the login package set. Don't assume any package's content survives across disconnects.
718 1096
719 1097 ### When GMCP goes silent
720 1098 If you stop receiving packets:
721 1099 1. Confirm `IAC DO 201` was sent.
722 1100 2. Confirm your terminal isn't stripping IAC sequences (some unix `tail` setups do).
723 1101 3. `Core.Hello` isn't required, but sending it keeps server-side fingerprint and telemetry useful.
724 1102
725 1103 ---
726 1104
727 1105 ## Source of truth
728 1106
729 - 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.
730 -
1107 + 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.