dredless 0.6.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +22 -57
  2. package/dist/browser/dredless.global.js +827 -1058
  3. package/dist/browser/dredless.global.js.map +4 -4
  4. package/docs/ACCOUNT.md +146 -0
  5. package/docs/API_REFERENCE.md +63 -2550
  6. package/docs/BROWSER_API.md +151 -70
  7. package/docs/CLIENT.md +183 -0
  8. package/docs/EVENTS.md +136 -0
  9. package/docs/GETTING_STARTED.md +135 -0
  10. package/docs/MACHINES.md +351 -0
  11. package/docs/MANAGEMENT.md +285 -0
  12. package/docs/NODE_API.md +120 -318
  13. package/docs/PLAYER_INVENTORY.md +235 -0
  14. package/docs/PROTOCOL.md +227 -0
  15. package/docs/README.md +44 -0
  16. package/docs/TROUBLESHOOTING.md +91 -0
  17. package/docs/WORLD_STATE.md +183 -0
  18. package/package.json +1 -2
  19. package/src/browser.d.ts +31 -1
  20. package/src/browser.js +10 -0
  21. package/src/client/core.js +302 -15
  22. package/src/client/domains/index.js +57 -7
  23. package/src/client/index.d.ts +1 -1
  24. package/src/index.d.ts +303 -14
  25. package/src/index.js +10 -0
  26. package/src/network/account.js +182 -0
  27. package/src/network/disconnect.d.ts +2 -0
  28. package/src/network/disconnect.js +53 -0
  29. package/src/network/index.d.ts +1 -0
  30. package/src/network/index.js +1 -0
  31. package/src/network/invites.d.ts +0 -3
  32. package/src/network/invites.js +3 -40
  33. package/src/network/session.d.ts +2 -0
  34. package/src/network/session.js +21 -0
  35. package/src/protocol/outbound/ship-management.d.ts +66 -1
  36. package/src/protocol/outbound/ship-management.js +83 -0
  37. package/src/state/model/decoder.js +101 -56
  38. package/src/state/model/index.d.ts +4 -0
  39. package/src/state/model/loader-config.js +0 -997
  40. package/src/state/model/state.js +28 -80
  41. package/src/state/world/index.js +27 -1
@@ -1,100 +1,181 @@
1
- # Dredless Browser API
1
+ # Browser setup
2
2
 
3
- This document covers the browser-specific top-level API: ambient browser sessions, browser cookie handling, and browser start/join helpers.
3
+ [Documentation index](README.md) · [Getting started](GETTING_STARTED.md) · [API reference](API_REFERENCE.md)
4
4
 
