Aabahran

Diff: GMCP

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