@crowdedkingdomstudios/crowdyjs 2.1.2 → 4.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.
- package/MIGRATION.md +26 -0
- package/README.md +155 -749
- package/dist/auth-state.d.ts +6 -16
- package/dist/auth-state.d.ts.map +1 -1
- package/dist/auth-state.js +9 -26
- package/dist/client.d.ts +14 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +24 -17
- package/dist/crowdy-client.d.ts +62 -16
- package/dist/crowdy-client.d.ts.map +1 -1
- package/dist/crowdy-client.js +70 -28
- package/dist/domains/apps.d.ts +48 -20
- package/dist/domains/apps.d.ts.map +1 -1
- package/dist/domains/apps.js +58 -35
- package/dist/domains/auth.d.ts +33 -22
- package/dist/domains/auth.d.ts.map +1 -1
- package/dist/domains/auth.js +51 -33
- package/dist/domains/serverStatus.d.ts +2 -1
- package/dist/domains/serverStatus.d.ts.map +1 -1
- package/dist/domains/serverStatus.js +5 -1
- package/dist/domains/udp.d.ts +28 -3
- package/dist/domains/udp.d.ts.map +1 -1
- package/dist/domains/udp.js +52 -1
- package/dist/domains/users.d.ts +19 -16
- package/dist/domains/users.d.ts.map +1 -1
- package/dist/domains/users.js +21 -39
- package/dist/errors.d.ts +42 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +42 -0
- package/dist/generated/graphql.d.ts +1473 -11
- package/dist/generated/graphql.d.ts.map +1 -1
- package/dist/generated/graphql.js +17 -8
- package/dist/index.d.ts +37 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -20
- package/dist/logger.d.ts +8 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +1 -0
- package/dist/realtime.d.ts +89 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +273 -0
- package/dist/session.d.ts +27 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +61 -0
- package/dist/subscriptions.d.ts +1 -48
- package/dist/subscriptions.d.ts.map +1 -1
- package/dist/subscriptions.js +1 -192
- package/dist/types.d.ts +2 -31
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -33
- package/dist/utils.d.ts +12 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +50 -0
- package/dist/world.d.ts +44 -0
- package/dist/world.d.ts.map +1 -0
- package/dist/world.js +105 -0
- package/package.json +12 -3
- package/dist/domains/appAccess.d.ts +0 -23
- package/dist/domains/appAccess.d.ts.map +0 -1
- package/dist/domains/appAccess.js +0 -42
- package/dist/domains/billing.d.ts +0 -17
- package/dist/domains/billing.d.ts.map +0 -1
- package/dist/domains/billing.js +0 -31
- package/dist/domains/organizations.d.ts +0 -33
- package/dist/domains/organizations.d.ts.map +0 -1
- package/dist/domains/organizations.js +0 -90
- package/dist/domains/payments.d.ts +0 -20
- package/dist/domains/payments.d.ts.map +0 -1
- package/dist/domains/payments.js +0 -28
- package/dist/domains/quotas.d.ts +0 -20
- package/dist/domains/quotas.d.ts.map +0 -1
- package/dist/domains/quotas.js +0 -34
package/README.md
CHANGED
|
@@ -1,818 +1,224 @@
|
|
|
1
|
-
# CrowdyJS
|
|
1
|
+
# CrowdyJS
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
Handles authentication, real-time subscriptions, and all replication-server
|
|
5
|
-
communication through a single `CrowdyClient` instance.
|
|
3
|
+
The official browser-first TypeScript SDK for **Crowded Kingdoms**. CrowdyJS gives you one typed client that handles auth, the world/replication GraphQL API, and the UDP proxy subscription stream behind a single shared session.
|
|
6
4
|
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
- [NPM](https://www.npmjs.com/package/@crowdedkingdomstudios/crowdyjs)
|
|
10
|
-
- [GitHub](https://github.com/CrowdedKingdoms/CrowdyJS)
|
|
11
|
-
- [Discord](https://discord.gg/crowdedkingdoms)
|
|
12
|
-
- [YouTube](https://www.youtube.com/@CrowdedKingdoms)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
## Installation
|
|
5
|
+
## Install
|
|
16
6
|
|
|
17
7
|
```bash
|
|
18
8
|
npm install @crowdedkingdomstudios/crowdyjs
|
|
19
9
|
```
|
|
20
10
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
The SDK uses the browser-native `WebSocket` API. In Node.js you need a
|
|
24
|
-
polyfill such as the `ws` package:
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
npm install ws
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
```javascript
|
|
31
|
-
import WebSocket from 'ws';
|
|
32
|
-
globalThis.WebSocket = WebSocket;
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Place this **before** importing `CrowdyClient`.
|
|
36
|
-
|
|
37
|
-
### Browser
|
|
38
|
-
|
|
39
|
-
No extra setup needed -- the SDK uses the built-in `WebSocket`.
|
|
40
|
-
|
|
41
|
-
## Quick Start
|
|
42
|
-
|
|
43
|
-
```javascript
|
|
44
|
-
import { CrowdyClient } from '@crowdedkingdomstudios/crowdyjs';
|
|
45
|
-
|
|
46
|
-
const client = new CrowdyClient({
|
|
47
|
-
graphqlEndpoint: 'https://your-server.com/graphql',
|
|
48
|
-
wsEndpoint: 'wss://your-server.com/graphql',
|
|
49
|
-
});
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
If omitted, both endpoints default to `localhost:3000/graphql`.
|
|
53
|
-
|
|
54
|
-
## Connection Lifecycle
|
|
55
|
-
|
|
56
|
-
The SDK follows a four-step lifecycle that matches the server protocol:
|
|
57
|
-
|
|
58
|
-
```
|
|
59
|
-
1. Login --> obtain a game token
|
|
60
|
-
2. Subscribe --> auto-opens UDP proxy session
|
|
61
|
-
3. Register --> tell the game server your chunk position
|
|
62
|
-
4. Send updates --> replicated to other clients in range
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### 1. Login
|
|
66
|
-
|
|
67
|
-
```javascript
|
|
68
|
-
const auth = await client.login('user@example.com', 'password');
|
|
69
|
-
// auth.token -- 64-char hex game token (set automatically)
|
|
70
|
-
// auth.user.email -- logged-in user
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Or register a new account:
|
|
74
|
-
|
|
75
|
-
```javascript
|
|
76
|
-
const auth = await client.register('user@example.com', 'password', 'MyGamertag');
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### 2. Subscribe to Notifications
|
|
80
|
-
|
|
81
|
-
Register one or more notification handlers. The first handler automatically
|
|
82
|
-
opens a WebSocket subscription and a UDP proxy session to the game server --
|
|
83
|
-
no explicit `connectUdpProxy()` call is needed.
|
|
84
|
-
|
|
85
|
-
```javascript
|
|
86
|
-
const unsub = client.onActorUpdate((notification) => {
|
|
87
|
-
console.log('Actor:', notification.uuid);
|
|
88
|
-
console.log('State:', notification.state);
|
|
89
|
-
console.log('Chunk:', notification.chunkX, notification.chunkY, notification.chunkZ);
|
|
90
|
-
console.log('Time:', notification.epochMillis);
|
|
91
|
-
});
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
Handlers are unsubscribed by calling the returned function:
|
|
95
|
-
|
|
96
|
-
```javascript
|
|
97
|
-
unsub(); // stop receiving ActorUpdateNotification
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
When all handlers are removed the WebSocket is closed automatically.
|
|
101
|
-
|
|
102
|
-
### 3. Register in a Chunk
|
|
103
|
-
|
|
104
|
-
Before other clients can see you, send an initial actor update so the game
|
|
105
|
-
server knows which chunk you occupy. Use a minimal base64 payload (the
|
|
106
|
-
server requires a non-empty `state`).
|
|
107
|
-
|
|
108
|
-
#### Generating a UUID
|
|
109
|
-
|
|
110
|
-
Every client must create its own UUID as a **random 32-byte UTF-8 string**.
|
|
111
|
-
This ensures each client produces a globally unique identifier without
|
|
112
|
-
relying on a central registry.
|
|
113
|
-
|
|
114
|
-
```javascript
|
|
115
|
-
// Node.js
|
|
116
|
-
const MY_UUID = crypto.randomBytes(32).toString('hex').slice(0, 32);
|
|
117
|
-
|
|
118
|
-
// Browser
|
|
119
|
-
const MY_UUID = Array.from(crypto.getRandomValues(new Uint8Array(16)))
|
|
120
|
-
.map((b) => b.toString(16).padStart(2, '0'))
|
|
121
|
-
.join('');
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
```javascript
|
|
125
|
-
await client.sendActorUpdate({
|
|
126
|
-
appId: 0,
|
|
127
|
-
chunk: { x: 0, y: 0, z: 0 },
|
|
128
|
-
distance: 8,
|
|
129
|
-
uuid: MY_UUID,
|
|
130
|
-
state: 'AA==', // minimal base64 payload for registration
|
|
131
|
-
sequenceNumber: 1,
|
|
132
|
-
});
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
Every actor in every client must do this. After registration, the game
|
|
136
|
-
server fans out subsequent updates to all registered clients in range.
|
|
137
|
-
|
|
138
|
-
### 4. Send Actor Updates
|
|
139
|
-
|
|
140
|
-
```javascript
|
|
141
|
-
// Build your binary state and base64-encode it
|
|
142
|
-
const stateBuffer = new ArrayBuffer(96);
|
|
143
|
-
const view = new DataView(stateBuffer);
|
|
144
|
-
view.setFloat32(0, posX, true);
|
|
145
|
-
view.setFloat32(4, posY, true);
|
|
146
|
-
view.setFloat32(8, posZ, true);
|
|
147
|
-
// ... fill remaining fields
|
|
148
|
-
|
|
149
|
-
const base64State = btoa(String.fromCharCode(...new Uint8Array(stateBuffer)));
|
|
150
|
-
|
|
151
|
-
await client.sendActorUpdate({
|
|
152
|
-
appId: 0,
|
|
153
|
-
chunk: { x: 0, y: 0, z: 0 },
|
|
154
|
-
distance: 8,
|
|
155
|
-
decayRate: 0,
|
|
156
|
-
uuid: MY_UUID,
|
|
157
|
-
state: base64State,
|
|
158
|
-
sequenceNumber: 2,
|
|
159
|
-
});
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
### 5. Disconnect
|
|
163
|
-
|
|
164
|
-
```javascript
|
|
165
|
-
await client.disconnectUdpProxy(); // release the UDP session
|
|
166
|
-
client.close(); // close WebSocket + clear state
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
Unsubscribing from notifications stops delivery but does **not** release
|
|
170
|
-
the UDP session. Call `disconnectUdpProxy()` explicitly, or the server
|
|
171
|
-
will release it after 30 seconds of inactivity.
|
|
172
|
-
|
|
173
|
-
## Request / Notification Mapping
|
|
174
|
-
|
|
175
|
-
Every accepted spatial request is fanned out by the game server as a
|
|
176
|
-
notification to all clients in range, **including the sender**. Treat
|
|
177
|
-
your own arrival as the round-trip echo for that send (correlate by
|
|
178
|
-
`uuid` + `sequenceNumber`).
|
|
179
|
-
|
|
180
|
-
| Mutation (you send) | Notification (everyone in range receives) |
|
|
181
|
-
|-----------------------------|-------------------------------------------|
|
|
182
|
-
| `sendActorUpdate(...)` | `ActorUpdateNotification` |
|
|
183
|
-
| `sendVoxelUpdate(...)` | `VoxelUpdateNotification` |
|
|
184
|
-
| `sendAudioPacket(...)` | `ClientAudioNotification` |
|
|
185
|
-
| `sendTextPacket(...)` | `ClientTextNotification` |
|
|
186
|
-
| `sendClientEvent(...)` | `ClientEventNotification` |
|
|
187
|
-
| *(generic spatial)* | *(generic spatial)* |
|
|
188
|
-
|
|
189
|
-
`ServerEventNotification` is server-originated (no matching client
|
|
190
|
-
mutation). `GenericErrorResponse` carries failures for any of the above
|
|
191
|
-
— correlate to the offending send via `sequenceNumber`. The legacy
|
|
192
|
-
`ActorUpdateResponse` and `VoxelUpdateResponse` ack types are retired
|
|
193
|
-
on the wire; rely on the self-fanout notification (and
|
|
194
|
-
`GenericErrorResponse` for failures) instead.
|
|
195
|
-
|
|
196
|
-
> *Generic spatial* refers to wire type `GENERIC_SPATIAL_1` (an arbitrary
|
|
197
|
-
> client-defined payload routed through the same spatial fan-out as the
|
|
198
|
-
> typed messages above). It is not currently exposed by this SDK.
|
|
199
|
-
|
|
200
|
-
## Subscription Handlers
|
|
201
|
-
|
|
202
|
-
All spatial notification types share a uniform header:
|
|
203
|
-
|
|
204
|
-
| Field | Type | Description |
|
|
205
|
-
|-------|------|-------------|
|
|
206
|
-
| `appId` | `string` | App ID (tenant / world identifier) |
|
|
207
|
-
| `chunkX` | `string` | Chunk X coordinate |
|
|
208
|
-
| `chunkY` | `string` | Chunk Y coordinate |
|
|
209
|
-
| `chunkZ` | `string` | Chunk Z coordinate |
|
|
210
|
-
| `distance` | `number` | Replication distance (0-8) |
|
|
211
|
-
| `decayRate` | `number` | Delivery decay (0-5) |
|
|
212
|
-
| `uuid` | `string` | Random 32-byte UTF-8 sender UUID |
|
|
213
|
-
| `sequenceNumber` | `number` | uint8 (0-255), wraps |
|
|
214
|
-
| `epochMillis` | `string` | Server UTC timestamp in ms |
|
|
215
|
-
|
|
216
|
-
Each handler receives a fully-typed notification object:
|
|
217
|
-
|
|
218
|
-
```javascript
|
|
219
|
-
client.onActorUpdate((n) => { /* n: ActorUpdateNotification -- adds: state */ });
|
|
220
|
-
client.onActorUpdateResponse((n) => { /* n: ActorUpdateResponse */ });
|
|
221
|
-
client.onVoxelUpdate((n) => { /* n: VoxelUpdateNotification -- adds: voxelX/Y/Z, voxelType, voxelState */ });
|
|
222
|
-
client.onVoxelUpdateResponse((n) => { /* n: VoxelUpdateResponse */ });
|
|
223
|
-
client.onClientAudio((n) => { /* n: ClientAudioNotification -- adds: audioData */ });
|
|
224
|
-
client.onClientText((n) => { /* n: ClientTextNotification -- adds: text */ });
|
|
225
|
-
client.onClientEvent((n) => { /* n: ClientEventNotification -- adds: eventType, state */ });
|
|
226
|
-
client.onServerEvent((n) => { /* n: ServerEventNotification -- adds: eventType, state */ });
|
|
227
|
-
client.onGenericError((e) => { /* e: GenericErrorResponse -- sequenceNumber, errorCode only */ });
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
`GenericErrorResponse` is the only type without the spatial header; it has
|
|
231
|
-
just `sequenceNumber` and `errorCode`.
|
|
232
|
-
|
|
233
|
-
## Input Parameters
|
|
234
|
-
|
|
235
|
-
### Common fields
|
|
236
|
-
|
|
237
|
-
All mutation inputs share these fields:
|
|
238
|
-
|
|
239
|
-
| Field | Type | Required | Default | Description |
|
|
240
|
-
|-------|------|----------|---------|-------------|
|
|
241
|
-
| `appId` | `number` | yes | -- | App ID (tenant / world identifier) |
|
|
242
|
-
| `chunk` | `{ x, y, z }` | yes | -- | Chunk coordinates (numbers) |
|
|
243
|
-
| `uuid` | `string` | yes | -- | Random 32-byte UTF-8 string (see [Generating a UUID](#generating-a-uuid)) |
|
|
244
|
-
| `distance` | `number` | no | `8` | Replication range (0-8 chunks, Chebyshev) |
|
|
245
|
-
| `decayRate` | `number` | no | `0` | Delivery decay (see table below) |
|
|
246
|
-
| `sequenceNumber` | `number` | no | `0` | uint8 (0-255) for correlation |
|
|
247
|
-
|
|
248
|
-
### `decayRate` values
|
|
249
|
-
|
|
250
|
-
| Value | Name | Behavior |
|
|
251
|
-
|-------|------|----------|
|
|
252
|
-
| 0 | None | All clients within `distance` receive every message |
|
|
253
|
-
| 1 | Exponential | Each ring receives half the messages of the previous ring |
|
|
254
|
-
| 2 | Linear 50% | Furthest ring receives 50% of messages |
|
|
255
|
-
| 3 | Linear 25% | Furthest ring receives 25% of messages |
|
|
256
|
-
| 4 | Linear 10% | Furthest ring receives 10% of messages |
|
|
257
|
-
| 5 | Linear 5% | Furthest ring receives 5% of messages |
|
|
258
|
-
|
|
259
|
-
### `sendActorUpdate`
|
|
260
|
-
|
|
261
|
-
| Field | Type | Description |
|
|
262
|
-
|-------|------|-------------|
|
|
263
|
-
| `state` | `string` | Base64-encoded binary state (must be non-empty) |
|
|
264
|
-
|
|
265
|
-
### `sendVoxelUpdate`
|
|
266
|
-
|
|
267
|
-
| Field | Type | Description |
|
|
268
|
-
|-------|------|-------------|
|
|
269
|
-
| `voxel` | `{ x, y, z }` | Voxel position within the chunk |
|
|
270
|
-
| `voxelType` | `number` | Voxel type ID |
|
|
271
|
-
| `voxelState` | `string` | Base64-encoded voxel state |
|
|
272
|
-
|
|
273
|
-
### `sendAudioPacket`
|
|
274
|
-
|
|
275
|
-
| Field | Type | Description |
|
|
276
|
-
|-------|------|-------------|
|
|
277
|
-
| `audioData` | `string` | Base64-encoded compressed audio |
|
|
278
|
-
|
|
279
|
-
### `sendTextPacket`
|
|
280
|
-
|
|
281
|
-
| Field | Type | Description |
|
|
282
|
-
|-------|------|-------------|
|
|
283
|
-
| `text` | `string` | Chat message text |
|
|
284
|
-
|
|
285
|
-
### `sendClientEvent`
|
|
286
|
-
|
|
287
|
-
| Field | Type | Description |
|
|
288
|
-
|-------|------|-------------|
|
|
289
|
-
| `eventType` | `number` | Custom event type ID |
|
|
290
|
-
| `state` | `string` | Base64-encoded event state |
|
|
11
|
+
CrowdyJS v4 targets browsers by default and uses native `fetch`, `WebSocket`, `crypto`, `btoa`, and `atob`. Node tools can still use the SDK, but must provide browser-compatible globals when opening realtime connections.
|
|
291
12
|
|
|
292
|
-
##
|
|
13
|
+
## Quick start
|
|
293
14
|
|
|
294
|
-
```
|
|
295
|
-
import
|
|
296
|
-
|
|
15
|
+
```ts
|
|
16
|
+
import {
|
|
17
|
+
BrowserLocalStorageTokenStore,
|
|
18
|
+
createCrowdyClient,
|
|
19
|
+
} from '@crowdedkingdomstudios/crowdyjs';
|
|
297
20
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
// 2. Subscribe (auto-opens UDP proxy session)
|
|
311
|
-
const unsubActors = client.onActorUpdate((n) => {
|
|
312
|
-
console.log(`Actor ${n.uuid} at chunk (${n.chunkX},${n.chunkY},${n.chunkZ})`);
|
|
313
|
-
console.log(` state=${n.state} seq=${n.sequenceNumber} t=${n.epochMillis}`);
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
const unsubErrors = client.onGenericError((e) => {
|
|
317
|
-
console.error(`Error: ${e.errorCode} (seq ${e.sequenceNumber})`);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
// 3. Register in chunk
|
|
321
|
-
await client.sendActorUpdate({
|
|
322
|
-
appId: 0,
|
|
323
|
-
chunk: { x: 0, y: 0, z: 0 },
|
|
324
|
-
distance: 8,
|
|
325
|
-
uuid: MY_UUID,
|
|
326
|
-
state: 'AA==',
|
|
327
|
-
sequenceNumber: 1,
|
|
21
|
+
const client = createCrowdyClient({
|
|
22
|
+
// Game API (world data + UDP proxy)
|
|
23
|
+
httpUrl: 'https://game.example.com',
|
|
24
|
+
wsUrl: 'wss://game.example.com',
|
|
25
|
+
// Management API (login, register, profile)
|
|
26
|
+
managementUrl: 'https://management.example.com',
|
|
27
|
+
tokenStore: new BrowserLocalStorageTokenStore(),
|
|
28
|
+
realtime: {
|
|
29
|
+
retryAttempts: 8,
|
|
30
|
+
waitTimeoutMs: 5000,
|
|
31
|
+
},
|
|
328
32
|
});
|
|
329
33
|
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
crypto.getRandomValues(buf);
|
|
335
|
-
const state = btoa(String.fromCharCode(...buf));
|
|
336
|
-
|
|
337
|
-
await client.sendActorUpdate({
|
|
338
|
-
appId: 0,
|
|
339
|
-
chunk: { x: 0, y: 0, z: 0 },
|
|
340
|
-
distance: 8,
|
|
341
|
-
uuid: MY_UUID,
|
|
342
|
-
state,
|
|
343
|
-
sequenceNumber: seq++ % 256,
|
|
344
|
-
});
|
|
345
|
-
}, 100);
|
|
346
|
-
|
|
347
|
-
// 5. Cleanup
|
|
348
|
-
clearInterval(interval);
|
|
349
|
-
unsubActors();
|
|
350
|
-
unsubErrors();
|
|
351
|
-
await client.disconnectUdpProxy();
|
|
352
|
-
client.close();
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
## Building a Real-Time Game
|
|
356
|
-
|
|
357
|
-
The Quick Start above is enough to send and receive packets. Once you wire
|
|
358
|
-
the SDK into an actual game these patterns will save you a lot of debugging
|
|
359
|
-
time.
|
|
360
|
-
|
|
361
|
-
### Decouple the network from the render loop
|
|
362
|
-
|
|
363
|
-
Browsers throttle `requestAnimationFrame` aggressively when the tab is
|
|
364
|
-
backgrounded — often to ~1 fps or fully paused. If your `sendActorUpdate`
|
|
365
|
-
cadence is driven by the render loop, your character will appear frozen
|
|
366
|
-
to other players the moment a user switches tabs. Drive sends with
|
|
367
|
-
`setInterval` (which keeps firing in background tabs):
|
|
368
|
-
|
|
369
|
-
```javascript
|
|
370
|
-
const SEND_INTERVAL_MS = 100; // 10 Hz
|
|
371
|
-
const sendId = setInterval(async () => {
|
|
372
|
-
await client.sendActorUpdate({
|
|
373
|
-
appId, chunk: getCurrentChunk(),
|
|
374
|
-
uuid: MY_UUID,
|
|
375
|
-
state: encodeLocalPose(),
|
|
376
|
-
sequenceNumber: nextSeq(),
|
|
377
|
-
});
|
|
378
|
-
}, SEND_INTERVAL_MS);
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
Apply incoming notifications **inside the WS callback**, not buffered for
|
|
382
|
-
the next frame. That keeps your data model in sync with real network
|
|
383
|
-
arrivals even while the render loop is paused:
|
|
384
|
-
|
|
385
|
-
```javascript
|
|
386
|
-
client.onActorUpdate((notification) => {
|
|
387
|
-
registry.applyRemote(notification, performance.now()); // synchronous
|
|
388
|
-
});
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
The renderer should treat the registry as a *view source* — sprites
|
|
392
|
-
interpolate toward registry-held targets each frame, but the registry
|
|
393
|
-
itself is mutated only by the network layer.
|
|
394
|
-
|
|
395
|
-
### Verify the round-trip with self-fanout
|
|
396
|
-
|
|
397
|
-
The game server fans out every accepted actor update to all clients in
|
|
398
|
-
the chunk, **including the sender**. Use your own arrival as the WS
|
|
399
|
-
round-trip echo (correlate by `uuid` + `sequenceNumber`):
|
|
400
|
-
|
|
401
|
-
```javascript
|
|
402
|
-
let lastSendAt = null, lastSendSeq = null;
|
|
403
|
-
let lastEchoAt = null, lastEchoSeq = null;
|
|
404
|
-
|
|
405
|
-
async function sendOnce() {
|
|
406
|
-
const seq = nextSeq();
|
|
407
|
-
const ok = await client.sendActorUpdate({ /* ... */, sequenceNumber: seq });
|
|
408
|
-
if (ok) { lastSendAt = performance.now(); lastSendSeq = seq; }
|
|
34
|
+
// Restore a previous session if there is one, otherwise log in.
|
|
35
|
+
await client.session.restore();
|
|
36
|
+
if (!client.session.getToken()) {
|
|
37
|
+
await client.auth.login({ email: 'player@example.com', password: 'secret' });
|
|
409
38
|
}
|
|
410
39
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
lastEchoSeq = n.sequenceNumber;
|
|
415
|
-
return; // don't render ourselves as a remote
|
|
416
|
-
}
|
|
417
|
-
registry.applyRemote(n);
|
|
418
|
-
});
|
|
40
|
+
// Fetch the per-app bootstrap (version requirements, UDP availability, spatial limits).
|
|
41
|
+
const bootstrap = await client.serverStatus.gameClientBootstrap('1');
|
|
42
|
+
console.log(bootstrap.versionInfo.minimumClientVersion);
|
|
419
43
|
```
|
|
420
44
|
|
|
421
|
-
`
|
|
422
|
-
is the canonical ack. `GenericErrorResponse` carries failures (correlate
|
|
423
|
-
via `sequenceNumber`).
|
|
45
|
+
Both endpoints share a single `AuthState`, so once `client.auth.login()` returns, every subsequent SDK call (against either endpoint) carries the bearer token automatically.
|
|
424
46
|
|
|
425
|
-
|
|
47
|
+
If `managementUrl` is omitted, the SDK falls back to `httpUrl` for backwards-compat with the single-endpoint deployment.
|
|
426
48
|
|
|
427
|
-
|
|
428
|
-
either direction. A 10 Hz send loop trivially keeps it warm. If your
|
|
429
|
-
sender pauses (e.g. no local input → no diff to send), the next mutation
|
|
430
|
-
will lazily re-establish via `ensureUdpProxyConnection` — `connectUdpProxy()`
|
|
431
|
-
is idempotent. There is no need for a separate keepalive.
|
|
49
|
+
## Sub-clients at a glance
|
|
432
50
|
|
|
433
|
-
|
|
51
|
+
| Sub-client | What it does |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `client.auth` | Register, log in, log out, password reset, email confirmation. |
|
|
54
|
+
| `client.users` | `me`, `updateGamertag`, profile reads. |
|
|
55
|
+
| `client.session` | Token store, `restore()`, `getToken()`, manual `setToken()`. |
|
|
56
|
+
| `client.serverStatus` | `gameClientBootstrap(appId)` — per-app version info, UDP status, spatial limits. |
|
|
57
|
+
| `client.chunks`, `client.voxels`, `client.actors`, `client.avatars`, `client.state` | World data reads + writes. |
|
|
58
|
+
| `client.teleport` | Teleport requests. |
|
|
59
|
+
| `client.udp` | UDP proxy subscriptions + spatial mutations (`sendActorUpdate`, `sendVoxelUpdate`, `sendAudioPacket`, `sendTextPacket`, `sendClientEvent`). |
|
|
60
|
+
| `client.realtime` | Connection status, manual `connect()` / `disconnect()`, `onStatus()` listener. |
|
|
61
|
+
| `client.world(appId)` | Higher-level helpers for browser games (`actor.join`, `actor.sendState`, `actor.sendText`). |
|
|
434
62
|
|
|
435
|
-
|
|
436
|
-
the internal client. Track wall-clock arrival time of *any* notification
|
|
437
|
-
and force a fresh subscription if it goes silent:
|
|
63
|
+
Auth and user reads always target `managementUrl`. Everything else targets `httpUrl` / `wsUrl`.
|
|
438
64
|
|
|
439
|
-
|
|
440
|
-
let lastNotificationAt = performance.now();
|
|
441
|
-
function bumpActivity() { lastNotificationAt = performance.now(); }
|
|
65
|
+
## Game-loop lifecycle
|
|
442
66
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
67
|
+
1. Authenticate with `client.auth.login()` or restore a previous token through `client.session.restore()`.
|
|
68
|
+
2. Subscribe to UDP proxy notifications with `client.udp.subscribe()` (the SDK will open the realtime socket on demand).
|
|
69
|
+
3. Join a chunk by sending an initial actor update.
|
|
70
|
+
4. Send actor, voxel, text, audio, and client-event updates through `client.udp` or the higher-level `client.world(appId)` helpers.
|
|
71
|
+
5. Call `client.udp.disconnect()` when leaving the world.
|
|
72
|
+
6. Call `client.close()` when disposing the SDK instance.
|
|
447
73
|
|
|
448
|
-
|
|
449
|
-
if (performance.now() - lastNotificationAt > 8000) {
|
|
450
|
-
forceResubscribe(); // see below
|
|
451
|
-
}
|
|
452
|
-
}, 1000);
|
|
453
|
-
```
|
|
74
|
+
## Per-app routing
|
|
454
75
|
|
|
455
|
-
|
|
456
|
-
`subscribe()` again — the SDK opens a new WebSocket on a fresh subscribe.
|
|
457
|
-
Then send one `sendActorUpdate` to re-register your chunk position so
|
|
458
|
-
others see you immediately.
|
|
76
|
+
When a player is about to join an app, query its routing fields on the management API first:
|
|
459
77
|
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
78
|
+
```graphql
|
|
79
|
+
query AppForRouting($id: BigInt!) {
|
|
80
|
+
app(id: $id) {
|
|
81
|
+
appId
|
|
82
|
+
splitMode
|
|
83
|
+
gameApiUrl
|
|
84
|
+
}
|
|
465
85
|
}
|
|
466
86
|
```
|
|
467
87
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
|
|
88
|
+
If `splitMode && gameApiUrl`, the app lives behind its own Game API deployment. Build a **second** `CrowdyClient` with `httpUrl: gameApiUrl` (and the matching `wsUrl`) **sharing the same `tokenStore` as the first client**, then drive gameplay through that client. Apps without `splitMode` keep working against the default `httpUrl` you configured.
|
|
89
|
+
|
|
90
|
+
## Realtime notifications
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const unsubscribe = client.udp.subscribe({
|
|
94
|
+
actorUpdate: (event) => {
|
|
95
|
+
console.log(event.uuid, event.state);
|
|
96
|
+
},
|
|
97
|
+
voxelUpdate: (event) => { /* ... */ },
|
|
98
|
+
text: (event) => { /* ... */ },
|
|
99
|
+
audio: (event) => { /* ... */ },
|
|
100
|
+
clientEvent: (event) => { /* ... */ },
|
|
101
|
+
serverEvent: (event) => { /* ... */ },
|
|
102
|
+
singleActorMessage: (event) => {
|
|
103
|
+
// A direct actor-to-actor message addressed to you.
|
|
104
|
+
console.log(event.uuid, event.payload); // payload is base64
|
|
105
|
+
},
|
|
106
|
+
genericError: (event) => {
|
|
107
|
+
console.warn(event.sequenceNumber, event.errorCode);
|
|
108
|
+
},
|
|
109
|
+
connectionEvent: (event) => {
|
|
110
|
+
console.warn(event.code, event.message);
|
|
111
|
+
},
|
|
112
|
+
error: (error) => {
|
|
113
|
+
console.error(error.code, error.message);
|
|
114
|
+
},
|
|
483
115
|
});
|
|
484
|
-
```
|
|
485
|
-
|
|
486
|
-
### Track presence with timestamps
|
|
487
|
-
|
|
488
|
-
There is no live "who is in this chunk" query — `client.actors.list({ chunk })`
|
|
489
|
-
returns the persisted DB roster, not the live UDP set. Each remote actor's
|
|
490
|
-
presence is inferred from the last notification you saw from them:
|
|
491
116
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
client.onActorUpdate((n) => {
|
|
496
|
-
if (n.uuid === MY_UUID) return;
|
|
497
|
-
const now = performance.now();
|
|
498
|
-
const existing = remotes.get(n.uuid);
|
|
499
|
-
if (!existing) {
|
|
500
|
-
remotes.set(n.uuid, { firstSeenAt: now, lastUpdateAt: now, pose: decode(n.state) });
|
|
501
|
-
} else {
|
|
502
|
-
existing.lastUpdateAt = now;
|
|
503
|
-
existing.pose = decode(n.state);
|
|
504
|
-
}
|
|
117
|
+
client.realtime.onStatus((status) => {
|
|
118
|
+
console.log('realtime:', status);
|
|
505
119
|
});
|
|
506
|
-
```
|
|
507
120
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
remotes simply because you couldn't have heard from them.
|
|
511
|
-
|
|
512
|
-
```javascript
|
|
513
|
-
const STALE_TIMEOUT_MS = 10_000;
|
|
514
|
-
setInterval(() => {
|
|
515
|
-
if (document.hidden || isWsStale()) return;
|
|
516
|
-
const now = performance.now();
|
|
517
|
-
for (const [uuid, r] of remotes) {
|
|
518
|
-
if (now - r.lastUpdateAt > STALE_TIMEOUT_MS) remotes.delete(uuid);
|
|
519
|
-
}
|
|
520
|
-
}, 3000);
|
|
121
|
+
// Later:
|
|
122
|
+
unsubscribe();
|
|
521
123
|
```
|
|
522
124
|
|
|
523
|
-
|
|
524
|
-
before reaping resumes — that lets remotes re-broadcast on their own
|
|
525
|
-
cadence first.
|
|
526
|
-
|
|
527
|
-
### Suggested module layout
|
|
125
|
+
The SDK uses the `graphql-transport-ws` protocol through `graphql-ws`, reconnects with backoff, re-reads the current token before reconnecting, and resubscribes automatically.
|
|
528
126
|
|
|
529
|
-
|
|
127
|
+
## Spatial sends
|
|
530
128
|
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
destroys sprites; the singleton session and registry persist, so revisiting
|
|
541
|
-
the scene (or returning from a hidden tab) does not lose presence state.
|
|
542
|
-
Recreating sprites on scene re-entry walks the existing registry entries.
|
|
543
|
-
|
|
544
|
-
### Diagnostics worth logging
|
|
545
|
-
|
|
546
|
-
When something goes wrong, these are the values you'll wish you had:
|
|
547
|
-
|
|
548
|
-
- `sendCount`, `lastSendAt`, `lastSendSeq` — your local sender's history.
|
|
549
|
-
- `echoCount`, `lastEchoAt`, `lastEchoSeq`, `msSinceLastEcho` — the WS
|
|
550
|
-
round-trip via self-fanout. If `echoCount` flatlines while `sendCount`
|
|
551
|
-
climbs, the WS path is wedged.
|
|
552
|
-
- `notificationsByType` — map of `__typename` to arrival count. Useful for
|
|
553
|
-
confirming what the server is actually emitting.
|
|
554
|
-
- Per-remote `lastUpdateAt` / `msSinceUpdate`.
|
|
555
|
-
- `visibilityState`, `hidden`, `lastWsResubscribeAt`, `reaperPaused`.
|
|
556
|
-
|
|
557
|
-
Expose them on a global handle (e.g. `window.__MYGAME_NET__.snapshot()`)
|
|
558
|
-
during development — a structured JSON snapshot is far easier to triage
|
|
559
|
-
than a wall of streaming logs.
|
|
560
|
-
|
|
561
|
-
## API Reference
|
|
562
|
-
|
|
563
|
-
### Constructor
|
|
564
|
-
|
|
565
|
-
```typescript
|
|
566
|
-
new CrowdyClient(config?: CrowdyClientConfig)
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
| Option | Type | Default | Description |
|
|
570
|
-
|--------|------|---------|-------------|
|
|
571
|
-
| `graphqlEndpoint` | `string` | `http://localhost:3000/graphql` | HTTP endpoint for mutations/queries |
|
|
572
|
-
| `wsEndpoint` | `string` | `ws://localhost:3000/graphql` | WebSocket endpoint for subscriptions |
|
|
573
|
-
| `timeout` | `number` | `60000` | HTTP request timeout in ms |
|
|
574
|
-
|
|
575
|
-
### Authentication
|
|
576
|
-
|
|
577
|
-
| Method | Returns | Description |
|
|
578
|
-
|--------|---------|-------------|
|
|
579
|
-
| `login(email, password)` | `Promise<AuthResponse>` | Login and store the game token |
|
|
580
|
-
| `register(email, password, gamertag?)` | `Promise<AuthResponse>` | Register and store the game token |
|
|
581
|
-
| `getAuthToken()` | `string \| null` | Get the current game token |
|
|
582
|
-
|
|
583
|
-
### UDP Proxy
|
|
584
|
-
|
|
585
|
-
| Method | Returns | Description |
|
|
586
|
-
|--------|---------|-------------|
|
|
587
|
-
| `connectUdpProxy()` | `Promise<UdpProxyConnectionStatus>` | Open a UDP session (idempotent: returns the existing status if one is open). Optional — `udpNotifications` and any `send*` mutation also create a session lazily. |
|
|
588
|
-
| `disconnectUdpProxy()` | `Promise<boolean>` | Release the UDP session and complete the current `udpNotifications` stream. To force a fresh socket: call `disconnectUdpProxy()` then `connectUdpProxy()`. |
|
|
589
|
-
| `getConnectionStatus()` | `Promise<UdpProxyConnectionStatus>` | Check if a UDP session is active |
|
|
590
|
-
|
|
591
|
-
### Mutations
|
|
592
|
-
|
|
593
|
-
| Method | Returns | Description |
|
|
594
|
-
|--------|---------|-------------|
|
|
595
|
-
| `sendActorUpdate(input)` | `Promise<boolean>` | Send an actor state update |
|
|
596
|
-
| `sendVoxelUpdate(input)` | `Promise<boolean>` | Modify a voxel in a chunk |
|
|
597
|
-
| `sendAudioPacket(input)` | `Promise<boolean>` | Send voice audio data |
|
|
598
|
-
| `sendTextPacket(input)` | `Promise<boolean>` | Send chat text |
|
|
599
|
-
| `sendClientEvent(input)` | `Promise<boolean>` | Send a custom event |
|
|
600
|
-
|
|
601
|
-
### Subscriptions
|
|
602
|
-
|
|
603
|
-
| Method | Handler receives | Description |
|
|
604
|
-
|--------|-----------------|-------------|
|
|
605
|
-
| `onActorUpdate(handler)` | `ActorUpdateNotification` | Another client's actor state |
|
|
606
|
-
| `onActorUpdateResponse(handler)` | `ActorUpdateResponse` | Server ack for your actor update |
|
|
607
|
-
| `onVoxelUpdate(handler)` | `VoxelUpdateNotification` | A voxel was modified |
|
|
608
|
-
| `onVoxelUpdateResponse(handler)` | `VoxelUpdateResponse` | Server ack for your voxel update |
|
|
609
|
-
| `onClientAudio(handler)` | `ClientAudioNotification` | Voice audio from another client |
|
|
610
|
-
| `onClientText(handler)` | `ClientTextNotification` | Chat text from another client |
|
|
611
|
-
| `onClientEvent(handler)` | `ClientEventNotification` | Custom event from another client |
|
|
612
|
-
| `onServerEvent(handler)` | `ServerEventNotification` | Event from the game server |
|
|
613
|
-
| `onGenericError(handler)` | `GenericErrorResponse` | Error from the server |
|
|
614
|
-
|
|
615
|
-
All subscription methods return an `UnsubscribeFn` -- call it to remove
|
|
616
|
-
the handler.
|
|
617
|
-
|
|
618
|
-
### Cleanup
|
|
619
|
-
|
|
620
|
-
| Method | Description |
|
|
621
|
-
|--------|-------------|
|
|
622
|
-
| `close()` | Close the WebSocket, remove all handlers, clear auth state |
|
|
623
|
-
|
|
624
|
-
## Domain APIs
|
|
625
|
-
|
|
626
|
-
In addition to the replication-focused methods documented above, the
|
|
627
|
-
client exposes a set of typed *domain wrappers* that cover the rest of
|
|
628
|
-
the Crowded Kingdoms GraphQL API. Each wrapper is mounted on the
|
|
629
|
-
`CrowdyClient` instance and uses the same auth token / base URL as the
|
|
630
|
-
replication API. Operation inputs and return shapes are generated from
|
|
631
|
-
the live `schema.gql` via `graphql-codegen`, so they stay in lockstep
|
|
632
|
-
with the server.
|
|
633
|
-
|
|
634
|
-
```javascript
|
|
635
|
-
const client = new CrowdyClient({ graphqlEndpoint: '...' });
|
|
636
|
-
await client.auth.login({ email, password });
|
|
637
|
-
```
|
|
638
|
-
|
|
639
|
-
### `client.auth` -- authentication & session lifecycle
|
|
640
|
-
|
|
641
|
-
```javascript
|
|
642
|
-
await client.auth.register({ email, password, gamertag });
|
|
643
|
-
await client.auth.login({ email, password });
|
|
644
|
-
await client.auth.confirmEmail('...token...');
|
|
645
|
-
await client.auth.requestPasswordReset('user@example.com');
|
|
646
|
-
await client.auth.resetPassword({ token: '...', newPassword: '...' });
|
|
647
|
-
await client.auth.resendConfirmationEmail('user@example.com');
|
|
648
|
-
await client.auth.changePassword('old', 'new');
|
|
649
|
-
await client.auth.logout();
|
|
650
|
-
await client.auth.logoutAllDevices();
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
The legacy shortcuts `client.login(email, password)` and
|
|
654
|
-
`client.register(email, password, gamertag?)` are kept for backwards
|
|
655
|
-
compatibility and delegate to `client.auth`.
|
|
656
|
-
|
|
657
|
-
### `client.users` -- user directory & profile
|
|
658
|
-
|
|
659
|
-
```javascript
|
|
660
|
-
const me = await client.users.me();
|
|
661
|
-
const u = await client.users.byId('123');
|
|
662
|
-
const all = await client.users.list();
|
|
663
|
-
const list = await client.users.byGamertag('Gandalf');
|
|
664
|
-
const list2 = await client.users.byEmail('a@b.c');
|
|
665
|
-
|
|
666
|
-
await client.users.updateGamertag({ gamertag: 'Newname', disambiguation: '0001' });
|
|
667
|
-
await client.users.updateUserState({ state: 'base64...' });
|
|
668
|
-
```
|
|
669
|
-
|
|
670
|
-
### `client.orgs` -- organizations / studios
|
|
671
|
-
|
|
672
|
-
```javascript
|
|
673
|
-
const org = await client.orgs.create({ name: 'Acme', slug: 'acme' });
|
|
674
|
-
const byId = await client.orgs.byId(org.orgId);
|
|
675
|
-
const bySlug = await client.orgs.bySlug('acme');
|
|
676
|
-
const members = await client.orgs.members(org.orgId);
|
|
677
|
-
|
|
678
|
-
await client.orgs.inviteMember({ orgId: org.orgId, userId: '42' });
|
|
679
|
-
const token = await client.orgs.createOrgToken({ orgId: org.orgId, label: 'CI' });
|
|
680
|
-
```
|
|
681
|
-
|
|
682
|
-
### `client.appAccess` -- app access tiers and grants
|
|
683
|
-
|
|
684
|
-
```javascript
|
|
685
|
-
const tiers = await client.appAccess.listTiers(appId);
|
|
686
|
-
const mine = await client.appAccess.myAccess(appId);
|
|
687
|
-
|
|
688
|
-
await client.appAccess.createTier({ appId, name: 'Pro', isFree: false, tierOrder: 2 });
|
|
689
|
-
await client.appAccess.grant({ appId, userId: '42', tierId });
|
|
690
|
-
await client.appAccess.revoke(appId, '42');
|
|
691
|
-
```
|
|
692
|
-
|
|
693
|
-
### `client.billing` -- wallets & app budgets
|
|
694
|
-
|
|
695
|
-
```javascript
|
|
696
|
-
const wallet = await client.billing.balance(orgId);
|
|
697
|
-
const txns = await client.billing.transactions({ orgId, limit: 50, offset: 0 });
|
|
698
|
-
const budget = await client.billing.appBudget(orgId, appId);
|
|
129
|
+
```ts
|
|
130
|
+
const response = await client.udp.sendActorUpdateAndWait({
|
|
131
|
+
appId: '1',
|
|
132
|
+
chunk: { x: '0', y: '0', z: '0' },
|
|
133
|
+
uuid: '0123456789abcdef0123456789abcdef',
|
|
134
|
+
state: 'AA==', // base64-encoded payload
|
|
135
|
+
distance: 8,
|
|
136
|
+
decayRate: 1,
|
|
137
|
+
});
|
|
699
138
|
|
|
700
|
-
|
|
139
|
+
console.log(response.__typename, response.sequenceNumber);
|
|
701
140
|
```
|
|
702
141
|
|
|
703
|
-
|
|
142
|
+
The plain `sendActorUpdate`, `sendVoxelUpdate`, `sendAudioPacket`, `sendTextPacket`, and `sendClientEvent` methods return the GraphQL mutation result immediately. The `AndWait` variants allocate a `sequenceNumber` when one is missing and wait for either a matching notification or `GenericErrorResponse`.
|
|
704
143
|
|
|
705
|
-
|
|
706
|
-
const orgQ = await client.quotas.byOrg(orgId);
|
|
707
|
-
const appQ = await client.quotas.byApp(appId);
|
|
144
|
+
### Actor-to-actor messages
|
|
708
145
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
146
|
+
```ts
|
|
147
|
+
// Delivered only to the actor whose UUID matches `targetUuid`; you must know
|
|
148
|
+
// that actor's current chunk. Fire-and-forget — the sender gets no echo, so
|
|
149
|
+
// there is no `AndWait` variant. The target receives a
|
|
150
|
+
// `SingleActorMessageNotification` on its subscription.
|
|
151
|
+
await client.udp.sendSingleActorMessage({
|
|
152
|
+
appId: '1',
|
|
153
|
+
chunk: { x: '7', y: '1', z: '2' }, // the TARGET actor's chunk
|
|
154
|
+
targetUuid: '0123456789abcdef0123456789abcdef',
|
|
155
|
+
payload: 'aGVsbG8=', // base64; embed sender identity here if you need it
|
|
715
156
|
});
|
|
716
|
-
await client.quotas.delete(q.quotaId);
|
|
717
157
|
```
|
|
718
158
|
|
|
719
|
-
|
|
159
|
+
## World helpers
|
|
720
160
|
|
|
721
|
-
```
|
|
722
|
-
const
|
|
723
|
-
const
|
|
724
|
-
const nearby = await client.chunks.byDistance({ appId, centerCoordinate: coordinates, maxDistance: 4 });
|
|
725
|
-
const voxels = await client.chunks.voxelList({ appId, coordinates });
|
|
161
|
+
```ts
|
|
162
|
+
const world = client.world('1');
|
|
163
|
+
const actor = world.actor();
|
|
726
164
|
|
|
727
|
-
await
|
|
728
|
-
await
|
|
729
|
-
await
|
|
730
|
-
```
|
|
731
|
-
|
|
732
|
-
### `client.voxels` -- voxel queries, history, rollback
|
|
165
|
+
await actor.join({ x: '0', y: '0', z: '0' });
|
|
166
|
+
await actor.sendState('AA==');
|
|
167
|
+
await actor.sendText('hello nearby players');
|
|
733
168
|
|
|
734
|
-
|
|
735
|
-
await
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
169
|
+
// Direct message to one other actor (you supply its UUID + current chunk):
|
|
170
|
+
await actor.sendToActor(
|
|
171
|
+
'0123456789abcdef0123456789abcdef',
|
|
172
|
+
'aGVsbG8=', // base64 payload
|
|
173
|
+
{ x: '7', y: '1', z: '2' },
|
|
174
|
+
);
|
|
740
175
|
```
|
|
741
176
|
|
|
742
|
-
|
|
177
|
+
The world helpers are thin wrappers over `client.udp.*` with the appId pre-bound — convenient for browser games. Advanced callers can always use `client.udp.*` with the generated GraphQL input types directly.
|
|
743
178
|
|
|
744
|
-
|
|
745
|
-
const a = await client.actors.create({
|
|
746
|
-
appId, uuid: '...', chunk: { x: '0', y: '0', z: '0' },
|
|
747
|
-
});
|
|
748
|
-
await client.actors.get(a.uuid);
|
|
749
|
-
await client.actors.list({ appId });
|
|
750
|
-
await client.actors.batchLookup({ uuids: [a.uuid] });
|
|
751
|
-
await client.actors.update(a.uuid, { publicState: '...' });
|
|
752
|
-
await client.actors.updateState(a.uuid, { publicState: '...' });
|
|
753
|
-
await client.actors.delete(a.uuid);
|
|
754
|
-
```
|
|
179
|
+
## Errors
|
|
755
180
|
|
|
756
|
-
|
|
757
|
-
`client.sendActorUpdate(...)` UDP path -- it is unchanged.
|
|
181
|
+
Transport and protocol failures throw structured error classes:
|
|
758
182
|
|
|
759
|
-
|
|
183
|
+
- `CrowdyHttpError` — non-2xx response from a GraphQL endpoint.
|
|
184
|
+
- `CrowdyGraphQLError` — preserves every GraphQL error including `path` and `extensions.code`.
|
|
185
|
+
- `CrowdyNetworkError` — network-level failure (DNS, TLS, connection refused).
|
|
186
|
+
- `CrowdyTimeoutError` — request or `AndWait` timed out.
|
|
187
|
+
- `CrowdyRealtimeError` — realtime subscription couldn't be established or was dropped.
|
|
188
|
+
- `CrowdyProtocolError` — server response failed schema validation.
|
|
760
189
|
|
|
761
|
-
|
|
762
|
-
const result = await client.teleport.request({
|
|
763
|
-
appId,
|
|
764
|
-
chunkAddress: { x: '0', y: '0', z: '0' },
|
|
765
|
-
voxelAddress: { x: 0, y: 0, z: 0 },
|
|
766
|
-
UUID: '...',
|
|
767
|
-
});
|
|
768
|
-
```
|
|
190
|
+
## Auth notes
|
|
769
191
|
|
|
770
|
-
|
|
192
|
+
- Use `client.auth.setToken(token)` if you need to seed a token externally (e.g. when restoring auth from a non-default storage).
|
|
193
|
+
- `client.session.restore()` reads from the configured `tokenStore`. `BrowserLocalStorageTokenStore` is provided; bring your own for SSR or Node usage.
|
|
194
|
+
- A single `AuthState` is observed by both the HTTP client and the realtime socket, so HTTP and WebSocket auth can never drift.
|
|
771
195
|
|
|
772
|
-
|
|
773
|
-
await client.state.update({ appId, state: 'base64...' });
|
|
774
|
-
await client.state.getOne(appId);
|
|
775
|
-
await client.state.getAll();
|
|
776
|
-
await client.state.delete(appId);
|
|
777
|
-
```
|
|
196
|
+
## What's NOT in CrowdyJS
|
|
778
197
|
|
|
779
|
-
|
|
198
|
+
CrowdyJS focuses on the **game-client surface**: auth, world data, UDP proxy, profile reads. The following operations are **not** exposed by the SDK and should be called against the management GraphQL API directly (with a server-side token, typically from a studio backend):
|
|
780
199
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
await client.serverStatus.versionInfo();
|
|
786
|
-
```
|
|
200
|
+
- Org / app / billing / payments / quotas operations
|
|
201
|
+
- Access-tier and runtime-permission administration
|
|
202
|
+
- Game-token issuance / revocation
|
|
203
|
+
- Marketplace and catalog management
|
|
787
204
|
|
|
788
|
-
|
|
205
|
+
The SDK is intentionally scoped to client-side, end-user-facing flows.
|
|
789
206
|
|
|
790
|
-
|
|
791
|
-
apps are provisioned out of band. `client.apps` exists as a stable
|
|
792
|
-
namespace for future additions; in the meantime use `client.appAccess`,
|
|
793
|
-
`client.state`, `client.billing`, `client.chunks`, and `client.voxels`
|
|
794
|
-
for app-scoped functionality.
|
|
207
|
+
## Low-level GraphQL access
|
|
795
208
|
|
|
796
|
-
|
|
209
|
+
Game-client methods are first-class, but generated operation documents are also available through a transport escape hatch:
|
|
797
210
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
documents:
|
|
211
|
+
```ts
|
|
212
|
+
import { VersionInfoDocument } from '@crowdedkingdomstudios/crowdyjs/generated';
|
|
801
213
|
|
|
802
|
-
|
|
803
|
-
npm run codegen # one-shot
|
|
804
|
-
npm run codegen:watch # watch mode
|
|
214
|
+
const data = await client.graphql.request(VersionInfoDocument);
|
|
805
215
|
```
|
|
806
216
|
|
|
807
|
-
|
|
217
|
+
Most consumers should prefer the typed methods on `client.auth`, `client.users`, `client.udp`, `client.serverStatus`, and `client.world()`.
|
|
808
218
|
|
|
809
|
-
##
|
|
219
|
+
## Migration
|
|
810
220
|
|
|
811
|
-
|
|
812
|
-
notification interfaces, input types, and handler signatures are fully
|
|
813
|
-
typed for IDE autocomplete and compile-time safety. Schema-derived
|
|
814
|
-
input/output types (e.g. `CreateOrganizationInput`, `Organization`,
|
|
815
|
-
`AppBudget`) are re-exported from the package root.
|
|
221
|
+
See [MIGRATION.md](MIGRATION.md) for breaking changes between SDK majors.
|
|
816
222
|
|
|
817
223
|
## License
|
|
818
224
|
|