@zaisma/game-server 1.23.0 → 1.24.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/bench/glass.js +24814 -24394
- package/bin/node.js +2 -2
- package/package.json +1 -1
- package/sdk/zaisma.d.ts +4 -0
- package/sdk/zaisma.js +64 -4
- package/src/doors.d.ts +142 -0
- package/src/doors.js +257 -0
- package/src/requests.d.ts +1 -1
- package/src/requests.js +2 -0
- package/src/room.d.ts +79 -43
- package/src/room.js +144 -20
package/src/room.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* running one copy of the game server. Names follow docs/NAMING.md.
|
|
8
8
|
*/
|
|
9
9
|
import { type GameDatabase } from "./database.ts";
|
|
10
|
+
import { type DoorContract, type DoorShape } from "./doors.ts";
|
|
10
11
|
import type { PassClaims, PlayerKind, ReceiptClaims } from "./pass.ts";
|
|
11
12
|
import type { Server } from "./requests.ts";
|
|
12
13
|
import { type Schedule, type ScheduledRun } from "./schedules.ts";
|
|
@@ -1117,7 +1118,7 @@ export type ClientMessage = {
|
|
|
1117
1118
|
* the game became unavailable, its kept room was deleted, or no players were left for its spectators.
|
|
1118
1119
|
*/
|
|
1119
1120
|
export type EndReason = "game" | "hosting_paused" | "game_server_stopped" | "game_unavailable" | "kept_room_deleted" | "no_players";
|
|
1120
|
-
export type RoomErrorCode = "room_full" | "room_closed" | "ended" | "too_big" | "too_fast" | "bad_message" | "bad_pass" | "bad_player_token" | "no_hello" | "not_allowed" | "game_unavailable" | "busy" | "kept_out" | "refused" | "room_not_running" | "hosting_paused" | "account_needed" | "game_not_open_to_player" | "blocked_player_here" | "other_version" | "spectators_full" | "spectator_cannot_send" | "already_playing" | "bad_origin" | "send_queue_full" | "chat_not_here" | "chat_not_allowed" | "chat_empty" | "chat_too_long" | "chat_too_fast";
|
|
1121
|
+
export type RoomErrorCode = "room_full" | "room_closed" | "ended" | "too_big" | "too_fast" | "bad_message" | "bad_pass" | "bad_player_token" | "no_hello" | "not_allowed" | "game_unavailable" | "busy" | "kept_out" | "refused" | "room_not_running" | "hosting_paused" | "rented_share_used" | "account_needed" | "game_not_open_to_player" | "blocked_player_here" | "other_version" | "spectators_full" | "spectator_cannot_send" | "already_playing" | "bad_origin" | "send_queue_full" | "chat_not_here" | "chat_not_allowed" | "chat_empty" | "chat_too_long" | "chat_too_fast";
|
|
1121
1122
|
export declare const ERROR_TEXT: Record<RoomErrorCode, string>;
|
|
1122
1123
|
/** What players are told when Zaisma ends a match at room.endsAt. */
|
|
1123
1124
|
export declare const HOSTING_PAUSED_END_MESSAGE = "This game's hosting is paused, so this match has ended. New rooms open again when the month resets or its developer adds credit.";
|
|
@@ -1334,6 +1335,7 @@ export interface StateSyncHooks {
|
|
|
1334
1335
|
export declare const NO_MESSAGE_TYPE = "(none)";
|
|
1335
1336
|
export declare const BYTES_MESSAGE_TYPE = "(bytes)";
|
|
1336
1337
|
export declare const ZAISMA_MESSAGE_TYPE = "(zaisma)";
|
|
1338
|
+
export declare const ROOM_FRAME: Readonly<Record<string, unknown>>;
|
|
1337
1339
|
/**
|
|
1338
1340
|
* Every message a room takes (docs/DOORS.md). receive's switch is over these names, so a new one without a row
|
|
1339
1341
|
* doesn't build. A spectator may send only what's for room members. hello comes first and proves the pass; chat is the host's to take before receive (readChatMessage);
|
|
@@ -1348,55 +1350,89 @@ export declare const ROOM_MESSAGE_DOORS: {
|
|
|
1348
1350
|
readonly source_path: "packages/game-server/src/pass.ts";
|
|
1349
1351
|
readonly symbol_name: "verifyPass";
|
|
1350
1352
|
};
|
|
1353
|
+
readonly takes: {
|
|
1354
|
+
readonly query: {};
|
|
1355
|
+
readonly body: {
|
|
1356
|
+
readonly kind: "json";
|
|
1357
|
+
readonly fields: {
|
|
1358
|
+
readonly idleTimeoutSec: DoorShape;
|
|
1359
|
+
readonly idleCheckEnabled: DoorShape;
|
|
1360
|
+
readonly type: DoorShape;
|
|
1361
|
+
readonly protocolVersionNumber: DoorShape;
|
|
1362
|
+
readonly pass: DoorShape;
|
|
1363
|
+
readonly playerToken: DoorShape;
|
|
1364
|
+
readonly joinData: DoorShape;
|
|
1365
|
+
readonly room: DoorShape;
|
|
1366
|
+
readonly isSpectator: DoorShape;
|
|
1367
|
+
readonly isQuickJoin: DoorShape;
|
|
1368
|
+
readonly avoidBlockedPlayers: DoorShape;
|
|
1369
|
+
readonly versionId: DoorShape;
|
|
1370
|
+
readonly playShare: DoorShape;
|
|
1371
|
+
readonly hasGraphicsChip: DoorShape;
|
|
1372
|
+
readonly isInputSeen: DoorShape;
|
|
1373
|
+
};
|
|
1374
|
+
readonly required: readonly ["type"];
|
|
1375
|
+
readonly maximumSizeBytes: number;
|
|
1376
|
+
};
|
|
1377
|
+
};
|
|
1378
|
+
readonly answers: import("./doors.ts").DoorAnswer;
|
|
1379
|
+
readonly limit: {
|
|
1380
|
+
readonly level: "steady";
|
|
1381
|
+
readonly counts: "player";
|
|
1382
|
+
};
|
|
1351
1383
|
};
|
|
1352
|
-
readonly probe_answer:
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
readonly
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
readonly
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
readonly
|
|
1362
|
-
whoMayCall:
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
readonly message: import("./doors.ts").DoorContractFields & {
|
|
1377
|
-
whoMayCall: import("./doors.ts").DoorCallerProvenByGuard;
|
|
1378
|
-
};
|
|
1379
|
-
readonly bytes: import("./doors.ts").DoorContractFields & {
|
|
1380
|
-
whoMayCall: import("./doors.ts").DoorCallerProvenByGuard;
|
|
1381
|
-
};
|
|
1382
|
-
readonly chat: import("./doors.ts").DoorContractFields & {
|
|
1383
|
-
whoMayCall: import("./doors.ts").DoorCallerProvenByGuard;
|
|
1384
|
-
};
|
|
1385
|
-
readonly block: import("./doors.ts").DoorContractFields & {
|
|
1386
|
-
whoMayCall: import("./doors.ts").DoorCallerProvenByGuard;
|
|
1387
|
-
};
|
|
1388
|
-
readonly unblock: import("./doors.ts").DoorContractFields & {
|
|
1389
|
-
whoMayCall: import("./doors.ts").DoorCallerProvenByGuard;
|
|
1390
|
-
};
|
|
1391
|
-
readonly friend_request: import("./doors.ts").DoorContractFields & {
|
|
1392
|
-
whoMayCall: import("./doors.ts").DoorCallerProvenByGuard;
|
|
1384
|
+
readonly probe_answer: DoorContract;
|
|
1385
|
+
readonly room: DoorContract;
|
|
1386
|
+
readonly kick: DoorContract;
|
|
1387
|
+
readonly here: DoorContract;
|
|
1388
|
+
readonly idle: DoorContract;
|
|
1389
|
+
readonly hidden: DoorContract;
|
|
1390
|
+
readonly shown: DoorContract;
|
|
1391
|
+
readonly back: DoorContract;
|
|
1392
|
+
readonly message: DoorContract;
|
|
1393
|
+
readonly bytes: {
|
|
1394
|
+
readonly whoMayCall: "room_player";
|
|
1395
|
+
readonly reaches: "room";
|
|
1396
|
+
readonly trusts: readonly ["player_pass"];
|
|
1397
|
+
readonly takes: {
|
|
1398
|
+
readonly query: {};
|
|
1399
|
+
readonly body: {
|
|
1400
|
+
readonly kind: "bytes";
|
|
1401
|
+
readonly maximumSizeBytes: number;
|
|
1402
|
+
};
|
|
1403
|
+
};
|
|
1404
|
+
readonly answers: import("./doors.ts").DoorAnswer;
|
|
1405
|
+
readonly limit: {
|
|
1406
|
+
readonly whyNoLimit: "room_token_bucket";
|
|
1407
|
+
};
|
|
1393
1408
|
};
|
|
1409
|
+
readonly chat: DoorContract;
|
|
1410
|
+
readonly block: DoorContract;
|
|
1411
|
+
readonly unblock: DoorContract;
|
|
1412
|
+
readonly friend_request: DoorContract;
|
|
1394
1413
|
readonly "zaisma:purchase": {
|
|
1395
|
-
readonly whoMayCall: "room_player";
|
|
1396
1414
|
readonly reaches: "purchase";
|
|
1397
1415
|
readonly trusts: readonly ["player_pass", "purchase_receipt"];
|
|
1416
|
+
readonly takes: import("./doors.ts").DoorTakes;
|
|
1417
|
+
readonly answers: import("./doors.ts").DoorAnswer;
|
|
1418
|
+
readonly limit: import("./doors.ts").DoorLimit;
|
|
1419
|
+
readonly whoMayCall: import("./doors.ts").DoorCallerProvenByGuard;
|
|
1420
|
+
} | {
|
|
1421
|
+
readonly reaches: "purchase";
|
|
1422
|
+
readonly trusts: readonly ["player_pass", "purchase_receipt"];
|
|
1423
|
+
readonly takes: import("./doors.ts").DoorTakes;
|
|
1424
|
+
readonly answers: import("./doors.ts").DoorAnswer;
|
|
1425
|
+
readonly limit: import("./doors.ts").DoorLimit;
|
|
1426
|
+
readonly whoMayCall: import("./doors.ts").DoorCallerProvenByFunction;
|
|
1427
|
+
readonly provenBy: import("./doors.ts").DoorProof;
|
|
1398
1428
|
};
|
|
1399
1429
|
};
|
|
1430
|
+
/**
|
|
1431
|
+
* Whether a message breaks its door's shape: a field of the wrong type or past its bound, or one it needs missing. A
|
|
1432
|
+
* name the door doesn't know is left alone and never read, unlike at other doors: a game's SDK comes from Zaisma and
|
|
1433
|
+
* its server may be the developer's own, older one, so a newer SDK's field has to keep working there.
|
|
1434
|
+
*/
|
|
1435
|
+
export declare function roomMessageProblem(type: Exclude<RoomMessageType, "bytes">, message: Record<string, unknown>): string | null;
|
|
1400
1436
|
export type RoomMessageType = keyof typeof ROOM_MESSAGE_DOORS;
|
|
1401
1437
|
export declare const OTHER_MESSAGE_TYPE = "(other)";
|
|
1402
1438
|
/** State sync's updates (stateSyncEnabled). */
|
package/src/room.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { CHAT_CHECK_LIMITS, CHAT_ROOM_LIMITS } from "./chat.js";
|
|
10
10
|
import { DatabaseError } from "./database.js";
|
|
11
|
-
import { checkDoor } from "./doors.js";
|
|
11
|
+
import { answersJson, checkDoor, fieldsOf, gameData, listOf, orNothing, realNumber, schemaProblems, text as textUpTo, yesOrNo } from "./doors.js";
|
|
12
12
|
import { isRoomCode, roomCode } from "./names.js";
|
|
13
13
|
import { readSchedules } from "./schedules.js";
|
|
14
14
|
import { roomCallChecks } from "./room-calls.js";
|
|
@@ -322,6 +322,7 @@ export const ERROR_TEXT = {
|
|
|
322
322
|
refused: "The game didn't let you into this room.",
|
|
323
323
|
room_not_running: "That room isn't running.",
|
|
324
324
|
hosting_paused: "This game's hosting is paused. It starts again when the month resets or its developer adds credit.",
|
|
325
|
+
rented_share_used: "This network looks like a server. Turn off your VPN or sign in with an account that has played before.",
|
|
325
326
|
account_needed: "Playing online needs a Zaisma account. Sign in on the game's page to play.",
|
|
326
327
|
game_not_open_to_player: "This game isn't open to this account.",
|
|
327
328
|
blocked_player_here: "Someone you blocked is in this room.",
|
|
@@ -445,6 +446,9 @@ export function readChatMessage(text) {
|
|
|
445
446
|
}
|
|
446
447
|
if (!parsed || typeof parsed !== "object" || parsed.type !== "chat")
|
|
447
448
|
return null;
|
|
449
|
+
// One of the wrong shape isn't chat: receive refuses it.
|
|
450
|
+
if (roomMessageProblem("chat", parsed))
|
|
451
|
+
return null;
|
|
448
452
|
const chat = parsed;
|
|
449
453
|
return { chatNumber: Number.isSafeInteger(chat.chatNumber) ? chat.chatNumber : 0, text: chat.text, toPlayerIds: chat.toPlayerIds, sizeBytes: utf8ByteCount(text) };
|
|
450
454
|
}
|
|
@@ -503,32 +507,150 @@ export const STATE_SYNC = Symbol("stateSync");
|
|
|
503
507
|
export const NO_MESSAGE_TYPE = "(none)";
|
|
504
508
|
export const BYTES_MESSAGE_TYPE = "(bytes)";
|
|
505
509
|
export const ZAISMA_MESSAGE_TYPE = "(zaisma)";
|
|
506
|
-
|
|
507
|
-
const
|
|
508
|
-
const
|
|
510
|
+
/** A pass or a player token, at most, and a whole hello with the fullest of them. */
|
|
511
|
+
const HELLO_PROOF_MAXIMUM_CHARACTER_COUNT = 96 * 1024;
|
|
512
|
+
const HELLO_MAXIMUM_SIZE_BYTES = HELLO_PROOF_MAXIMUM_CHARACTER_COUNT + 2 * ROOM_LIMITS.maximumJoinDataSizeBytes + 8 * 1024;
|
|
513
|
+
/** What a player is sent, by its type, with each type's fields closed: a game's own data and a room's settings pass as they are. Checked in tests only, since this is the busiest path there is. */
|
|
514
|
+
const looseObject = { type: "object" };
|
|
515
|
+
const frameOf = (messageType, fields = {}, optional = {}) => ({
|
|
516
|
+
type: "object",
|
|
517
|
+
properties: { type: { type: "string", enum: [messageType] }, ...fields, ...optional },
|
|
518
|
+
required: ["type", ...Object.keys(fields)],
|
|
519
|
+
additionalProperties: false,
|
|
520
|
+
});
|
|
521
|
+
const wireText = { type: "string" };
|
|
522
|
+
const wireNumber = { type: "number" };
|
|
523
|
+
const wireYesOrNo = { type: "boolean" };
|
|
524
|
+
const wireNullable = (type) => ({ type: [type, "null"] });
|
|
525
|
+
const wirePlayers = { type: "array", items: looseObject };
|
|
526
|
+
const WIRE_DATA = {};
|
|
527
|
+
export const ROOM_FRAME = {
|
|
528
|
+
anyOf: [
|
|
529
|
+
frameOf("welcome", { me: looseObject, isSpectator: wireYesOrNo, players: wirePlayers, room: looseObject, spectatorCount: wireNumber, hostPlayerId: wireNullable("string"), awayChecks: looseObject, endsAt: wireNullable("string"), probeIntervalSec: wireNullable("number"), protocolVersionNumber: wireNumber, canChat: wireYesOrNo }),
|
|
530
|
+
frameOf("probe", { probeNumber: wireNumber, serverTimeMs: wireNumber, pingDurationMs: wireNullable("number") }),
|
|
531
|
+
frameOf("ending", { endsAt: wireText }),
|
|
532
|
+
frameOf("ending_cancelled"),
|
|
533
|
+
frameOf("version_update", { step: wireText, versionId: wireText, appliesAt: wireText, isReloading: wireYesOrNo }),
|
|
534
|
+
frameOf("players", { players: wirePlayers, hostPlayerId: wireNullable("string") }),
|
|
535
|
+
frameOf("away", { awayReason: wireText, seatHoldDurationSec: wireNumber }),
|
|
536
|
+
frameOf("kicked", { message: wireText, isKeptOut: wireYesOrNo }),
|
|
537
|
+
frameOf("room", { room: looseObject, spectatorCount: wireNumber }),
|
|
538
|
+
frameOf("move", { roomCode: wireText }),
|
|
539
|
+
frameOf("message", { data: WIRE_DATA, fromPlayerId: wireNullable("string") }),
|
|
540
|
+
frameOf("room_message", { roomMessage: looseObject }),
|
|
541
|
+
frameOf("state", { serverTimeMs: wireNumber }, { full: WIRE_DATA, changes: { type: "array" } }),
|
|
542
|
+
frameOf("blocks", { playerIds: { type: "array", items: wireText } }),
|
|
543
|
+
frameOf("friend_request"),
|
|
544
|
+
frameOf("chat", { fromPlayerId: wireText, text: wireText, hasSwearing: wireYesOrNo, hasSlur: wireYesOrNo, sentAt: wireText }),
|
|
545
|
+
frameOf("chat_sent", { chatNumber: wireNumber, sentAt: wireText }),
|
|
546
|
+
frameOf("chat_stopped", { chatNumber: wireNumber, reason: wireText, message: wireText }),
|
|
547
|
+
frameOf("error", { code: wireText, message: wireText }, { versionId: wireText, chatNumber: wireNumber }),
|
|
548
|
+
frameOf("ended", { message: wireText, endReason: wireText }),
|
|
549
|
+
],
|
|
550
|
+
};
|
|
551
|
+
/** The room's settings as a client may ask for them: tidyRoomInfo cuts a name and labels to their limits, and these are only the outer wall. */
|
|
552
|
+
const ASKED_ROOM = fieldsOf({
|
|
553
|
+
isListed: yesOrNo,
|
|
554
|
+
isOpen: yesOrNo,
|
|
555
|
+
friendJoinsEnabled: yesOrNo,
|
|
556
|
+
name: textUpTo(1000),
|
|
557
|
+
labels: gameData(4 * 1024),
|
|
558
|
+
});
|
|
559
|
+
const PLAYER_ID_SHAPE = textUpTo(80);
|
|
560
|
+
const IDLE_FIELDS = { idleTimeoutSec: realNumber(0, Number.MAX_SAFE_INTEGER), idleCheckEnabled: yesOrNo };
|
|
561
|
+
/** A connection's limit is a room's message bucket, which takes a token per player before any of these runs. */
|
|
562
|
+
const roomMessage = (whoMayCall, fields = {}, required = []) => ({
|
|
563
|
+
whoMayCall,
|
|
564
|
+
reaches: "room",
|
|
565
|
+
trusts: ["player_pass"],
|
|
566
|
+
takes: { query: {}, body: { kind: "json", fields: { type: textUpTo(40), ...fields }, required: ["type", ...required], maximumSizeBytes: ROOM_LIMITS.maximumGameMessageSizeBytes } },
|
|
567
|
+
answers: answersJson(ROOM_FRAME),
|
|
568
|
+
limit: { whyNoLimit: "room_token_bucket" },
|
|
569
|
+
});
|
|
509
570
|
/**
|
|
510
571
|
* Every message a room takes (docs/DOORS.md). receive's switch is over these names, so a new one without a row
|
|
511
572
|
* doesn't build. A spectator may send only what's for room members. hello comes first and proves the pass; chat is the host's to take before receive (readChatMessage);
|
|
512
573
|
* bytes is every binary message (receiveBytes). A type with no row, maybe from a newer SDK, is counted and ignored.
|
|
513
574
|
*/
|
|
514
575
|
export const ROOM_MESSAGE_DOORS = {
|
|
515
|
-
hello: {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
576
|
+
hello: {
|
|
577
|
+
whoMayCall: "pass_holder",
|
|
578
|
+
reaches: "room",
|
|
579
|
+
trusts: ["player_pass"],
|
|
580
|
+
provenBy: { source_path: "packages/game-server/src/pass.ts", symbol_name: "verifyPass" },
|
|
581
|
+
takes: {
|
|
582
|
+
query: {},
|
|
583
|
+
body: {
|
|
584
|
+
kind: "json",
|
|
585
|
+
fields: {
|
|
586
|
+
type: textUpTo(40),
|
|
587
|
+
protocolVersionNumber: realNumber(0, Number.MAX_SAFE_INTEGER),
|
|
588
|
+
pass: textUpTo(HELLO_PROOF_MAXIMUM_CHARACTER_COUNT),
|
|
589
|
+
playerToken: orNothing(textUpTo(HELLO_PROOF_MAXIMUM_CHARACTER_COUNT)),
|
|
590
|
+
joinData: gameData(ROOM_LIMITS.maximumJoinDataSizeBytes * 2),
|
|
591
|
+
room: ASKED_ROOM,
|
|
592
|
+
isSpectator: yesOrNo,
|
|
593
|
+
isQuickJoin: yesOrNo,
|
|
594
|
+
avoidBlockedPlayers: yesOrNo,
|
|
595
|
+
versionId: textUpTo(80),
|
|
596
|
+
playShare: textUpTo(400),
|
|
597
|
+
hasGraphicsChip: yesOrNo,
|
|
598
|
+
isInputSeen: yesOrNo,
|
|
599
|
+
...IDLE_FIELDS,
|
|
600
|
+
},
|
|
601
|
+
required: ["type"],
|
|
602
|
+
maximumSizeBytes: HELLO_MAXIMUM_SIZE_BYTES,
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
answers: answersJson(ROOM_FRAME),
|
|
606
|
+
limit: { level: "steady", counts: "player" },
|
|
607
|
+
},
|
|
608
|
+
probe_answer: roomMessage("room_member", { probeNumber: realNumber(0, Number.MAX_SAFE_INTEGER) }),
|
|
609
|
+
room: roomMessage("room_host", { room: ASKED_ROOM }, ["room"]),
|
|
610
|
+
kick: roomMessage("room_host", { playerId: PLAYER_ID_SHAPE, keepOut: yesOrNo }, ["playerId"]),
|
|
611
|
+
here: roomMessage("room_player"),
|
|
612
|
+
idle: roomMessage("room_player"),
|
|
613
|
+
hidden: roomMessage("room_player"),
|
|
614
|
+
shown: roomMessage("room_player"),
|
|
615
|
+
back: roomMessage("room_player", { joinData: gameData(ROOM_LIMITS.maximumJoinDataSizeBytes * 2), ...IDLE_FIELDS }),
|
|
616
|
+
message: roomMessage("room_player", { data: gameData(ROOM_LIMITS.maximumGameMessageSizeBytes) }),
|
|
617
|
+
bytes: {
|
|
618
|
+
whoMayCall: "room_player",
|
|
619
|
+
reaches: "room",
|
|
620
|
+
trusts: ["player_pass"],
|
|
621
|
+
takes: { query: {}, body: { kind: "bytes", maximumSizeBytes: ROOM_LIMITS.maximumGameMessageSizeBytes } },
|
|
622
|
+
answers: answersJson(ROOM_FRAME),
|
|
623
|
+
limit: { whyNoLimit: "room_token_bucket" },
|
|
624
|
+
},
|
|
625
|
+
chat: roomMessage("room_player", { chatNumber: realNumber(0, Number.MAX_SAFE_INTEGER), text: textUpTo(ROOM_LIMITS.maximumGameMessageSizeBytes), toPlayerIds: listOf(PLAYER_ID_SHAPE, 200) }, ["text"]),
|
|
626
|
+
block: roomMessage("room_player", { playerId: PLAYER_ID_SHAPE }, ["playerId"]),
|
|
627
|
+
unblock: roomMessage("room_player", { playerId: PLAYER_ID_SHAPE }, ["playerId"]),
|
|
628
|
+
friend_request: roomMessage("room_player", { playerId: PLAYER_ID_SHAPE }, ["playerId"]),
|
|
629
|
+
"zaisma:purchase": {
|
|
630
|
+
...roomMessage("room_player", { purchaseId: gameData(4096), receipt: gameData(HELLO_PROOF_MAXIMUM_CHARACTER_COUNT) }),
|
|
631
|
+
reaches: "purchase",
|
|
632
|
+
trusts: ["player_pass", "purchase_receipt"],
|
|
633
|
+
},
|
|
531
634
|
};
|
|
635
|
+
/**
|
|
636
|
+
* Whether a message breaks its door's shape: a field of the wrong type or past its bound, or one it needs missing. A
|
|
637
|
+
* name the door doesn't know is left alone and never read, unlike at other doors: a game's SDK comes from Zaisma and
|
|
638
|
+
* its server may be the developer's own, older one, so a newer SDK's field has to keep working there.
|
|
639
|
+
*/
|
|
640
|
+
export function roomMessageProblem(type, message) {
|
|
641
|
+
const body = ROOM_MESSAGE_DOORS[type].takes.body;
|
|
642
|
+
for (const name of body.required ?? [])
|
|
643
|
+
if (!(name in message))
|
|
644
|
+
return `${name} is missing`;
|
|
645
|
+
for (const [name, shape] of Object.entries(body.fields)) {
|
|
646
|
+
if (!(name in message))
|
|
647
|
+
continue;
|
|
648
|
+
const [problem] = schemaProblems(shape, message[name], name);
|
|
649
|
+
if (problem)
|
|
650
|
+
return problem;
|
|
651
|
+
}
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
532
654
|
const isRoomMessageType = (type) => typeof type === "string" && Object.hasOwn(ROOM_MESSAGE_DOORS, type);
|
|
533
655
|
const PLAYER_PROVEN = ["room_member", "room_player"];
|
|
534
656
|
const HOST_PROVEN = ["room_member", "room_player", "room_host"];
|
|
@@ -1792,6 +1914,8 @@ export class RoomCore {
|
|
|
1792
1914
|
const type = isRoomMessageType(message.type) ? message.type : null;
|
|
1793
1915
|
if (type && !checkDoor(ROOM_MESSAGE_DOORS[type], this.hostPlayerId === playerId ? HOST_PROVEN : PLAYER_PROVEN))
|
|
1794
1916
|
return "not_allowed";
|
|
1917
|
+
if (type && type !== "bytes" && roomMessageProblem(type, message))
|
|
1918
|
+
return "bad_message";
|
|
1795
1919
|
switch (type) {
|
|
1796
1920
|
case "room":
|
|
1797
1921
|
return this.hostChange(playerId, message);
|