castle-web-sdk 0.4.36 → 0.4.38
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/dist/castle.d.ts +3 -3
- package/dist/commands.d.ts +46 -1
- package/dist/multiplayer.d.ts +85 -2
- package/dist/multiplayer.js +422 -12
- package/dist/multiplayerProtocol.d.ts +54 -2
- package/dist/multiplayerVoice.d.ts +26 -2
- package/dist/multiplayerVoice.js +118 -3
- package/dist/server/platformHandle.d.ts +54 -0
- package/dist/server/wrapper.d.ts +79 -1
- package/dist/server/wrapper.js +334 -0
- package/dist/transport.js +1 -0
- package/package.json +1 -1
package/dist/server/wrapper.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isBinaryPayload, toUint8Array, } from "../multiplayerProtocol";
|
|
2
|
+
import { encodeStorageValue } from "../storageJson";
|
|
2
3
|
import { createServerStorage } from "./storage";
|
|
3
4
|
// The publish bundler binds the author's default callbacks export once and
|
|
4
5
|
// exports the returned one-argument function as `__castleBootSession`.
|
|
@@ -42,6 +43,109 @@ async function bootSession(platformHandle, callbacks) {
|
|
|
42
43
|
catch {
|
|
43
44
|
// Older shims may reject event names they do not recognize.
|
|
44
45
|
}
|
|
46
|
+
if (callbacks.onWorldCharacterTurn &&
|
|
47
|
+
typeof platformHandle.worldCharacterSend === "function" &&
|
|
48
|
+
typeof platformHandle.worldCharacterReject === "function") {
|
|
49
|
+
platformHandle.on("worldCharacterTurn", async (raw) => {
|
|
50
|
+
const request = assertPlatformWorldCharacterTurn(raw);
|
|
51
|
+
let settled = false;
|
|
52
|
+
const turn = Object.freeze({
|
|
53
|
+
requestId: request.requestId,
|
|
54
|
+
operationId: request.operationId,
|
|
55
|
+
speaker: Object.freeze({ ...request.speaker }),
|
|
56
|
+
characterId: request.characterId,
|
|
57
|
+
input: request.input,
|
|
58
|
+
audience: Object.freeze({
|
|
59
|
+
disclosure: request.audience.disclosure,
|
|
60
|
+
players: Object.freeze(request.audience.players.map((player) => Object.freeze({ ...player }))),
|
|
61
|
+
characterIds: Object.freeze([...request.audience.characterIds]),
|
|
62
|
+
}),
|
|
63
|
+
async send() {
|
|
64
|
+
if (settled)
|
|
65
|
+
throw new TypeError("This shared character request is already settled.");
|
|
66
|
+
settled = true;
|
|
67
|
+
const rawReceipt = await platformHandle.worldCharacterSend(request.grantId);
|
|
68
|
+
return parseWorldCharacterAdmission(rawReceipt, request.operationId);
|
|
69
|
+
},
|
|
70
|
+
reject(code) {
|
|
71
|
+
if (settled)
|
|
72
|
+
return;
|
|
73
|
+
settled = true;
|
|
74
|
+
platformHandle.worldCharacterReject(request.grantId, code);
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
await callbacks.onWorldCharacterTurn(session, turn);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
if (!settled)
|
|
82
|
+
turn.reject();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (callbacks.onWorldCharacterCommand &&
|
|
87
|
+
hasWorldCharacterPlatform(platformHandle, "worldCharacterCommand")) {
|
|
88
|
+
platformHandle.on("worldCharacterCommand", async (raw) => {
|
|
89
|
+
const request = assertPlatformWorldCharacterCommand(raw);
|
|
90
|
+
let settled = false;
|
|
91
|
+
const command = Object.freeze({
|
|
92
|
+
requestId: request.requestId,
|
|
93
|
+
operationId: request.operationId,
|
|
94
|
+
speaker: Object.freeze({ ...request.speaker }),
|
|
95
|
+
command: deepFreezeJsonObject(request.command),
|
|
96
|
+
async apply() {
|
|
97
|
+
if (settled)
|
|
98
|
+
throw new TypeError("This shared character command is already settled.");
|
|
99
|
+
settled = true;
|
|
100
|
+
return parseSharedCharacterJson(await platformHandle.worldCharacterCommand(request.grantId));
|
|
101
|
+
},
|
|
102
|
+
reject(code) {
|
|
103
|
+
if (settled)
|
|
104
|
+
return;
|
|
105
|
+
settled = true;
|
|
106
|
+
platformHandle.worldCharacterReject(request.grantId, code);
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
try {
|
|
110
|
+
await callbacks.onWorldCharacterCommand(session, command);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
if (!settled)
|
|
114
|
+
command.reject();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (callbacks.onWorldCharacterRead &&
|
|
119
|
+
hasWorldCharacterPlatform(platformHandle, "worldCharacterRead")) {
|
|
120
|
+
platformHandle.on("worldCharacterRead", async (raw) => {
|
|
121
|
+
const request = assertPlatformWorldCharacterRead(raw);
|
|
122
|
+
let settled = false;
|
|
123
|
+
const read = Object.freeze({
|
|
124
|
+
requestId: request.requestId,
|
|
125
|
+
speaker: Object.freeze({ ...request.speaker }),
|
|
126
|
+
query: deepFreezeJsonObject(request.query),
|
|
127
|
+
async read() {
|
|
128
|
+
if (settled)
|
|
129
|
+
throw new TypeError("This shared character read is already settled.");
|
|
130
|
+
settled = true;
|
|
131
|
+
return parseSharedCharacterJson(await platformHandle.worldCharacterRead(request.grantId));
|
|
132
|
+
},
|
|
133
|
+
reject(code) {
|
|
134
|
+
if (settled)
|
|
135
|
+
return;
|
|
136
|
+
settled = true;
|
|
137
|
+
platformHandle.worldCharacterReject(request.grantId, code);
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
try {
|
|
141
|
+
await callbacks.onWorldCharacterRead(session, read);
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
if (!settled)
|
|
145
|
+
read.reject();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
45
149
|
platformHandle.on("replaced", async () => {
|
|
46
150
|
await callbacks.onReplaced?.(session);
|
|
47
151
|
});
|
|
@@ -68,6 +172,7 @@ function createAuthorSession(platformHandle, players) {
|
|
|
68
172
|
deckId: platformHandle.config.deckId,
|
|
69
173
|
mode: platformHandle.config.mode,
|
|
70
174
|
storage: createServerStorage(platformHandle),
|
|
175
|
+
world: createServerWorld(platformHandle),
|
|
71
176
|
send(playerId, data) {
|
|
72
177
|
if (isBinaryPayload(data)) {
|
|
73
178
|
requireSendBinary(platformHandle);
|
|
@@ -106,6 +211,235 @@ function createAuthorSession(platformHandle, players) {
|
|
|
106
211
|
},
|
|
107
212
|
};
|
|
108
213
|
}
|
|
214
|
+
const WORLD_OPERATION_ID_MAX_BYTES = 128;
|
|
215
|
+
const WORLD_OPERATION_ID_MAX_CHARACTERS = 120;
|
|
216
|
+
const WORLD_CHECKPOINT_VERSION_MAX = 2_147_483_647;
|
|
217
|
+
function createServerWorld(platformHandle) {
|
|
218
|
+
if (!platformHandle.config.world)
|
|
219
|
+
return null;
|
|
220
|
+
if (typeof platformHandle.worldCheckpointRead !== "function" ||
|
|
221
|
+
typeof platformHandle.worldCheckpointCommit !== "function") {
|
|
222
|
+
throw worldError("The persistent-world checkpoint broker is unavailable.", "read");
|
|
223
|
+
}
|
|
224
|
+
const initial = parseWorldSnapshot(platformHandle.config.world, "read");
|
|
225
|
+
return Object.freeze({
|
|
226
|
+
snapshot: initial,
|
|
227
|
+
characters: createServerWorldCharacters(platformHandle),
|
|
228
|
+
async read() {
|
|
229
|
+
const result = await platformHandle.worldCheckpointRead();
|
|
230
|
+
return parseWorldSnapshot(parsePlatformJson(result, "read"), "read");
|
|
231
|
+
},
|
|
232
|
+
async commit(input) {
|
|
233
|
+
const expectedRevision = assertWorldRevision(input?.expectedRevision);
|
|
234
|
+
const operationId = assertWorldOperationId(input?.operationId);
|
|
235
|
+
const checkpoint = assertWorldCheckpoint(input?.checkpoint);
|
|
236
|
+
const raw = await platformHandle.worldCheckpointCommit(JSON.stringify({
|
|
237
|
+
expectedRevision,
|
|
238
|
+
operationId,
|
|
239
|
+
checkpoint,
|
|
240
|
+
}));
|
|
241
|
+
const result = parsePlatformJson(raw, "commit");
|
|
242
|
+
if (typeof result.replayed !== "boolean") {
|
|
243
|
+
throw worldError("The platform returned an invalid commit receipt.", "commit");
|
|
244
|
+
}
|
|
245
|
+
return Object.freeze({
|
|
246
|
+
revision: assertWorldRevision(result.revision),
|
|
247
|
+
replayed: result.replayed,
|
|
248
|
+
operationId,
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
function createServerWorldCharacters(platformHandle) {
|
|
254
|
+
return Object.freeze({
|
|
255
|
+
async publicInspect() {
|
|
256
|
+
if (typeof platformHandle.storage !== "function") {
|
|
257
|
+
throw new TypeError("The shared character broker is unavailable.");
|
|
258
|
+
}
|
|
259
|
+
return parseSharedCharacterJson(await platformHandle.storage("worldCharacterPublicInspect", "{}"));
|
|
260
|
+
},
|
|
261
|
+
async publicRelay(input) {
|
|
262
|
+
if (typeof platformHandle.storage !== "function") {
|
|
263
|
+
throw new TypeError("The shared character broker is unavailable.");
|
|
264
|
+
}
|
|
265
|
+
const operationId = assertWorldOperationId(input?.operationId);
|
|
266
|
+
const sourceId = assertWorldCharacterId(input?.sourceId, "sourceId", 128);
|
|
267
|
+
const transmittingCharacterId = assertWorldCharacterId(input?.transmittingCharacterId, "transmittingCharacterId");
|
|
268
|
+
if (!Array.isArray(input?.recipientCharacterIds) ||
|
|
269
|
+
input.recipientCharacterIds.length > 3) {
|
|
270
|
+
throw new TypeError("session.world.characters.publicRelay: recipientCharacterIds may contain at most 3 ids.");
|
|
271
|
+
}
|
|
272
|
+
const recipientCharacterIds = input.recipientCharacterIds.map((id) => assertWorldCharacterId(id, "recipientCharacterIds"));
|
|
273
|
+
if (new Set(recipientCharacterIds).size !== recipientCharacterIds.length) {
|
|
274
|
+
throw new TypeError("session.world.characters.publicRelay: recipientCharacterIds may not contain duplicates.");
|
|
275
|
+
}
|
|
276
|
+
return parseSharedCharacterJson(await platformHandle.storage("worldCharacterPublicRelay", JSON.stringify({
|
|
277
|
+
operationId,
|
|
278
|
+
sourceId,
|
|
279
|
+
transmittingCharacterId,
|
|
280
|
+
recipientCharacterIds: recipientCharacterIds.sort(),
|
|
281
|
+
})));
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
function parseWorldSnapshot(value, operation) {
|
|
286
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
287
|
+
throw worldError("The platform returned an invalid world checkpoint.", "read");
|
|
288
|
+
}
|
|
289
|
+
const raw = value;
|
|
290
|
+
return Object.freeze({
|
|
291
|
+
revision: assertWorldRevision(raw.revision),
|
|
292
|
+
checkpoint: raw.checkpoint === null
|
|
293
|
+
? null
|
|
294
|
+
: assertWorldCheckpoint(raw.checkpoint, operation),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
function assertPlatformWorldCharacterTurn(value) {
|
|
298
|
+
if (!value ||
|
|
299
|
+
typeof value !== "object" ||
|
|
300
|
+
typeof value.grantId !== "string" ||
|
|
301
|
+
typeof value.requestId !== "string" ||
|
|
302
|
+
typeof value.operationId !== "string" ||
|
|
303
|
+
typeof value.characterId !== "string" ||
|
|
304
|
+
typeof value.input !== "string" ||
|
|
305
|
+
!value.speaker ||
|
|
306
|
+
typeof value.speaker.playerId !== "string" ||
|
|
307
|
+
!value.audience ||
|
|
308
|
+
(value.audience.disclosure !== "participants" && value.audience.disclosure !== "world") ||
|
|
309
|
+
!Array.isArray(value.audience.players) ||
|
|
310
|
+
!Array.isArray(value.audience.characterIds)) {
|
|
311
|
+
throw new TypeError("The platform sent an invalid shared character request.");
|
|
312
|
+
}
|
|
313
|
+
return value;
|
|
314
|
+
}
|
|
315
|
+
function assertPlatformWorldCharacterCommand(value) {
|
|
316
|
+
if (!value ||
|
|
317
|
+
typeof value !== "object" ||
|
|
318
|
+
typeof value.grantId !== "string" ||
|
|
319
|
+
typeof value.requestId !== "string" ||
|
|
320
|
+
typeof value.operationId !== "string" ||
|
|
321
|
+
!value.speaker ||
|
|
322
|
+
typeof value.speaker.playerId !== "string" ||
|
|
323
|
+
!value.command ||
|
|
324
|
+
typeof value.command !== "object" ||
|
|
325
|
+
Array.isArray(value.command)) {
|
|
326
|
+
throw new TypeError("The platform sent an invalid shared character command.");
|
|
327
|
+
}
|
|
328
|
+
return value;
|
|
329
|
+
}
|
|
330
|
+
function assertPlatformWorldCharacterRead(value) {
|
|
331
|
+
if (!value ||
|
|
332
|
+
typeof value !== "object" ||
|
|
333
|
+
typeof value.grantId !== "string" ||
|
|
334
|
+
typeof value.requestId !== "string" ||
|
|
335
|
+
!value.speaker ||
|
|
336
|
+
typeof value.speaker.playerId !== "string" ||
|
|
337
|
+
!value.query ||
|
|
338
|
+
typeof value.query !== "object" ||
|
|
339
|
+
Array.isArray(value.query)) {
|
|
340
|
+
throw new TypeError("The platform sent an invalid shared character read.");
|
|
341
|
+
}
|
|
342
|
+
return value;
|
|
343
|
+
}
|
|
344
|
+
function hasWorldCharacterPlatform(platformHandle, operation) {
|
|
345
|
+
return (typeof platformHandle[operation] === "function" &&
|
|
346
|
+
typeof platformHandle.worldCharacterReject === "function");
|
|
347
|
+
}
|
|
348
|
+
function deepFreezeJsonObject(value) {
|
|
349
|
+
const normalized = JSON.parse(JSON.stringify(value));
|
|
350
|
+
deepFreeze(normalized);
|
|
351
|
+
return normalized;
|
|
352
|
+
}
|
|
353
|
+
function deepFreeze(value) {
|
|
354
|
+
if (!value || typeof value !== "object")
|
|
355
|
+
return;
|
|
356
|
+
Object.freeze(value);
|
|
357
|
+
for (const child of Object.values(value))
|
|
358
|
+
deepFreeze(child);
|
|
359
|
+
}
|
|
360
|
+
function parseSharedCharacterJson(json) {
|
|
361
|
+
try {
|
|
362
|
+
return JSON.parse(json);
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
throw new TypeError("The platform returned an invalid shared character response.");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function assertWorldCharacterId(value, field, maxCharacters = 100) {
|
|
369
|
+
if (typeof value !== "string" ||
|
|
370
|
+
value.length === 0 ||
|
|
371
|
+
value.length > maxCharacters ||
|
|
372
|
+
value.includes("\0") ||
|
|
373
|
+
new TextEncoder().encode(value).byteLength > 128) {
|
|
374
|
+
throw new TypeError(`session.world.characters: ${field} must be non-empty text of at most ${maxCharacters} characters and 128 bytes.`);
|
|
375
|
+
}
|
|
376
|
+
return value;
|
|
377
|
+
}
|
|
378
|
+
function parseWorldCharacterAdmission(json, operationId) {
|
|
379
|
+
let value;
|
|
380
|
+
try {
|
|
381
|
+
value = JSON.parse(json);
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
throw new TypeError("The platform returned an invalid shared character receipt.");
|
|
385
|
+
}
|
|
386
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
387
|
+
throw new TypeError("The platform returned an invalid shared character receipt.");
|
|
388
|
+
}
|
|
389
|
+
const receipt = value;
|
|
390
|
+
if (receipt.operationId !== operationId ||
|
|
391
|
+
(receipt.status !== "queued" &&
|
|
392
|
+
receipt.status !== "completed" &&
|
|
393
|
+
receipt.status !== "interrupted") ||
|
|
394
|
+
typeof receipt.replayed !== "boolean") {
|
|
395
|
+
throw new TypeError("The platform returned an invalid shared character receipt.");
|
|
396
|
+
}
|
|
397
|
+
return Object.freeze({
|
|
398
|
+
operationId,
|
|
399
|
+
status: receipt.status,
|
|
400
|
+
replayed: receipt.replayed,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
function assertWorldCheckpoint(value, operation = "commit") {
|
|
404
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
405
|
+
throw worldError("A world checkpoint requires version and value.", operation);
|
|
406
|
+
}
|
|
407
|
+
const raw = value;
|
|
408
|
+
if (!Number.isSafeInteger(raw.version) ||
|
|
409
|
+
raw.version < 0 ||
|
|
410
|
+
raw.version > WORLD_CHECKPOINT_VERSION_MAX) {
|
|
411
|
+
throw worldError(`A world checkpoint version must be an integer from 0 through ${WORLD_CHECKPOINT_VERSION_MAX}.`, operation);
|
|
412
|
+
}
|
|
413
|
+
const text = encodeStorageValue(raw.value, `session.world.${operation}`);
|
|
414
|
+
const normalized = JSON.parse(text);
|
|
415
|
+
return Object.freeze({ version: raw.version, value: normalized });
|
|
416
|
+
}
|
|
417
|
+
function assertWorldRevision(value) {
|
|
418
|
+
if (typeof value === "string" && /^(?:0|[1-9][0-9]*)$/.test(value))
|
|
419
|
+
return value;
|
|
420
|
+
throw worldError("A world checkpoint revision must be a canonical decimal string.", "commit");
|
|
421
|
+
}
|
|
422
|
+
function assertWorldOperationId(value) {
|
|
423
|
+
if (typeof value !== "string" ||
|
|
424
|
+
value.length === 0 ||
|
|
425
|
+
value.length > WORLD_OPERATION_ID_MAX_CHARACTERS ||
|
|
426
|
+
value.includes("\0") ||
|
|
427
|
+
new TextEncoder().encode(value).byteLength > WORLD_OPERATION_ID_MAX_BYTES) {
|
|
428
|
+
throw worldError(`A world checkpoint operation id must be at most ${WORLD_OPERATION_ID_MAX_CHARACTERS} characters and ${WORLD_OPERATION_ID_MAX_BYTES} bytes.`, "commit");
|
|
429
|
+
}
|
|
430
|
+
return value;
|
|
431
|
+
}
|
|
432
|
+
function parsePlatformJson(value, operation) {
|
|
433
|
+
try {
|
|
434
|
+
return JSON.parse(value);
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
throw worldError("The platform returned an invalid world checkpoint response.", operation);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function worldError(message, operation) {
|
|
441
|
+
return new TypeError(`session.world.${operation}: ${message}`);
|
|
442
|
+
}
|
|
109
443
|
function requireSendBinary(platformHandle) {
|
|
110
444
|
if (typeof platformHandle.sendBinary !== "function")
|
|
111
445
|
unsupportedBinaryRuntime();
|
package/dist/transport.js
CHANGED