| 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. |