5
- Use this entrypoint in browser contexts:
5
+ ## Contents
6
+
7
+ - [Execution context](#execution-context)
8
+ - [Bundled imports](#bundled-imports)
9
+ - [Global script](#global-script)
10
+ - [Session methods](#session-methods)
11
+ - [Connection methods](#connection-methods)
12
+ - [Attach to an existing socket](#attach-to-an-existing-socket)
13
+
14
+ ## Execution context
15
+
16
+ Browser sessions use the browser's ambient cookie jar with `credentials: "include"`. They do not isolate identities like
17
+ Node sessions. Changing an anonymous key affects the ambient session.
18
+
19
+ Use a Drednot page integration or a content-script context with access to the site's cookies and origin. Background
20
+ pages, popups, workers, and unrelated origins may lack that access. Importing the package does not bypass browser
21
+ cookie, origin, or CORS restrictions.
22
+
23
+ Browsers manage `Cookie` and `Set-Cookie` themselves. JavaScript cannot install a separate cookie jar or custom
24
+ authentication headers on a browser WebSocket.
25
+
26
+ ## Bundled imports
6
27
 
7
28
  ```js
8
- import Dredless, { BrowserSession } from "dredless/browser";
29
+ import Dredless, {BrowserSession, DredlessClient} from "dredless/browser";
9
30
  ```
10
31
 
11
- Normal browser usage:
32
+ Prefer this explicit entrypoint for browser types. Shared machine constants and protocol tools are named exports; the
33
+ default export is the runtime namespace, not the entire module.
34
+
35
+ **Fragment:** use the site's existing login without creating a new anonymous identity:
12
36
 
13
37
  ```js
14
- const session = await Dredless.createAnonSession();
38
+ const session = await Dredless.createSession();
15
39
  const servers = await Dredless.fetchServers();
16
- const ships = await Dredless.fetchShips(servers[0]);
17
- const client = await Dredless.startShip(servers[0], ships[0]);
40
+ const server = servers[0];
41
+ if (!server) throw new Error("No servers found");
42
+ console.log(await session.fetchShips(server));
18
43
  ```
19
44
 
20
- The browser API uses the current browser cookie jar with `credentials: "include"`. Browser JavaScript cannot read `Set-Cookie`, cannot set the `Cookie` header, and cannot set custom WebSocket headers, so browser authentication is ambient rather than multi-session.
45
+ ## Global script
21
46
 
22
- Shared input shapes such as `Server`, `ServerRef`, `Ship`, `ShipRef`, and `ShipSpec` are documented in [API_REFERENCE.md](./API_REFERENCE.md#common-input-types). Shared client/world/entity/domain APIs are also documented in [API_REFERENCE.md](./API_REFERENCE.md).
47
+ Run this only in an appropriate browser context. Use a bundle version that matches the API you intend to call; an
48
+ unversioned CDN URL follows published releases rather than this repository's current checkout.
23
49
 
24
- ## `DredlessBrowser` Namespace
50
+ ```html
25
51
 
26
- `dredless/browser` exports a browser-first namespace. It assumes the current browser cookie jar and does not expose `Session` or `AnonSession`.
52
+ <script src="https://cdn.jsdelivr.net/npm/dredless/dist/browser/dredless.global.js"></script>
53
+ <script>
54
+ (async () => {
55
+ const session = await Dredless.createSession();
56
+ console.log((await session.fetchAccountStatus()).account);
57
+ })().catch(console.error);
58
+ </script>
59
+ ```
60
+
61
+ The global is `Dredless`, containing the browser runtime namespace. It does not expose every named ESM export. Use
62
+ bundled imports when you need additional named helpers.
63
+
64
+ ## Session methods
27
65
 
28
66
  ```ts
29
- interface DredlessBrowserNamespace {
30
- BrowserSession: typeof BrowserSession;
31
- Connection: typeof Connection;
32
- DredlessClient: typeof DredlessClient;
33
- browserSession: typeof browserSession;
34
- createBrowserSession: typeof createBrowserSession;
35
- createSession: typeof createSession;
36
- createAnonSession: typeof createAnonSession;
37
- createAnonToken: typeof createAnonToken;
38
- fetchAnonKey: typeof fetchAnonKey;
39
- setBrowserSession: typeof setBrowserSession;
40
- fetchAccountStatus: typeof fetchAccountStatus;
41
- fetchNoticeVersion: typeof fetchNoticeVersion;
42
- fetchGameVersion: typeof fetchGameVersion;
43
- fetchServers: typeof fetchServers;
44
- fetchServerStatus: typeof fetchServerStatus;
45
- fetchServerStatuses: typeof fetchServerStatuses;
46
- fetchShips: typeof fetchShips;
47
- fetchShipList: typeof fetchShipList;
48
- joinShip: typeof joinShip;
49
- startShip: typeof startShip;
50
- startNewShip: typeof startNewShip;
51
- joinInvite: typeof joinInvite;
52
- }
67
+ new BrowserSession(baseUrl ? : string)
68
+ :
69
+ BrowserSession;
70
+ Dredless.browserSession(baseUrl ? : string)
71
+ :
72
+ BrowserSession;
73
+ Dredless.createBrowserSession(baseUrl ? : string)
74
+ :
75
+ BrowserSession;
76
+ Dredless.setBrowserSession(session ? : BrowserSession | null)
77
+ :
78
+ BrowserSession | null;
79
+ Dredless.createSession(noticeVersion ? : number | null, baseUrl ? : string)
80
+ :
81
+ Promise<BrowserSession>;
82
+ Dredless.createAnonSession(anonKey ? : string | null, noticeVersion ? : number | null,
83
+ baseUrl ? : string)
84
+ :
85
+ Promise<BrowserSession>;
86
+ Dredless.fetchAnonKey(noticeVersion ? : number | null, baseUrl ? : string)
87
+ :
88
+ Promise<string>;
89
+ Dredless.createAnonToken(noticeVersion ? : number | null, baseUrl ? : string)
90
+ :
91
+ Promise<string>;
92
+
93
+ session.fetchAccountStatus()
94
+ :
95
+ Promise<unknown>;
96
+ session.readAnonKey()
97
+ :
98
+ Promise<string>;
99
+ session.fetchAnonKey(noticeVersion ? : number | null)
100
+ :
101
+ Promise<string>;
53
102
  ```
54
103
 
55
- `createSession(noticeVersion?, baseUrl?)` initializes the ambient browser session, fetches account status, and returns the active `BrowserSession`.
104
+ `createSession()` refreshes status on the ambient session. `createAnonSession()` applies a supplied key or posts an
105
+ anonymous login, then refreshes status. `fetchAnonKey()`/`createAnonToken()` request a new key. Key retrieval uses
106
+ `cookieStore` or `document.cookie`; it rejects when the key is not readable even if the login request succeeded.
56
107
 
57
- `createAnonSession(anonKey?, noticeVersion?, baseUrl?)` creates or applies an anonymous browser session. If `anonKey` is omitted, it posts to `/account/login/anon`, then reads `anon_key` through `cookieStore` or `document.cookie`. If the key cannot be read, it throws. If `anonKey` is provided, it is written to the ambient cookie jar when possible. The return value is still the active `BrowserSession`, not an isolated cookie jar.
108
+ `BrowserSession` inherits [account methods](ACCOUNT.md), HTTP `request()`, ship reads, and client factories from the
109
+ session API. It uses browser-owned cookies and does not expose a registered-account login flow. Its JavaScript-readable
110
+ `gameSession`/`gameToken` properties may be empty even when browser authentication works.
58
111
 
59
- `fetchAnonKey(noticeVersion?, baseUrl?)` / `createAnonToken(noticeVersion?, baseUrl?)` posts to `/account/login/anon` and returns the JS-readable `anon_key`. It throws if the login succeeds but the key is not readable through `cookieStore` or `document.cookie`.
112
+ ## Connection methods
60
113
 
61
- Browser support is tested in page and content-script contexts on `drednot.io`. Background pages, popups, workers, and other non-page contexts are unlikely to expose `drednot.io` cookies to this API.
114
+ Session methods have the same arguments as [Node session methods](NODE_API.md#join-start-and-invites). Top-level browser
115
+ helpers take the ambient session as an optional final argument:
62
116
 
63
- `fetchShips(server)` and `fetchShipList(server)` use the default ambient `BrowserSession`. Pass an explicit `BrowserSession` as the last argument only when targeting a different `baseUrl`.
117
+ ```ts
118
+ Dredless.fetchShips(server
119
+ :
120
+ ServerRef, session ? : BrowserSession | null
121
+ ):
122
+ Promise<Ship[]>;
123
+ Dredless.fetchShipList(server
124
+ :
125
+ ServerRef, session ? : BrowserSession | null
126
+ ):
127
+ Promise<ShipList>;
128
+ Dredless.fetchAccountStatus(session ? : BrowserSession | null)
129
+ :
130
+ Promise<unknown>;
131
+ Dredless.joinShip(server
132
+ :
133
+ ServerRef, ship ? : ShipRef, session ? : BrowserSession | null
134
+ ):
135
+ Promise<DredlessClient>;
136
+ Dredless.startShip(server
137
+ :
138
+ ServerRef, ship ? : ShipRef, session ? : BrowserSession | null
139
+ ):
140
+ Promise<DredlessClient>;
141
+ Dredless.startNewShip(server
142
+ :
143
+ ServerRef, name ? : string, color ? : string,
144
+ session ? : BrowserSession | null
145
+ ):
146
+ Promise<DredlessClient>;
147
+ Dredless.joinInvite(server
148
+ :
149
+ ServerRef, code
150
+ :
151
+ string, session ? : BrowserSession | null
152
+ ):
153
+ Promise<DredlessClient>;
154
+ ```
64
155
 
65
- `joinShip`, `startShip`, `startNewShip`, and `joinInvite` return ready `DredlessClient` instances just like the Node helpers.
156
+ The HTTP join/start distinctions and readiness conditions are the same as Node. `fetchServers`, `fetchServerStatus`,
157
+ `fetchServerStatuses`, `fetchGameVersion`, and `fetchNoticeVersion` are also on the browser namespace. The browser
158
+ namespace does not contain the Node `Session`/`AnonSession` constructors.
66
159
 
67
- ## `BrowserSession`
160
+ ## Attach to an existing socket
68
161
 
69
- `BrowserSession` is the browser counterpart to `Session`, but it is ambient-cookie based. It cannot read `Set-Cookie` and cannot set websocket headers, so authentication must be accepted by the browser's cookie jar.
162
+ **Fragment:** `socket` is a WebSocket you already obtained from your integration. Attach early: Dredless cannot
163
+ reconstruct frames the socket received before attachment.
70
164
 
71
- ```ts
72
- class BrowserSession {
73
- constructor(baseUrl?: string);
74
- baseUrl: string;
75
- ambientAuth: true;
76
-
77
- readAnonKey(): Promise<string>;
78
- fetchAnonKey(noticeVersion?: number | null): Promise<string>;
79
- fetchAccountStatus(): Promise<unknown>;
80
- fetchShips(server: ServerRef): Promise<Ship[]>;
81
- fetchShipList(server: ServerRef): Promise<ShipList>;
82
-
83
- joinShipConnection(server: ServerRef, ship?: ShipRef): Promise<Connection>;
84
- startShipConnection(server: ServerRef, ship?: ShipRef): Promise<Connection>;
85
- startNewShipConnection(server: ServerRef, name?: string, color?: string): Promise<Connection>;
86
- joinInviteConnection(server: ServerRef, code: string): Promise<Connection>;
87
-
88
- joinShip(server: ServerRef, ship?: ShipRef): Promise<DredlessClient>;
89
- startShip(server: ServerRef, ship?: ShipRef): Promise<DredlessClient>;
90
- startNewShip(server: ServerRef, name?: string, color?: string): Promise<DredlessClient>;
91
- joinInvite(server: ServerRef, code: string): Promise<DredlessClient>;
92
- }
165
+ ```js
166
+ const client = DredlessClient.attachWebSocket(socket, {mode: "readonly"});
167
+ client.on("model", ({world}) => console.log(world.id, world.model.players()));
93
168
  ```
94
169
 
170
+ | Mode | Writes | Automatically sends hello/bootstrap/keepalive | `close()` |
171
+ |-----------------------|----------|-----------------------------------------------|-------------------------------|
172
+ | `"observe"` (default) | Allowed | No | Can close the attached socket |
173
+ | `"bootstrap"` | Allowed | Yes | Can close the attached socket |
174
+ | `"readonly"` | Rejected | No | No-op |
95
175
 
96
- ## Browser Connections
97
-
98
- Browser start/join helpers still produce `Connection` objects internally, but a browser `Connection` may have an empty `gameToken` when the token is only available through browser-managed cookies. `DredlessClient` uses the browser `WebSocket` implementation and relies on browser-managed cookies and origin headers.
176
+ Use `readonly` for observation. `observe` permits explicit writes despite its name. Avoid two clients independently
177
+ sending initialization or input to one socket.
99
178
 
100
- The shared `DredlessClient` API is documented in [API_REFERENCE.md](./API_REFERENCE.md#dredlessclient).
179
+ For an already-initialized socket, `ready: true` and `sid` can supply known protocol readiness metadata. They do not
180
+ recover missing world state. See [attachment options](CLIENT.md#attach-an-existing-websocket)
181
+ and [event timing](EVENTS.md#ordering-and-listeners).
package/docs/CLIENT.md ADDED
@@ -0,0 +1,183 @@
1
+ # Client lifecycle and networking
2
+
3
+ [Documentation index](README.md) · [Events](EVENTS.md) · [API reference](API_REFERENCE.md)
4
+
5
+ ## Contents
6
+
7
+ - [Construction and retention](#construction-and-retention)
8
+ - [Completion and readiness](#completion-and-readiness)
9
+ - [Attach an existing WebSocket](#attach-an-existing-websocket)
10
+ - [Disconnects](#disconnects)
11
+ - [Send commands and messages](#send-commands-and-messages)
12
+ - [Inspect client state](#inspect-client-state)
13
+
14
+ ## Construction and retention
15
+
16
+ Normally obtain a client from a [session factory](NODE_API.md#join-start-and-invites). Construct one yourself when you
17
+ need listeners installed before the connection becomes ready:
18
+
19
+ ```ts
20
+ new DredlessClient(connection: Connection, options?: DredlessClientOptions): DredlessClient;
21
+
22
+ interface DredlessClientOptions {
23
+ connect?: boolean; // true
24
+ packetHistory?: number; // 200 packets
25
+ eventHistory?: number; // 200 events per WorldState
26
+ modelPacketHistory?: number; // 100 packets per WorldState
27
+ chunkHistory?: number; // 64 chunks per WorldState
28
+ }
29
+
30
+ client.whenReady(): Promise<DredlessClient>;
31
+ client.close(code?: number, reason?: string): DredlessClient;
32
+ client.disconnect(code?: number, reason?: string): DredlessClient;
33
+ ```
34
+
35
+ The constructor connects immediately unless `connect: false`. That option supports offline inspection; it does not
36
+ provide a later public `connect()` method. History limits bound retained debugging buffers, not the live decoded world.
37
+ `0` disables a buffer; `Infinity` makes it unbounded. Negative/non-finite values fall back to defaults, except the
38
+ explicit `Infinity` case.
39
+
40
+ `close()` defaults to code `1000` and reason `"client"`; `disconnect()` is an alias. Readonly attachments make closing a
41
+ no-op. For writable clients, closure state changes when the socket close handler runs.
42
+
43
+ ## Completion and readiness
44
+
45
+ `whenReady()` resolves when the server's ready packet arrives. Session client factories await it before returning. A
46
+ close before readiness rejects with `DisconnectError`; connection/decoding setup failures can reject with other errors.
47
+
48
+ Readiness is not a guarantee that all entities, model components, inventory, or interaction panels are populated.
49
+ `currentShip()` and individual fields can be missing. Read existing caches and subscribe to
50
+ relevant [events](EVENTS.md). Install listeners using the connection-descriptor pattern if you need the earliest
51
+ packets.
52
+
53
+ Most synchronous actions return the client and send or queue a command. They do not verify its effect. Commands
54
+ generally queue until connected, ready, and carrying a session ID; decoded messages queue until connected and, by
55
+ default, ready. Writes after a disconnect reject rather than being queued for reconnection. Promise-based
56
+ management/menu methods have their own readiness checks, timeout, and overlap rules.
57
+
58
+ | Example | Completion meaning |
59
+ |-----------------------------|----------------------------------------|
60
+ | `loader.configure(config)` | Configuration sent/queued |
61
+ | `tips.choose(entity, text)` | Menu received and selection sent |
62
+ | `door.setRank("crew")` | Requested rank observed in model state |
63
+ | `management.rename(name)` | Rename allowed and commit sent |
64
+ | `management.resetInvite()` | A changed nonempty invite key observed |
65
+ | `management.save()` | Server closed with `DropReason.Saved` |
66
+
67
+ A timeout ends local waiting; it does not cancel an action already processed by the server. Do not automatically repeat
68
+ a mutation solely because its confirmation timed out.
69
+
70
+ ## Attach an existing WebSocket
71
+
72
+ ```ts
73
+ DredlessClient.attachWebSocket(socket: unknown, options?: AttachWebSocketOptions): DredlessClient;
74
+
75
+ interface AttachWebSocketOptions {
76
+ mode?: "observe" | "bootstrap" | "readonly";
77
+ connected?: boolean | null;
78
+ ready?: boolean;
79
+ sid?: number | null;
80
+ baseUrl?: string | null;
81
+ session?: Session | null;
82
+ serverId?: number | null;
83
+ server?: Server | null;
84
+ netPort?: number | null;
85
+ gameToken?: string;
86
+ }
87
+ ```
88
+
89
+ Modes and browser ownership concerns are covered in [browser attachment](BROWSER_API.md#attach-to-an-existing-socket).
90
+ Metadata does not reconstruct frames received before attachment. `ready: true`/`sid` should describe an
91
+ already-initialized protocol session, not invent a new one.
92
+
93
+ ## Disconnects
94
+
95
+ Listen to `"disconnect"` for normalized information:
96
+
97
+ ```ts
98
+ interface DisconnectInfo {
99
+ readonly code: number;
100
+ readonly name: keyof typeof DropReason | null;
101
+ readonly reason: string;
102
+ readonly wasClean: boolean | null;
103
+ }
104
+ ```
105
+
106
+ **Fragment:**
107
+
108
+ ```js
109
+ client.on("disconnect", (info) => {
110
+ console.log(info.name, info.code, info.reason);
111
+ });
112
+ ```
113
+
114
+ The close handler first marks the client disconnected/not ready, clears `sid`, pending writes, keepalive,
115
+ management/panel state, and command acknowledgments. It sets `client.lastDisconnect` and `client.net.lastDisconnect`,
116
+ then emits `"disconnect"` and the original `"close"` arguments. World data and historical buffers remain available as
117
+ last-known state, not evidence of a live connection.
118
+
119
+ There is no automatic reconnect. Create/join a new client and reinstall subscriptions when your application decides to
120
+ reconnect.
121
+
122
+ `DisconnectError` carries the normalized record in `.disconnect`. `normalizeDisconnect()` accepts a browser close event
123
+ or Node-style code/reason. Missing codes become `1006`; unknown/standard codes have `name: null`. `wasClean` is `null`
124
+ when the socket API does not supply it.
125
+
126
+ | Code | `DropReason` name | Code | `DropReason` name |
127
+ |------|-------------------|------|-------------------|
128
+ | 4000 | ProtocolError | 4010 | Idle |
129
+ | 4001 | BadVersion | 4011 | ShipDestruct |
130
+ | 4002 | ConnectLimit | 4012 | JoinBlocked |
131
+ | 4003 | ConnectRate | 4013 | HardBanned |
132
+ | 4004 | TeamLimit | 4014 | NoLoad |
133
+ | 4005 | FullPlayers | 4015 | Saved |
134
+ | 4006 | FullTeams | 4016 | InvalidInvite |
135
+ | 4007 | TeamNotAvailable | 4017 | Shutdown |
136
+ | 4008 | Kicked | 4018 | ServerError |
137
+ | 4009 | Banned | 4019 | GTFO |
138
+
139
+ These exports are available from Node, browser, and `dredless/network`.
140
+
141
+ ## Send commands and messages
142
+
143
+ Prefer player, machine, and management methods for normal actions. `client.net` is the lower-level transport domain:
144
+
145
+ ```ts
146
+ client.net.send(command?: Command): DredlessClient;
147
+ client.net.sendMessage(message: unknown, options?: {afterReady?: boolean}): DredlessClient;
148
+ client.net.sendRaw(message: unknown, options?: {afterReady?: boolean}): DredlessClient;
149
+ client.net.sendEntityCommand(cmd: string, args?: unknown[]): DredlessClient;
150
+ client.net.sendUiConfig(data: unknown): DredlessClient;
151
+ client.net.sendBlueprintPlacement(placement: BlueprintPlacement): DredlessClient;
152
+ client.net.setOutfit(outfit: unknown): DredlessClient;
153
+ ```
154
+
155
+ `send()` applies input settings, fills command defaults, and signs the command. It is for player-input packets.
156
+ `sendMessage()` MessagePack-encodes a decoded protocol object. `afterReady` defaults to `true`.
157
+
158
+ Despite its name, `sendRaw()` is an alias of `sendMessage()`: it does not transmit pre-encoded bytes unchanged. Do not
159
+ pass the output of `encodeMsgpack()` to it expecting raw transport. UI config byte arrays are payloads passed to
160
+ `sendUiConfig()`, not whole WebSocket frames.
161
+
162
+ `net` also exposes `connected`, `ready`, `sid`, `packetCount`, `lastPacket`, and `lastDisconnect`. Direct client
163
+ transport methods exist too; using the domain makes intent clearer.
164
+
165
+ ## Inspect client state
166
+
167
+ ```ts
168
+ client.state(options?: {includeTiles?: boolean; includeModel?: boolean}): ClientSnapshot;
169
+ client.snapshot(options?: {includeTiles?: boolean; includeModel?: boolean}): ClientSnapshot;
170
+ ```
171
+
172
+ The implementation exposes `snapshot()` as well as `state()`. The result groups connection metadata, worlds, inventory,
173
+ panels, management caches (including `crewLog`), recent packet/event buffers, acknowledgments, and decode errors. Full
174
+ fields live in [ClientSnapshot](../src/index.d.ts).
175
+
176
+ Client/world snapshots are not uniformly cloned or frozen. For example, `client.state().inventory` and management
177
+ objects are shared references; don't mutate them. [World state](WORLD_STATE.md#live-reads-and-snapshots) distinguishes
178
+ frozen entity snapshots from aggregate state. Snapshots may contain session credentials through `session`/`connection`;
179
+ select fields before logging.
180
+
181
+ `includeModel` includes raw model tables, and `includeTiles` includes tile values. For blocks, use a world snapshot with
182
+ `includeBlocks: true`; the client snapshot implementation currently does not forward `includeBlocks`, even though its
183
+ declaration lists the option.
package/docs/EVENTS.md ADDED
@@ -0,0 +1,136 @@
1
+ # Event catalog
2
+
3
+ [Documentation index](README.md) · [Client lifecycle](CLIENT.md) · [API reference](API_REFERENCE.md)
4
+
5
+ ## Contents
6
+
7
+ - [Ordering and listeners](#ordering-and-listeners)
8
+ - [Lifecycle and transport](#lifecycle-and-transport)
9
+ - [World updates](#world-updates)
10
+ - [Player and interaction updates](#player-and-interaction-updates)
11
+ - [Management updates](#management-updates)
12
+ - [Errors](#errors)
13
+
14
+ ## Ordering and listeners
15
+
16
+ ```ts
17
+ client.on(type: string, callback: (...args: any[]) => void): DredlessClient;
18
+ client.off(type: string, callback: (...args: any[]) => void): DredlessClient;
19
+ client.once(type: string, callback: (...args: any[]) => void): DredlessClient;
20
+ ```
21
+
22
+ Callbacks run synchronously. Returned promises are not awaited, so handle errors inside async listeners. Keep frequent
23
+ model/packet listeners short. Ordinary listener exceptions are forwarded to an `"error"` listener when one exists;
24
+ otherwise they can interrupt dispatch. Errors encountered during packet processing can be recorded as decode errors.
25
+
26
+ Events are not replayed when you subscribe. Factory methods wait for readiness, so initial world, inventory, or
27
+ management events may have happened before you receive the client. Subscribe and also read the corresponding cache. For
28
+ earliest-event capture, construct a client from a [Connection descriptor](NODE_API.md#construct-a-client-yourself).
29
+
30
+ **Fragment:** keep a callback reference to remove an `on()` listener:
31
+
32
+ ```js
33
+ const report = () => console.log(client.management.crewLog()?.entries.length ?? 0);
34
+ client.on("crew-log", report);
35
+ report();
36
+ // Later:
37
+ client.off("crew-log", report);
38
+ ```
39
+
40
+ `once()` removes its wrapper before invoking it. `off()` expects the registered function identity; it does not cancel a
41
+ `once()` wrapper using the original callback. A manually emitted event does not update state or simulate an authentic
42
+ server response.
43
+
44
+ ## Lifecycle and transport
45
+
46
+ | Event | Callback arguments | Timing/meaning |
47
+ |--------------|----------------------------------------------------|---------------------------------------------------------------------------------|
48
+ | `attach` | `(client)` | Attached socket initialized; can fire during `attachWebSocket()` |
49
+ | `open` | `(client)` | Socket marked connected; can fire during attachment |
50
+ | `ready` | `(client)` | Ready packet processed, `sid`/world set, queued writes flushed |
51
+ | `disconnect` | `(info: DisconnectInfo)` | Close cleanup completed; see [disconnects](CLIENT.md#disconnects) |
52
+ | `close` | Browser `(event, reason)` or Node `(code, reason)` | Raw close arguments, after `disconnect` |
53
+ | `packet` | `(packet)` | Decoded inbound packet retained, before it is applied to world/management state |
54
+ | `command` | `(command)` | Signed input sent; no server acknowledgment implied |
55
+ | `message` | `(message)` | Decoded outgoing message encoded/sent; no server acknowledgment implied |
56
+ | `bootstrap` | `(message)` | Initial outfit/bootstrap message sent by an owning client |
57
+ | `event` | `(packet)` | Inbound packet not handled by a more specific branch |
58
+
59
+ WebSocket errors are distinct from a later close. A `ready` event does not mean every state component is present. Raw
60
+ `packet` callbacks run before state application; read updated state from a specific update event instead.
61
+
62
+ ## World updates
63
+
64
+ These events receive an update object with `.world` referencing the already-updated `WorldState`:
65
+
66
+ | Event | Callback arguments | Important fields |
67
+ |-----------------|--------------------|-------------------------------------------------------------------------------|
68
+ | `world` | `(update)` | `update.world` with metadata applied |
69
+ | `tiles` | `(update)` | `world`, `decoded`, `updates`, `errors` |
70
+ | `model` | `(update)` | `world`, `result` with model changes, side events, command number, timing |
71
+ | `comms-bubble` | `(update)` | `world`, `packet`, `bubble` |
72
+ | `world-removed` | See below | World removed from store |
73
+ | `ack` | `(ack)` | `{world, commandNumber}`; latest advancing nonnegative command acknowledgment |
74
+
75
+ The current implementation emits `world-removed` twice: first with the world update object, then with its raw packet.
76
+ Handle that duplication if subscribing directly. It does not imply two removals.
77
+
78
+ `model` fires after world/model application but before its side events are dispatched to inventory, panels, and other
79
+ caches. The corresponding acknowledgment is processed afterward too. Do not assume an inventory event embedded in the
80
+ same packet has already run inside a `model` listener.
81
+
82
+ ## Player and interaction updates
83
+
84
+ | Event | Callback arguments | Cache/state |
85
+ |------------------|--------------------------------------|----------------------------------------------------------|
86
+ | `inventory` | `(inventory: InventoryState, world)` | `client.inventory.state()` updated |
87
+ | `tip-list` | `(menu: TipList, world)` | Frozen menu; empty options also emitted |
88
+ | `pui` | `(panel, world)` | `client.puiPanels`, keyed by entity |
89
+ | `comms` | `(panel, world)` | `client.commsPanels` and active panel |
90
+ | `side-event` | `(event, world)` | Remaining model side events; also receives raw tip lists |
91
+ | `tip_warn` | `(event, world)` | Warning retained in `client.warnings` |
92
+ | `sfx` | `(event, world)` | Effect retained in `client.effects` |
93
+ | `chat` | `(packet)` | Raw chat packet retained in `client.chat` |
94
+ | `motd` | `(packet)` | Raw MOTD packet retained in `client.motd` |
95
+ | `outfit` | `(packet)` | Outfit cached by protocol session ID |
96
+ | `cpu` | `(packet)` | `client.cpuLoad` set from packet |
97
+ | `scanner-result` | `(result, packet)` | `lastScannerResult` and history updated |
98
+
99
+ `tip-list` does not carry an authoritative target ID from the server. Its `.entity` is inferred from a pending menu
100
+ request, or `null` for unsolicited menus. [Context menus](MACHINES.md#context-menus-and-access) explains concurrency and
101
+ targeting.
102
+
103
+ `CommsEvent.messages` contains `{raw, text}` entries; `text` flattens supported rich-text content.
104
+ See [declarations](../src/index.d.ts) for full `InventoryState`, `TipList`, `PuiEvent`, `CommsEvent`, and
105
+ `ScannerResult` shapes.
106
+
107
+ ## Management updates
108
+
109
+ | Event | Callback arguments | Meaning |
110
+ |-------------------------|-----------------------------------|-------------------------------------------------------------|
111
+ | `ship-config` | `(config: ShipConfig | null, packet)` | Config cache updated or cleared |
112
+ | `captain-subrank` | `(rank: CaptainSubrank | null, packet)` | Captain/cheat cache updated or cleared |
113
+ | `player-list` | `(list: ShipPlayerList | null, packet)` | Player-list changes merged, or cache cleared |
114
+ | `crew-log` | `(log: CrewLog | null, packet)` | Reset/amend/append applied, or log cleared |
115
+ | `demote` | `(packet)` | Permission-scoped caches cleared before this event |
116
+ | `starter-recovery` | `({success, failReason}, packet)` | Recovery response; `failReason` is string or `null` |
117
+ | `patron-perks-response` | `({failure}, packet)` | Patron operation response; applied patrons come from config |
118
+ | `session` | `(packet)` | Raw captain/session packet, after dedicated handling |
119
+
120
+ Demotion clears ship config, captain subrank, player list, and crew log. It emits `demote`, then the four cache events
121
+ with `null`, then the raw `session` event. Disconnect also clears these caches, but uses the disconnect events rather
122
+ than emitting each cache event.
123
+
124
+ Rename permission replies remain on `session`; the rename promise handles them internally. It does not expose a
125
+ dedicated commit-success event. Detailed operation contracts are in [management](MANAGEMENT.md).
126
+
127
+ ## Errors
128
+
129
+ | Event | Callback arguments | Use |
130
+ |----------------|---------------------------------|-----------------------------------------------------------------------|
131
+ | `error` | `(error)` | Connection/frame errors and forwarded listener exceptions |
132
+ | `decode-error` | `(entry, error, packet, world)` | A retained processing/decoding error; context arguments may be absent |
133
+
134
+ `entry` includes `time`, `message`, `packetType`, and `world`. Inspect `client.debug.decodeErrors()` for recent retained
135
+ entries. Some decoders can retain partial state despite reporting an error; an empty/missing decoded component is not
136
+ proof it does not exist on the server.