| 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 |
|
## Contents
|
| 6 |
6 |
|
|
| 7 |
7 |
|
1. [Protocol basics](#protocol-basics)
|
| 8 |
8 |
|
2. [Handshake and client identification](#handshake-and-client-identification)
|
| 9 |
9 |
|
3. [Package reference](#package-reference)
|
| 10 |
10 |
|
- [Char.Vitals](#charvitals)
|
| 11 |
11 |
|
- [Char.Status](#charstatus)
|
| 12 |
12 |
|
- [Char.Worth](#charworth)
|
| 13 |
13 |
|
- [Char.Affects](#charaffects)
|
| 14 |
14 |
|
- [Char.Combat](#charcombat)
|
| 15 |
15 |
|
- [Room.Info](#roominfo)
|
| 16 |
16 |
|
- [Room.Chars](#roomchars)
|
| 17 |
17 |
|
- [Room.Items](#roomitems)
|
| 18 |
18 |
|
- [Map.Tiles](#maptiles)
|
| 19 |
19 |
|
- [Group.Info](#groupinfo)
|
| 20 |
20 |
|
- [World.Time](#worldtime)
|
| 21 |
21 |
|
- [World.Moons](#worldmoons)
|
| 22 |
22 |
|
- [Comm.Channel](#commchannel)
|
| 23 |
23 |
|
4. [Client to server messages](#client-to-server-messages)
|
| 24 |
24 |
|
5. [Scripting recipes](#scripting-recipes)
|
| 25 |
25 |
|
6. [Caveats and gotchas](#caveats-and-gotchas)
|
| 26 |
26 |
|
|
| 27 |
27 |
|
---
|
| 28 |
28 |
|
|
| 29 |
29 |
|
## Protocol basics
|
| 30 |
30 |
|
|
| 31 |
31 |
|
GMCP rides on TELNET option 201 (0xC9). Each message looks like:
|
| 32 |
32 |
|
|
| 33 |
33 |
|
```
|
| 34 |
34 |
|
IAC SB 201 <package-name> <space> <json-payload> IAC SE
|
| 35 |
35 |
|
```
|
| 36 |
36 |
|
|
| 37 |
37 |
|
Example:
|
| 38 |
38 |
|
```
|
| 39 |
39 |
|
IAC SB 201 Char.Vitals {"hp":850,"maxhp":900,...} IAC SE
|
| 40 |
40 |
|
```
|
| 41 |
41 |
|
|
| 42 |
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.
|
| 43 |
43 |
|
|
| 44 |
44 |
|
### Enabling GMCP
|
| 45 |
45 |
|
|
| 46 |
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.
|
| 47 |
47 |
|
|
| 48 |
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.
|
| 49 |
49 |
|
|
| 50 |
50 |
|
### Packet ordering
|
| 51 |
51 |
|
|
| 52 |
52 |
|
GMCP messages interleave with normal telnet output. A single `look` produces a burst:
|
| 53 |
53 |
|
|
| 54 |
54 |
|
```
|
| 55 |
55 |
|
Room.Info {...}
|
| 56 |
56 |
|
Room.Chars [...]
|
| 57 |
57 |
|
Room.Items [...]
|
| 58 |
58 |
|
Map.Tiles {...}
|
| 59 |
59 |
|
<then the plain-text room description>
|
| 60 |
60 |
|
```
|
| 61 |
61 |
|
|
| 62 |
62 |
|
Buffer packets by name and keep the latest value for each one. Older values are stale.
|
| 63 |
63 |
|
|
| 64 |
64 |
|
### String escaping
|
| 65 |
65 |
|
|
| 66 |
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.
|
| 67 |
67 |
|
|
| 68 |
68 |
|
---
|
| 69 |
69 |
|
|
| 70 |
70 |
|
## Handshake and client identification
|
| 71 |
71 |
|
|
| 72 |
72 |
|
### Server to client
|
| 73 |
73 |
|
|
| 74 |
74 |
|
Sent right after telnet negotiation:
|
| 75 |
75 |
|
```
|
| 76 |
76 |
|
IAC WILL 201 (server)
|
| 77 |
77 |
|
IAC DO 201 (client, your job)
|
| 78 |
78 |
|
```
|
| 79 |
79 |
|
|
| 80 |
80 |
|
### Client to server
|
| 81 |
81 |
|
|
| 82 |
82 |
|
Three optional packages the server watches for.
|
| 83 |
83 |
|
|
| 84 |
84 |
|
#### `Core.Hello`
|
| 85 |
85 |
|
Standard GMCP fingerprint. Send once, right after you accept the option.
|
| 86 |
86 |
|
```json
|
| 87 |
87 |
|
Core.Hello {"client":"Mudlet","version":"4.17.2"}
|
| 88 |
88 |
|
```
|
| 89 |
89 |
|
Fields: `client` (string), `version` (string). Stored server-side for stats. No functional consequence.
|
| 90 |
90 |
|
|
| 91 |
91 |
|
#### `Client.Fingerprint.Report`
|
| 92 |
92 |
|
Free-form JSON. Forsaken Lands accepts it and stores it per-connection. Skip it if you don't need it.
|
| 93 |
93 |
|
|
| 94 |
94 |
|
#### `Proxy.Ident`
|
| 95 |
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.
|
| 96 |
96 |
|
|
| 97 |
97 |
|
---
|
| 98 |
98 |
|
|
| 99 |
99 |
|
## Package reference
|
| 100 |
100 |
|
|
| 101 |
101 |
|
### `Char.Vitals`
|
| 102 |
102 |
|
|
| 103 |
103 |
|
Drives HP, mana, and movement bars.
|
| 104 |
104 |
|
|
| 105 |
105 |
|
**Shape:**
|
| 106 |
106 |
|
```json
|
| 107 |
107 |
|
Char.Vitals {"hp":850,"maxhp":900,"mana":760,"maxmana":820,"move":250,"maxmove":250}
|
| 108 |
108 |
|
```
|
| 109 |
109 |
|
|
| 110 |
110 |
|
**Fields:**
|
| 111 |
111 |
|
| Field | Type | Notes |
|
| 112 |
112 |
|
|---|---|---|
|
| 113 |
113 |
|
| `hp` | int | current hit points |
|
| 114 |
114 |
|
| `maxhp` | int | max hit points with buffs |
|
| 115 |
115 |
|
| `mana` | int | current mana |
|
| 116 |
116 |
|
| `maxmana` | int | max mana with buffs |
|
| 117 |
117 |
|
| `move` | int | current movement |
|
| 118 |
118 |
|
| `maxmove` | int | max movement |
|
| 119 |
119 |
|
|
| 120 |
120 |
|
**Emitted on:** every prompt fire.
|
| 121 |
121 |
|
|
| 122 |
122 |
|
---
|
| 123 |
123 |
|
|
| 124 |
124 |
|
### `Char.Status`
|
| 125 |
125 |
|
|
| 126 |
126 |
|
Slow-changing identity fields.
|
| 127 |
127 |
|
|
| 128 |
128 |
|
**Shape:**
|
| 129 |
129 |
|
```json
|
| 130 |
130 |
|
Char.Status {"name":"Gibble","level":50,"race":"gnome","class":"Psi"}
|
| 131 |
131 |
|
```
|
| 132 |
132 |
|
|
| 133 |
133 |
|
**Fields:**
|
| 134 |
134 |
|
| Field | Type | Notes |
|
| 135 |
135 |
|
|---|---|---|
|
| 136 |
136 |
|
| `name` | string | character name |
|
| 137 |
137 |
|
| `level` | int | character level |
|
| 138 |
138 |
|
| `race` | string | race table name, lower-case short key |
|
| 139 |
139 |
|
| `class` | string | class table name (display casing preserved) |
|
| 140 |
140 |
|
|
| 141 |
141 |
|
**Emitted on:** login, level gain.
|
| 142 |
142 |
|
|
| 143 |
143 |
|
---
|
| 144 |
144 |
|
|
| 145 |
145 |
|
### `Char.Worth`
|
| 146 |
146 |
|
|
| 147 |
147 |
|
Currency and soft progression counters.
|
| 148 |
148 |
|
|
| 149 |
149 |
|
**Shape:**
|
| 150 |
150 |
|
```json
|
| 151 |
151 |
|
Char.Worth {"gold":12345,"bank":99000,"exp":812400,"tnl":45085,"trains":2,"practices":0,"cps":1713,"rps":9,"cabal":"Knight"}
|
| 152 |
152 |
|
```
|
| 153 |
153 |
|
|
| 154 |
154 |
|
**Fields:**
|
| 155 |
155 |
|
| Field | Type | Notes |
|
| 156 |
156 |
|
|---|---|---|
|
| 157 |
157 |
|
| `gold` | int | carried gold |
|
| 158 |
158 |
|
| `bank` | int | banked gold |
|
| 159 |
159 |
|
| `exp` | int | lifetime earned experience |
|
| 160 |
160 |
|
| `tnl` | int | experience remaining to next level (0 if capped) |
|
| 161 |
161 |
|
| `trains` | int | unspent training sessions |
|
| 162 |
162 |
|
| `practices` | int | unspent practice sessions |
|
| 163 |
163 |
|
| `cps` | int | cabal points |
|
| 164 |
164 |
|
| `rps` | int | roleplay points |
|
| 165 |
165 |
|
| `cabal` | string | current cabal name, or `"none"` if uncabaled |
|
| 166 |
166 |
|
|
| 167 |
167 |
|
**Emitted on:** every prompt and login.
|
| 168 |
168 |
|
|
| 169 |
169 |
|
---
|
| 170 |
170 |
|
|
| 171 |
171 |
|
### `Char.Affects`
|
| 172 |
172 |
|
|
| 173 |
173 |
|
Every active spell, skill, and song. Same list the in-game `affects` command shows.
|
| 174 |
174 |
|
|
| 175 |
175 |
|
**Shape:**
|
| 176 |
176 |
|
```json
|
| 177 |
177 |
|
Char.Affects {"affects":[
|
| 178 |
178 |
|
{"kind":"spell","name":"bless","duration":6,"level":50,"location":"hitroll","modifier":4},
|
| 179 |
179 |
|
{"kind":"spell","name":"armor","duration":44,"level":50,"location":"ac","modifier":-20},
|
| 180 |
180 |
|
{"kind":"song","name":"bagatelle of bravado","duration":8,"level":50,"location":"damroll","modifier":3}
|
| 181 |
181 |
|
]}
|
| 182 |
182 |
|
```
|
| 183 |
183 |
|
|
| 184 |
184 |
|
**Fields per affect:**
|
| 185 |
185 |
|
| Field | Type | Notes |
|
| 186 |
186 |
|
|---|---|---|
|
| 187 |
187 |
|
| `kind` | string | `"spell"` or `"song"`. Group on this. Don't trust name alone (see below). |
|
| 188 |
188 |
|
| `name` | string | stripped of color codes |
|
| 189 |
189 |
|
| `duration` | int | ticks remaining (a tick is roughly 30 seconds to a minute of real time). `-1` means permanent. |
|
| 190 |
190 |
|
| `level` | int | level the affect was cast at |
|
| 191 |
191 |
|
| `location` | string | stat it modifies (`"hitroll"`, `"damroll"`, `"ac"`, `"str"`, `"mana"`, `"none"`, etc.) |
|
| 192 |
192 |
|
| `modifier` | int | magnitude (negative is better for AC, etc.) |
|
| 193 |
193 |
|
|
| 194 |
194 |
|
The payload is the full state, not a diff. Replace your local list each time.
|
| 195 |
195 |
|
|
| 196 |
196 |
|
**Emitted on:** login, every prompt, and any add/remove/expire.
|
| 197 |
197 |
|
|
| 198 |
198 |
|
**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.
|
| 199 |
199 |
|
|
| 200 |
200 |
|
---
|
| 201 |
201 |
|
|
| 202 |
202 |
|
### `Char.Combat`
|
| 203 |
203 |
|
|
| 204 |
204 |
|
Set when fighting. Empty object when not.
|
| 205 |
205 |
|
|
| 206 |
206 |
|
**Shape (fighting):**
|
| 207 |
207 |
|
```json
|
| 208 |
208 |
|
Char.Combat {"target":"Durim","condition":"quite a few wounds","hp_pct":54}
|
| 209 |
209 |
|
```
|
| 210 |
210 |
|
|
| 211 |
211 |
|
**Shape (not fighting):**
|
| 212 |
212 |
|
```json
|
| 213 |
213 |
|
Char.Combat {}
|
| 214 |
214 |
|
```
|
| 215 |
215 |
|
|
| 216 |
216 |
|
**Fields:**
|
| 217 |
217 |
|
| Field | Type | Notes |
|
| 218 |
218 |
|
|---|---|---|
|
| 219 |
219 |
|
| `target` | string | name of the char you're fighting (stripped) |
|
| 220 |
220 |
|
| `condition` | string | flavor text: `"excellent"`, `"a few scratches"`, `"small wounds"`, `"quite a few wounds"`, `"big nasty wounds"`, `"pretty hurt"`, `"awful"`, `"bleeding to death"` |
|
| 221 |
221 |
|
| `hp_pct` | int | 0 to 100; clamps to 0 on death |
|
| 222 |
222 |
|
|
| 223 |
223 |
|
**Emitted on:** prompt. The empty object fires once when combat ends, so use it as the explicit "tear down combat HUD" signal.
|
| 224 |
224 |
|
|
| 225 |
225 |
|
---
|
| 226 |
226 |
|
|
| 227 |
227 |
|
### `Room.Info`
|
| 228 |
228 |
|
|
| 229 |
229 |
|
The room you're standing in.
|
| 230 |
230 |
|
|
| 231 |
231 |
|
**Shape:**
|
| 232 |
232 |
|
```json
|
| 233 |
233 |
|
Room.Info {
|
| 234 |
234 |
|
"num": 16601,
|
| 235 |
235 |
|
"name": "The Academy Courtyard",
|
| 236 |
236 |
|
"area": "Val Miran",
|
| 237 |
237 |
|
"terrain": "city",
|
| 238 |
238 |
|
"exits": {"north":16600,"east":16602,"west":16603}
|
| 239 |
239 |
|
}
|
| 240 |
240 |
|
```
|
| 241 |
241 |
|
|
| 242 |
242 |
|
**Fields:**
|
| 243 |
243 |
|
| Field | Type | Notes |
|
| 244 |
244 |
|
|---|---|---|
|
| 245 |
245 |
|
| `num` | int | room vnum |
|
| 246 |
246 |
|
| `name` | string | stripped of color codes |
|
| 247 |
247 |
|
| `area` | string | area name (zone) |
|
| 248 |
248 |
|
| `terrain` | string | one of: `inside`, `city`, `field`, `forest`, `hills`, `mountain`, `water_swim`, `water_noswim`, `swamp`, `air`, `desert`, `lava`, `snow` |
|
| 249 |
249 |
|
| `exits` | object | direction-name to destination room vnum. Includes `up`/`down` when present. |
|
| 250 |
250 |
|
|
| 251 |
251 |
|
**Emitted on:** every look at your current room (movement, force-look, etc).
|
| 252 |
252 |
|
|
| 253 |
253 |
|
---
|
| 254 |
254 |
|
|
| 255 |
255 |
|
### `Room.Chars`
|
| 256 |
256 |
|
|
| 257 |
257 |
|
Everyone else visible in your room.
|
| 258 |
258 |
|
|
| 259 |
259 |
|
**Shape:**
|
| 260 |
260 |
|
```json
|
| 261 |
261 |
|
Room.Chars [
|
| 262 |
262 |
|
{"name":"Durim","npc":false},
|
| 263 |
263 |
|
{"name":"a city guard","npc":true}
|
| 264 |
264 |
|
]
|
| 265 |
265 |
|
```
|
| 266 |
266 |
|
|
| 267 |
267 |
|
**Fields per entry:**
|
| 268 |
268 |
|
| Field | Type | Notes |
|
| 269 |
269 |
|
|---|---|---|
|
| 270 |
270 |
|
| `name` | string | as you see them (handles invis/disguise/PERS()) |
|
| 271 |
271 |
|
| `npc` | bool | `true` for mobs, `false` for PCs |
|
| 272 |
272 |
|
|
| 273 |
273 |
|
You are omitted. People you can't see are omitted.
|
| 274 |
274 |
|
|
| 275 |
275 |
|
**Emitted on:** every look at the current room.
|
| 276 |
276 |
|
|
| 277 |
277 |
|
---
|
| 278 |
278 |
|
|
| 279 |
279 |
|
### `Room.Items`
|
| 280 |
280 |
|
|
| 281 |
281 |
|
Objects on the ground.
|
| 282 |
282 |
|
|
| 283 |
283 |
|
**Shape:**
|
| 284 |
284 |
|
```json
|
| 285 |
285 |
|
Room.Items [
|
| 286 |
286 |
|
{"name":"a worn leather boot","type":"armor"},
|
| 287 |
287 |
|
{"name":"a pile of gold coins","type":"money"}
|
| 288 |
288 |
|
]
|
| 289 |
289 |
|
```
|
| 290 |
290 |
|
|
| 291 |
291 |
|
**Fields per entry:**
|
| 292 |
292 |
|
| Field | Type | Notes |
|
| 293 |
293 |
|
|---|---|---|
|
| 294 |
294 |
|
| `name` | string | short description, color-stripped |
|
| 295 |
295 |
|
| `type` | string | item type: `weapon`, `armor`, `container`, `food`, `potion`, `wand`, `scroll`, `staff`, `money`, `light`, `corpse_npc`, `corpse_pc`, etc. |
|
| 296 |
296 |
|
|
| 297 |
297 |
|
Items you can't see are omitted (e.g. invisible without detect-invis).
|
| 298 |
298 |
|
|
| 299 |
299 |
|
**Emitted on:** every look at the current room.
|
| 300 |
300 |
|
|
| 301 |
301 |
|
---
|
| 302 |
302 |
|
|
| 303 |
303 |
|
### `Map.Tiles`
|
| 304 |
304 |
|
|
| 305 |
305 |
|
BFS-generated minimap around the player. Lets clients render a tactical floor view plus arbitrary vertical depth.
|
| 306 |
306 |
|
|
| 307 |
307 |
|
**Top-level shape:**
|
| 308 |
308 |
|
```json
|
| 309 |
309 |
|
Map.Tiles {
|
| 310 |
310 |
|
"r": 7,
|
| 311 |
311 |
|
"g": [ [ cell|null, cell|null, ... ], ... ],
|
| 312 |
312 |
|
"a": [ zlayer_room, ... ],
|
| 313 |
313 |
|
"b": [ zlayer_room, ... ],
|
| 314 |
314 |
|
"zr": [ zroom, ... ],
|
| 315 |
315 |
|
"areas": { "<area_vnum>": {"name":"…","color":"#rrggbb"}, … },
|
| 316 |
316 |
|
"t": " ... @e ...| ..."
|
| 317 |
317 |
|
}
|
| 318 |
318 |
|
```
|
| 319 |
319 |
|
|
| 320 |
320 |
|
**Top-level fields:**
|
| 321 |
321 |
|
| Field | Type | Notes |
|
| 322 |
322 |
|
|---|---|---|
|
| 323 |
323 |
|
| `r` | int | radius of the BFS (4 small, 7 default, 10 large). Grid is `(2r+1) x (2r+1)`. |
|
| 324 |
324 |
|
| `g` | 2D array | rows by columns, current floor only. Positions outside the BFS reach are `null`. Player is always at `[r][r]`. |
|
| 325 |
325 |
|
| `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 |
326 |
|
| `b` | array | legacy mirror of `a` for *down* exits. |
|
| 327 |
327 |
|
| `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 |
328 |
|
| `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 |
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. |
|
| 330 |
330 |
|
|
| 331 |
331 |
|
#### Per-cell (`g[y][x]`) shape
|
| 332 |
332 |
|
|
| 333 |
333 |
|
```json
|
| 334 |
334 |
|
{
|
| 335 |
335 |
|
"s": 1,
|
| 336 |
336 |
|
"e": "neswud",
|
| 337 |
337 |
|
"l": 2,
|
| 338 |
338 |
|
"h": 1,
|
| 339 |
339 |
|
"ar": 30,
|
| 340 |
340 |
|
"f": "s$b",
|
| 341 |
341 |
|
"d": {"n":"locked","s":"closed"},
|
| 342 |
342 |
|
"ex": {"n":16600,"e":16602,"w":16603,"u":16800,"d":16900}
|
| 343 |
343 |
|
}
|
| 344 |
344 |
|
```
|
| 345 |
345 |
|
|
| 346 |
346 |
|
| Field | Type | Notes |
|
| 347 |
347 |
|
|---|---|---|
|
| 348 |
348 |
|
| `s` | int | sector index (0-12, same order as `Room.Info` terrain enum). |
|
| 349 |
349 |
|
| `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 |
350 |
|
| `l` | int | light level 0-4. `0` dark+night, `1` dark, `2` indoors, `3` outdoors+night, `4` outdoors+day. |
|
| 351 |
351 |
|
| `h` | int | present and `1` only on the cell you're standing in. Omitted everywhere else. |
|
| 352 |
352 |
|
| `ar` | int | area vnum. Cross-reference with top-level `areas` for name and color. |
|
| 353 |
353 |
|
| `f` | string | flag chars from `s` (safe/no-PK), `$` (shop), `b` (bank), `t` (trainer), `h` (healer). Omitted when empty. |
|
| 354 |
354 |
|
| `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 |
355 |
|
| `ex` | object | exit destinations as direction-letter to vnum. Omitted if no exits. |
|
| 356 |
356 |
|
|
| 357 |
357 |
|
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.
|
| 358 |
358 |
|
|
| 359 |
359 |
|
#### Per-cell in `a` / `b` (legacy z-layers)
|
| 360 |
360 |
|
|
| 361 |
361 |
|
```json
|
| 362 |
362 |
|
{"x":7,"y":7,"s":0,"e":"u","vnum":2051}
|
| 363 |
363 |
|
```
|
| 364 |
364 |
|
|
| 365 |
365 |
|
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 |
366 |
|
|
| 367 |
367 |
|
#### Per-room in `zr`
|
| 368 |
368 |
|
|
| 369 |
369 |
|
```json
|
| 370 |
370 |
|
{
|
| 371 |
371 |
|
"x": 5, "y": 7, "z": -2,
|
| 372 |
372 |
|
"s": 0,
|
| 373 |
373 |
|
"e": "neud",
|
| 374 |
374 |
|
"l": 1,
|
| 375 |
375 |
|
"ar": 30,
|
| 376 |
376 |
|
"f": "s",
|
| 377 |
377 |
|
"d": {"u":"closed"},
|
| 378 |
378 |
|
"ex": {"n":3041,"e":3042,"u":3010,"d":3120}
|
| 379 |
379 |
|
}
|
| 380 |
380 |
|
```
|
| 381 |
381 |
|
|
| 382 |
382 |
|
| Field | Type | Notes |
|
| 383 |
383 |
|
|---|---|---|
|
| 384 |
384 |
|
| `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 |
385 |
|
| `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 |
386 |
|
| `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 |
387 |
|
|
| 388 |
388 |
|
#### Secret exits
|
| 389 |
389 |
|
|
| 390 |
390 |
|
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 |
391 |
|
|
| 392 |
392 |
|
**Emitted on:** every look at the current room.
|
| 393 |
393 |
|
|
| 394 |
394 |
|
**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 |
395 |
|
|
| 396 |
396 |
|
---
|
| 397 |
397 |
|
|
| 398 |
398 |
|
### `Group.Info`
|
| 399 |
399 |
|
|
| 400 |
400 |
|
Your group roster. Empty (`{}`) when solo.
|
| 401 |
401 |
|
|
| 402 |
402 |
|
**Shape (grouped):**
|
| 403 |
403 |
|
```json
|
| 404 |
404 |
|
Group.Info {
|
| 405 |
405 |
|
"leader": "Durim",
|
| 406 |
406 |
|
"members": [
|
| 407 |
|
- |
{"name":"Durim","level":50,"class":"Dkn","hp_pct":78,"mana_pct":55,"move_pct":93,"tnl":1250},
|
| 408 |
|
- |
{"name":"Gibble","level":50,"class":"Psi","hp_pct":91,"mana_pct":40,"move_pct":88,"tnl":0}
|
|
407 |
+ |
{"id":1769388810,"name":"Durim","level":50,"class":"Dkn","hp_pct":78,"mana_pct":55,"move_pct":93,"tnl":1250},
|
|
408 |
+ |
{"id":1769401002,"name":"Gibble","level":50,"class":"Psi","hp_pct":91,"mana_pct":40,"move_pct":88,"tnl":0}
|
| 409 |
409 |
|
]
|
| 410 |
410 |
|
}
|
| 411 |
411 |
|
```
|
| 412 |
412 |
|
|
| 413 |
413 |
|
**Shape (solo):**
|
| 414 |
414 |
|
```json
|
| 415 |
415 |
|
Group.Info {}
|
| 416 |
416 |
|
```
|
| 417 |
417 |
|
|
| 418 |
418 |
|
**Top-level fields:**
|
| 419 |
|
- |
- `leader` (string): leader's visible name.
|
| 420 |
|
- |
- `members` (array): every member including you. Members with `NOWHO` are omitted.
|
|
419 |
+ |
- `leader` (string): leader's name.
|
|
420 |
+ |
- `members` (array): every member including you and any charmed followers. Members with `NOWHO` are omitted.
|
| 421 |
421 |
|
|
| 422 |
422 |
|
**Per-member fields:**
|
| 423 |
423 |
|
| Field | Type | Notes |
|
| 424 |
424 |
|
|---|---|---|
|
| 425 |
|
- |
| `name` | string | as you see them |
|
|
425 |
+ |
| `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 |
+ |
| `name` | string | member's real name (PCs) or short description (charmies). Not filtered by visibility, so blindness does not turn group members into `someone`. |
|
| 426 |
427 |
|
| `level` | int | 1 to 60 |
|
| 427 |
428 |
|
| `class` | string | short class key (e.g. `Dkn`, `Psi`, `War`) for PCs, `"mob"` for charmies |
|
| 428 |
429 |
|
| `hp_pct`, `mana_pct`, `move_pct` | int | 0 to 100 |
|
| 429 |
430 |
|
| `tnl` | int | exp to next level. `0` for NPCs and capped chars. |
|
| 430 |
431 |
|
|
| 431 |
432 |
|
**Emitted on:** prompt, login, group join/leave.
|
| 432 |
433 |
|
|
| 433 |
434 |
|
---
|
| 434 |
435 |
|
|
| 435 |
436 |
|
### `World.Time`
|
| 436 |
437 |
|
|
| 437 |
438 |
|
In-game clock and weather.
|
| 438 |
439 |
|
|
| 439 |
440 |
|
**Shape:**
|
| 440 |
441 |
|
```json
|
| 441 |
442 |
|
World.Time {"hour":14,"day":12,"month":3,"year":2026,"sunlight":"light","sky":"cloudy"}
|
| 442 |
443 |
|
```
|
| 443 |
444 |
|
|
| 444 |
445 |
|
**Fields:**
|
| 445 |
446 |
|
| Field | Type | Notes |
|
| 446 |
447 |
|
|---|---|---|
|
| 447 |
448 |
|
| `hour` | int | 0-23 game-time |
|
| 448 |
449 |
|
| `day` | int | day of month |
|
| 449 |
450 |
|
| `month` | int | 0-17 (see game calendar) |
|
| 450 |
451 |
|
| `year` | int | game year |
|
| 451 |
452 |
|
| `sunlight` | string | `"dark"`, `"rise"`, `"light"`, `"set"` |
|
| 452 |
453 |
|
| `sky` | string | `"cloudless"`, `"cloudy"`, `"raining"`, `"lightning"` |
|
| 453 |
454 |
|
|
| 454 |
455 |
|
**Emitted on:** login. Also broadcast to every GMCP-enabled connection on each weather tick (roughly every two minutes real time).
|
| 455 |
456 |
|
|
| 456 |
457 |
|
---
|
| 457 |
458 |
|
|
| 458 |
459 |
|
### `World.Moons`
|
| 459 |
460 |
|
|
| 460 |
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.
|
| 461 |
462 |
|
|
| 462 |
463 |
|
**Shape:**
|
| 463 |
464 |
|
```json
|
| 464 |
465 |
|
World.Moons {
|
| 465 |
466 |
|
"moons": [
|
| 466 |
467 |
|
{"name":"Lysenties","active":true,"phase":4,"phase_name":"full"},
|
| 467 |
468 |
|
{"name":"Nercuros","active":true,"phase":2,"phase_name":"first half"},
|
| 468 |
469 |
|
{"name":"Dyphrities","active":false,"phase":0,"phase_name":"new"}
|
| 469 |
470 |
|
],
|
| 470 |
471 |
|
"eclipse": false,
|
| 471 |
472 |
|
"triad": false,
|
| 472 |
473 |
|
"near_alignment": false
|
| 473 |
474 |
|
}
|
| 474 |
475 |
|
```
|
| 475 |
476 |
|
|
| 476 |
477 |
|
**Top-level fields:**
|
| 477 |
478 |
|
| Field | Type | Notes |
|
| 478 |
479 |
|
|---|---|---|
|
| 479 |
480 |
|
| `moons` | array | one entry per moon, in fixed order Lysenties / Nercuros / Dyphrities. |
|
| 480 |
481 |
|
| `eclipse` | bool | true while a solar or lunar eclipse is active. All moon-driven scalars collapse to zero during eclipse. |
|
| 481 |
482 |
|
| `triad` | bool | true when all three moons are full simultaneously. |
|
| 482 |
483 |
|
| `near_alignment` | bool | true when phases are close enough to trigger near-alignment buffs/effects. |
|
| 483 |
484 |
|
|
| 484 |
485 |
|
**Per-moon fields:**
|
| 485 |
486 |
|
| Field | Type | Notes |
|
| 486 |
487 |
|
|---|---|---|
|
| 487 |
488 |
|
| `name` | string | display name (`"Lysenties"`, `"Nercuros"`, `"Dyphrities"`) |
|
| 488 |
489 |
|
| `active` | bool | false if the moon is currently dormant (rare; treat as inactive for any moon-derived effects). |
|
| 489 |
490 |
|
| `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. |
|
| 490 |
491 |
|
| `phase_name` | string | display string for the phase. |
|
| 491 |
492 |
|
|
| 492 |
493 |
|
**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.
|
| 493 |
494 |
|
|
| 494 |
495 |
|
---
|
| 495 |
496 |
|
|
| 496 |
497 |
|
### `Comm.Channel`
|
| 497 |
498 |
|
|
| 498 |
499 |
|
One message per chat-like event you hear.
|
| 499 |
500 |
|
|
| 500 |
501 |
|
**Base shape (all channels):**
|
| 501 |
502 |
|
```json
|
| 502 |
503 |
|
Comm.Channel {"channel":"say","speaker":"Durim","text":"Hello there."}
|
| 503 |
504 |
|
```
|
| 504 |
505 |
|
|
| 505 |
506 |
|
**Extended shape (say, yell, tell, gtell add language and direction metadata):**
|
| 506 |
507 |
|
```json
|
| 507 |
508 |
|
Comm.Channel {
|
| 508 |
509 |
|
"channel":"say",
|
| 509 |
510 |
|
"speaker":"Durim",
|
| 510 |
511 |
|
"text":"xak zee nog",
|
| 511 |
512 |
|
"language":"foreign",
|
| 512 |
513 |
|
"understood":false
|
| 513 |
514 |
|
}
|
| 514 |
515 |
|
```
|
| 515 |
516 |
|
```json
|
| 516 |
517 |
|
Comm.Channel {
|
| 517 |
518 |
|
"channel":"tell",
|
| 518 |
519 |
|
"speaker":"Gibble",
|
| 519 |
520 |
|
"text":"meet at the altar",
|
| 520 |
521 |
|
"language":"common",
|
| 521 |
522 |
|
"understood":true,
|
| 522 |
523 |
|
"direction":"received"
|
| 523 |
524 |
|
}
|
| 524 |
525 |
|
```
|
| 525 |
526 |
|
|
| 526 |
527 |
|
**Base fields (always present):**
|
| 527 |
528 |
|
| Field | Type | Notes |
|
| 528 |
529 |
|
|---|---|---|
|
| 529 |
530 |
|
| `channel` | string | see table below |
|
| 530 |
531 |
|
| `speaker` | string | visible name. For `tell`, this is the sender (you only receive a `tell` packet as the recipient). |
|
| 531 |
532 |
|
| `text` | string | color-stripped. Garbled per-listener on language-aware channels. |
|
| 532 |
533 |
|
|
| 533 |
534 |
|
**Optional fields (say, yell, tell, gtell):**
|
| 534 |
535 |
|
| Field | Type | Notes |
|
| 535 |
536 |
|
|---|---|---|
|
| 536 |
537 |
|
| `language` | string | language name (`"common"`, `"drow"`, `"orcish"`, etc.) when the listener understood. `"foreign"` when they didn't. The actual foreign language never leaks. |
|
| 537 |
538 |
|
| `understood` | bool | `true` means `text` is the original. `false` means `text` is phoneme-garbled to match the terminal's foreign-tongue rendering. |
|
| 538 |
539 |
|
| `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. |
|
| 539 |
540 |
|
|
| 540 |
541 |
|
**Channel values:**
|
| 541 |
542 |
|
|
| 542 |
543 |
|
| Value | Source | Language-aware | Direction field |
|
| 543 |
544 |
|
|---|---|---|---|
|
| 544 |
545 |
|
| `say` | `say` | yes | no |
|
| 545 |
546 |
|
| `yell` | `yell` | yes | no |
|
| 546 |
547 |
|
| `tell` | `tell`, `reply`, language-specific tells (`tarchaic`, `tmogwei`, etc.) | yes | yes (recipient side only) |
|
| 547 |
548 |
|
| `gtell` | `gtell` and group-tell projection | yes | no |
|
| 548 |
549 |
|
| `pray` | `pray` | no | no |
|
| 549 |
550 |
|
| `newbie` | `newbie` | no | no |
|
| 550 |
551 |
|
| `cabal` | `cabal chat` | no | no |
|
| 551 |
552 |
|
| `clan` | `clan chat` | no | no |
|
| 552 |
553 |
|
| `faction` | `faction chat` | no | no |
|
| 553 |
554 |
|
| `immortal` | `immtalk` (imms only) | no | no |
|
| 554 |
555 |
|
| `imp` | `imptalk` (IMP+ only) | no | no |
|
| 555 |
556 |
|
|
| 556 |
557 |
|
**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.
|
| 557 |
558 |
|
|
| 558 |
559 |
|
**Language semantics:**
|
| 559 |
560 |
|
- The speaker's own packet (for say, yell, gtell) is always `understood:true` with the actual language name.
|
| 560 |
561 |
|
- 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).
|
| 561 |
562 |
|
- 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.
|
| 562 |
563 |
|
|
| 563 |
564 |
|
**Imm/imp chat:** broadcast to everyone with the right trust whose channel toggle is on. No language filtering. No `direction` field.
|
| 564 |
565 |
|
|
| 565 |
566 |
|
**Emitted on:** each matching channel message.
|
| 566 |
567 |
|
|
| 567 |
568 |
|
**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.
|
| 568 |
569 |
|
|
| 569 |
570 |
|
---
|
| 570 |
571 |
|
|
| 571 |
572 |
|
## Client to server messages
|
| 572 |
573 |
|
|
| 573 |
574 |
|
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.
|
| 574 |
575 |
|
|
| 575 |
576 |
|
---
|
| 576 |
577 |
|
|
| 577 |
578 |
|
## Scripting recipes
|
| 578 |
579 |
|
|
| 579 |
580 |
|
### Mudlet, HP/mana bars from Char.Vitals
|
| 580 |
581 |
|
|
| 581 |
582 |
|
```lua
|
| 582 |
583 |
|
registerAnonymousEventHandler("gmcp.Char.Vitals", function()
|
| 583 |
584 |
|
local v = gmcp.Char.Vitals
|
| 584 |
585 |
|
hpBar:setValue(v.hp, v.maxhp)
|
| 585 |
586 |
|
manaBar:setValue(v.mana, v.maxmana)
|
| 586 |
587 |
|
moveBar:setValue(v.move, v.maxmove)
|
| 587 |
588 |
|
end)
|
| 588 |
589 |
|
```
|
| 589 |
590 |
|
|
| 590 |
591 |
|
### Mudlet, combat target widget
|
| 591 |
592 |
|
|
| 592 |
593 |
|
```lua
|
| 593 |
594 |
|
registerAnonymousEventHandler("gmcp.Char.Combat", function()
|
| 594 |
595 |
|
local c = gmcp.Char.Combat
|
| 595 |
596 |
|
if not c or not c.target then
|
| 596 |
597 |
|
combatHUD:hide()
|
| 597 |
598 |
|
else
|
| 598 |
599 |
|
combatHUD:show()
|
| 599 |
600 |
|
combatName:echo(c.target)
|
| 600 |
601 |
|
combatBar:setValue(c.hp_pct, 100)
|
| 601 |
602 |
|
end
|
| 602 |
603 |
|
end)
|
| 603 |
604 |
|
```
|
| 604 |
605 |
|
|
| 605 |
606 |
|
### Mudlet, songs separate from spells
|
| 606 |
607 |
|
|
| 607 |
608 |
|
```lua
|
| 608 |
609 |
|
registerAnonymousEventHandler("gmcp.Char.Affects", function()
|
| 609 |
610 |
|
local spells, songs = {}, {}
|
| 610 |
611 |
|
for _, a in ipairs(gmcp.Char.Affects.affects or {}) do
|
| 611 |
612 |
|
if a.kind == "song" then
|
| 612 |
613 |
|
table.insert(songs, a)
|
| 613 |
614 |
|
else
|
| 614 |
615 |
|
table.insert(spells, a)
|
| 615 |
616 |
|
end
|
| 616 |
617 |
|
end
|
| 617 |
618 |
|
renderSpells(spells)
|
| 618 |
619 |
|
renderSongs(songs)
|
| 619 |
620 |
|
end)
|
| 620 |
621 |
|
```
|
| 621 |
622 |
|
|
| 622 |
623 |
|
### Mudlet, minimap with zone tints
|
| 623 |
624 |
|
|
| 624 |
625 |
|
```lua
|
| 625 |
626 |
|
registerAnonymousEventHandler("gmcp.Map.Tiles", function()
|
| 626 |
627 |
|
local m = gmcp.Map.Tiles
|
| 627 |
628 |
|
local r = m.r
|
| 628 |
629 |
|
for y = 1, #m.g do
|
| 629 |
630 |
|
for x = 1, #m.g[y] do
|
| 630 |
631 |
|
local cell = m.g[y][x]
|
| 631 |
632 |
|
if cell == vim.NIL then cell = nil end -- Mudlet uses vim.NIL for JSON null
|
| 632 |
633 |
|
if cell then
|
| 633 |
634 |
|
local tint = m.areas and m.areas[tostring(cell.ar)] and m.areas[tostring(cell.ar)].color or "#444"
|
| 634 |
635 |
|
drawCell(x, y, cell.s, cell.e, tint, cell.l, cell.f, cell.h)
|
| 635 |
636 |
|
else
|
| 636 |
637 |
|
drawCell(x, y, nil)
|
| 637 |
638 |
|
end
|
| 638 |
639 |
|
end
|
| 639 |
640 |
|
end
|
| 640 |
641 |
|
-- Off-floor rooms (towers, dungeons with stacked floors)
|
| 641 |
642 |
|
for _, z in ipairs(m.zr or {}) do
|
| 642 |
643 |
|
drawZRoom(z.x, z.y, z.z, z.s, z.e, z.ar)
|
| 643 |
644 |
|
end
|
| 644 |
645 |
|
end)
|
| 645 |
646 |
|
```
|
| 646 |
647 |
|
|
| 647 |
648 |
|
### Mudlet, World.Moons phase tracker
|
| 648 |
649 |
|
|
| 649 |
650 |
|
```lua
|
| 650 |
651 |
|
registerAnonymousEventHandler("gmcp.World.Moons", function()
|
| 651 |
652 |
|
local m = gmcp.World.Moons
|
| 652 |
653 |
|
for _, moon in ipairs(m.moons) do
|
| 653 |
654 |
|
moonWidget[moon.name]:setPhase(moon.phase, moon.phase_name, moon.active)
|
| 654 |
655 |
|
end
|
| 655 |
656 |
|
eclipseIndicator:setVisible(m.eclipse)
|
| 656 |
657 |
|
triadIndicator:setVisible(m.triad)
|
| 657 |
658 |
|
end)
|
| 658 |
659 |
|
```
|
| 659 |
660 |
|
|
| 660 |
661 |
|
### tintin++, chat log split by channel
|
| 661 |
662 |
|
|
| 662 |
663 |
|
```
|
| 663 |
664 |
|
#action {Comm.Channel %*} {
|
| 664 |
665 |
|
#var CHAN %1[channel];
|
| 665 |
666 |
|
#var WHO %1[speaker];
|
| 666 |
667 |
|
#var MSG %1[text];
|
| 667 |
668 |
|
#line log chat-$CHAN.txt <$WHO>: $MSG
|
| 668 |
669 |
|
}
|
| 669 |
670 |
|
```
|
| 670 |
671 |
|
|
| 671 |
672 |
|
### Generic, room-change trigger
|
| 672 |
673 |
|
|
| 673 |
674 |
|
`Room.Info` fires once per look. Stash `room.num` to detect movement:
|
| 674 |
675 |
|
|
| 675 |
676 |
|
```
|
| 676 |
677 |
|
prevRoom = nil
|
| 677 |
678 |
|
onRoomInfo = function(info)
|
| 678 |
679 |
|
if info.num ~= prevRoom then
|
| 679 |
680 |
|
prevRoom = info.num
|
| 680 |
681 |
|
onEnterRoom(info)
|
| 681 |
682 |
|
end
|
| 682 |
683 |
|
end
|
| 683 |
684 |
|
```
|
| 684 |
685 |
|
|
| 685 |
686 |
|
---
|
| 686 |
687 |
|
|
| 687 |
688 |
|
## Caveats and gotchas
|
| 688 |
689 |
|
|
| 689 |
690 |
|
### Ordering
|
| 690 |
691 |
|
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.
|
| 691 |
692 |
|
|
| 692 |
693 |
|
### Stale packets
|
| 693 |
694 |
|
- `Char.Combat {}` is the explicit "combat ended" signal. Don't wait for a timeout.
|
| 694 |
695 |
|
- `Group.Info {}` is the explicit "solo" signal.
|
| 695 |
696 |
|
- `Char.Affects` always sends the full list. Replace, don't merge.
|
| 696 |
697 |
|
|
| 697 |
698 |
|
### Map timing
|
| 698 |
699 |
|
`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.
|
| 699 |
700 |
|
|
| 700 |
701 |
|
### Size limits
|
| 701 |
702 |
|
- `Char.Affects`: up to 8KB. Truncates silently on overflow.
|
| 702 |
703 |
|
- `Map.Tiles`: up to 96KB.
|
| 703 |
704 |
|
- `Group.Info`, `Room.Chars`, `Room.Items`: up to 8KB.
|
| 704 |
705 |
|
- `Comm.Channel` text field: up to about 1KB after JSON-escape.
|
| 705 |
706 |
|
|
| 706 |
707 |
|
### Invisibility
|
| 707 |
|
- |
`Room.Chars` and `Group.Info` apply the normal `can_see()` check. Your client only sees what your character sees.
|
|
708 |
+ |
`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`.
|
| 708 |
709 |
|
|
| 709 |
710 |
|
### Color stripping
|
| 710 |
711 |
|
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.
|
| 711 |
712 |
|
|
| 712 |
713 |
|
### Secret exits
|
| 713 |
714 |
|
Non-imms: secret passages and closed-and-secret doors are absent from `Map.Tiles`. Imms see them with `d` value `"hidden"`.
|
| 714 |
715 |
|
|
| 715 |
716 |
|
### Reconnection
|
| 716 |
717 |
|
After copyover/hotboot the server re-emits the login package set. Don't assume any package's content survives across disconnects.
|
| 717 |
718 |
|
|
| 718 |
719 |
|
### When GMCP goes silent
|
| 719 |
720 |
|
If you stop receiving packets:
|
| 720 |
721 |
|
1. Confirm `IAC DO 201` was sent.
|
| 721 |
722 |
|
2. Confirm your terminal isn't stripping IAC sequences (some unix `tail` setups do).
|
| 722 |
723 |
|
3. `Core.Hello` isn't required, but sending it keeps server-side fingerprint and telemetry useful.
|
| 723 |
724 |
|
|
| 724 |
725 |
|
---
|
| 725 |
726 |
|
|
| 726 |
727 |
|
## Source of truth
|
| 727 |
728 |
|
|
| 728 |
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.
|
| 729 |
730 |
|
|