@zaisma/game-server 1.12.0 → 1.14.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/src/room.d.ts CHANGED
@@ -17,7 +17,7 @@ export interface Player {
17
17
  id: string;
18
18
  /** The account's name, or a guest's generated one like "Swift Fox 42". */
19
19
  name: string;
20
- /** A signed-in Zaisma account, or a guest. */
20
+ /** A signed-in Zaisma account, a guest, or a player your own server let in from outside Zaisma (outside_zaisma). */
21
21
  kind: PlayerKind;
22
22
  /**
23
23
  * The permanent items the account owns in this game, by item_name, from the pass it joined with (its
@@ -155,6 +155,27 @@ export interface Room {
155
155
  * turned away; they stay here.
156
156
  */
157
157
  moveTo(roomCode: string, moveOptions: MoveOptions): void;
158
+ /**
159
+ * Send a message to another live room of this game by its code, like telling a world that a party is on its way.
160
+ * Its game server's onRoomMessage takes it, or, without one, each of its players' games hears it as the room's
161
+ * "roomMessage" event. message is plain JSON, up to 16 KB. isDelivered is false when no such room is running.
162
+ * A room sends up to 20 room messages a second (ROOM_MESSAGE_LIMITS). On your own server it reaches the Node runner's own rooms.
163
+ */
164
+ sendToRoom(roomCode: string, message: unknown): Promise<{
165
+ isDelivered: boolean;
166
+ }>;
167
+ /**
168
+ * Send a message to every other live room of this game with someone in it, or only those holding every label in
169
+ * roomSelection.labels, like starting a live event everywhere. Each takes it as with sendToRoom; never this room.
170
+ * roomCount is how many took it. Up to 10 a minute for the whole game.
171
+ */
172
+ sendToEveryRoom(message: unknown, roomSelection?: RoomSelection): Promise<{
173
+ roomCount: number;
174
+ }>;
175
+ /** The game's live rooms, or only those holding every label in roomSelection.labels, newest first, up to 500 (ROOM_MESSAGE_LIMITS.maximumListedRoomCount). */
176
+ listRooms(roomSelection?: RoomSelection): Promise<{
177
+ rooms: LiveRoom[];
178
+ }>;
158
179
  /**
159
180
  * The game's own SQLite database, shared by all its rooms and kept between matches: for what the
160
181
  * game server owns or must trust, like inventories, leaderboards and bans. Plain SQL, awaited in
@@ -572,6 +593,14 @@ export interface GameServer<S = any> {
572
593
  * countdown or save. Never runs on your own server.
573
594
  */
574
595
  onVersionUpdate?(state: S, versionUpdate: VersionUpdate, room: Room): HandlerResult<S>;
596
+ /**
597
+ * A message for this room from another room of the game (room.sendToRoom, room.sendToEveryRoom), the game server
598
+ * outside rooms (server.sendToRoom in onRequest, onSchedule, onWebhook) or the developer (the API and MCP), like a
599
+ * live event starting or a moderator freezing a player. Your game decides what players see. Without this handler,
600
+ * each player's game hears it as the room's "roomMessage" event. Runs in a kept room with nobody in it too, when a
601
+ * message was sent to it by its code.
602
+ */
603
+ onRoomMessage?(state: S, roomMessage: RoomMessage, room: Room): HandlerResult<S>;
575
604
  }
576
605
  /**
577
606
  * What onVersionUpdate hears, as players' games do: the version the match moves to, when it switches at
@@ -582,6 +611,94 @@ export interface VersionUpdate {
582
611
  appliesAt: string;
583
612
  isReloading: boolean;
584
613
  }
614
+ /** Who sent a room message: another room's game server, the game server outside rooms (requests, schedules, webhooks), or the developer (the API and MCP, scheduled sends included). */
615
+ export type RoomMessageSenderKind = "room" | "server" | "developer";
616
+ /** Every RoomMessageSenderKind, in the order the docs give them. */
617
+ export declare const ROOM_MESSAGE_SENDER_KINDS: readonly ["room", "server", "developer"];
618
+ /**
619
+ * What onRoomMessage hears, and each player's game as the room's "roomMessage" event without it: the message as
620
+ * sent, who sent it, the code of the room that sent it (null unless senderKind is "room") and when (an ISO time).
621
+ */
622
+ export interface RoomMessage {
623
+ message: unknown;
624
+ senderKind: RoomMessageSenderKind;
625
+ fromRoomCode: string | null;
626
+ sentAt: string;
627
+ }
628
+ /** Which rooms a call means: those holding every label given, like { Map: "Docks" }; leave labels out for every room. */
629
+ export interface RoomSelection {
630
+ labels?: Record<string, string>;
631
+ }
632
+ /** One of the game's live rooms, as listRooms answers it. createdAt is an ISO time; continent is where it runs, like EU, or null. */
633
+ export interface LiveRoom {
634
+ roomCode: string;
635
+ name: string | null;
636
+ labels: Record<string, string>;
637
+ playerCount: number;
638
+ maximumPlayerCount: number;
639
+ spectatorCount: number;
640
+ isListed: boolean;
641
+ isOpen: boolean;
642
+ isKept: boolean;
643
+ versionId: string | null;
644
+ continent: string | null;
645
+ createdAt: string;
646
+ }
647
+ /** Someone in a live room, as server.getRoom answers: a player, or a spectator (isSpectator), with why they're away (awayReason) and when they joined (an ISO time). */
648
+ export interface RoomPlayer {
649
+ playerId: string;
650
+ name: string;
651
+ kind: PlayerKind;
652
+ isAway: boolean;
653
+ awayReason: AwayReason | null;
654
+ joinedAt: string;
655
+ isSpectator: boolean;
656
+ }
657
+ /** How rooms reach each other: what a room message may hold, and how many go where. */
658
+ export declare const ROOM_MESSAGE_LIMITS: {
659
+ /** A room message, as JSON, the same as a player's game message. */
660
+ readonly maximumRoomMessageSizeBytes: number;
661
+ /** Room messages one room's game server sends a second (room.sendToRoom, room.sendToEveryRoom). */
662
+ readonly maximumRoomSendsPerSec: 20;
663
+ /** Room messages one room takes a second; past it the sender hears it's busy (room_busy). */
664
+ readonly maximumReceivedRoomMessagesPerSec: 100;
665
+ /** Sends to every room a minute, for the whole game, from its game server, the API and the MCP together. */
666
+ readonly maximumEveryRoomSendsPerMin: 10;
667
+ /** Scheduled sends to every room waiting at once, a game. */
668
+ readonly maximumScheduledRoomMessageCount: 100;
669
+ /** How far ahead a send to every room can be scheduled. */
670
+ readonly maximumScheduleAheadDurationDays: 90;
671
+ /** Rooms one listRooms answers, newest first. */
672
+ readonly maximumListedRoomCount: 500;
673
+ };
674
+ /**
675
+ * How a room or the game server reaches the game's other live rooms, its calls already checked: Zaisma's rooms
676
+ * through the rooms Worker, the Node runner among its own rooms. Without it, the calls throw room_call_unavailable.
677
+ */
678
+ export interface RoomReach {
679
+ sendToRoom(toRoomCode: string, message: unknown): Promise<{
680
+ isDelivered: boolean;
681
+ }>;
682
+ sendToEveryRoom(message: unknown, labels: Record<string, string> | null): Promise<{
683
+ roomCount: number;
684
+ }>;
685
+ listRooms(labels: Record<string, string> | null): Promise<{
686
+ rooms: LiveRoom[];
687
+ }>;
688
+ }
689
+ /** What the game server reaches outside rooms besides RoomReach (server.getRoom, kickFromRoom, endRoom), its calls already checked. */
690
+ export interface ServerRoomReach extends RoomReach {
691
+ getRoom(roomCode: string): Promise<{
692
+ room: LiveRoom;
693
+ players: RoomPlayer[];
694
+ } | null>;
695
+ kickFromRoom(roomCode: string, playerId: string, message: string, isKeptOut: boolean): Promise<{
696
+ isDone: boolean;
697
+ }>;
698
+ endRoom(roomCode: string, message: string | undefined): Promise<{
699
+ isDone: boolean;
700
+ }>;
701
+ }
585
702
  /**
586
703
  * What onRestored hears: when the state it came back with was saved (an ISO time), how many seconds of
587
704
  * play since then are lost, and the version of the game that saved it (null on your own server), which
@@ -641,7 +758,7 @@ export declare function readJoinAnswer(answer: unknown): {
641
758
  mistake: GameServerError;
642
759
  };
643
760
  /** Everything a game server object may have, in the order the docs give them. Anything else is refused. */
644
- export declare const GAME_SERVER_OPTIONS: readonly ["maximumPlayerCount", "maximumSpectatorCount", "ticksPerSec", "hostControlsEnabled", "idleCheck", "idleTimeoutSec", "awayMode", "seatHoldDurationSec", "stateSyncEnabled", "adultChatFilterEnabled", "reconnectTimeoutSec", "maximumRoomCountPerPlayer", "stateSaveIntervalSec", "setup", "onJoin", "canJoin", "onChat", "onMoved", "onLeave", "onSpectatorJoin", "onSpectatorLeave", "onAway", "onBack", "onMessage", "onBytes", "onPurchase", "onRefund", "onTick", "onEnding", "onEndingCancelled", "onRestored", "stateView", "onVersionUpdate", "onRequest", "schedules", "onSchedule", "onWebhook"];
761
+ export declare const GAME_SERVER_OPTIONS: readonly ["maximumPlayerCount", "maximumSpectatorCount", "ticksPerSec", "hostControlsEnabled", "idleCheck", "idleTimeoutSec", "awayMode", "seatHoldDurationSec", "stateSyncEnabled", "adultChatFilterEnabled", "reconnectTimeoutSec", "maximumRoomCountPerPlayer", "stateSaveIntervalSec", "setup", "onJoin", "canJoin", "onChat", "onMoved", "onLeave", "onSpectatorJoin", "onSpectatorLeave", "onAway", "onBack", "onMessage", "onBytes", "onPurchase", "onRefund", "onTick", "onEnding", "onEndingCancelled", "onRestored", "stateView", "onVersionUpdate", "onRoomMessage", "onRequest", "schedules", "onSchedule", "onWebhook"];
645
762
  export declare const ROOM_LIMITS: {
646
763
  /** Players in one room: what one room carries at 60 ticks a second (test/load/room-load.test.ts). */
647
764
  readonly maximumPlayerCount: 100;
@@ -892,6 +1009,9 @@ export type ServerMessage = {
892
1009
  type: "message";
893
1010
  data: unknown;
894
1011
  fromPlayerId: string | null;
1012
+ } | {
1013
+ type: "room_message";
1014
+ roomMessage: RoomMessage;
895
1015
  } | {
896
1016
  type: "state";
897
1017
  serverTimeMs: number;
@@ -997,7 +1117,7 @@ export type ClientMessage = {
997
1117
  * the game became unavailable, its kept room was deleted, or no players were left for its spectators.
998
1118
  */
999
1119
  export type EndReason = "game" | "hosting_paused" | "game_server_stopped" | "game_unavailable" | "kept_room_deleted" | "no_players";
1000
- export type RoomErrorCode = "room_full" | "room_closed" | "ended" | "too_big" | "too_fast" | "bad_message" | "bad_pass" | "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";
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";
1001
1121
  export declare const ERROR_TEXT: Record<RoomErrorCode, string>;
1002
1122
  /** What players are told when Zaisma ends a match at room.endsAt. */
1003
1123
  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.";
@@ -1091,7 +1211,14 @@ export interface Transport {
1091
1211
  isBackedUp?(playerId: string): boolean;
1092
1212
  /** Where the room hands purchases over (onPurchase, onRefund). Without it, purchases never reach the room. */
1093
1213
  purchaseHandling?: PurchaseHandling;
1214
+ /** room.sendToRoom, sendToEveryRoom and listRooms, from this room. Without it, they throw room_call_unavailable. */
1215
+ roomReach?: RoomReach;
1094
1216
  }
1217
+ /**
1218
+ * The game server's server (onRequest, onSchedule, onWebhook) from what the runtime gives it: its calls to the game's
1219
+ * rooms checked as a room's are, then made through roomReach, or throwing room_call_unavailable without one.
1220
+ */
1221
+ export declare function newServer(serverParts: Pick<Server, "database" | "secrets" | "verifyReceipt">, roomReach?: ServerRoomReach): Server;
1095
1222
  /** room.secrets and server.secrets where a game has none. */
1096
1223
  export declare const NO_SECRETS: Readonly<Record<string, string>>;
1097
1224
  /** room.verifyReceipt and server.verifyReceipt where the runtime has no receipt keys. */
@@ -1123,6 +1250,12 @@ export interface PlayerChatSettings {
1123
1250
  whoCanChat: "nobody" | "friends" | "anyone";
1124
1251
  chatGroupId: string | null;
1125
1252
  }
1253
+ /**
1254
+ * The chat settings of a player your own server let in from outside Zaisma (outside_zaisma): they chat with
1255
+ * anyone who chats with anyone, in no chat group, and Zaisma's chat check stops any message between them and
1256
+ * an under-18 account. Zaisma's chat groups are chat_group_… IDs, so none is ever this one.
1257
+ */
1258
+ export declare const OUTSIDE_ZAISMA_CHAT_SETTINGS: PlayerChatSettings;
1126
1259
  /** A checked pass's chat settings, for RoomCore.setChatSettings: null for a guest. A setting this version doesn't know chats with nobody. */
1127
1260
  export declare function chatSettingsOf(claims: PassClaims): PlayerChatSettings | null;
1128
1261
  /** A chat message from a client as readChatMessage found it, for startChat to check. */
@@ -1306,6 +1439,10 @@ export declare class RoomCore<S = any> {
1306
1439
  private setupData;
1307
1440
  /** When this room last reserved rooms, for the limit. */
1308
1441
  private reservedAtMs;
1442
+ /** When this room sent room messages, within the last second (ROOM_MESSAGE_LIMITS.maximumRoomSendsPerSec). */
1443
+ private roomSentAtMs;
1444
+ /** When this room took room messages, within the last second (ROOM_MESSAGE_LIMITS.maximumReceivedRoomMessagesPerSec). */
1445
+ private roomReceivedAtMs;
1309
1446
  private buckets;
1310
1447
  /** Probes this room has sent, to every player: each probe's number. */
1311
1448
  private probeCount;
@@ -1400,7 +1537,9 @@ export declare class RoomCore<S = any> {
1400
1537
  canChat(playerId: string): boolean;
1401
1538
  /**
1402
1539
  * Whether one player's chat may reach another, canChat's rule on passes (pass.ts): two accounts in the
1403
- * same chat group with no block either way, friends unless both chat with anyone.
1540
+ * same chat group with no block either way, friends unless both chat with anyone. A player from outside
1541
+ * Zaisma is in no chat group: they chat with anyone who chats with anyone, and Zaisma's chat check stops
1542
+ * what would reach an under-18 account or come from one.
1404
1543
  */
1405
1544
  private mayChat;
1406
1545
  /** Who a player's chat may reach: the players in the match (or those of toPlayerIds in it), never spectators, kept only where they may chat. */
@@ -1638,6 +1777,18 @@ export declare class RoomCore<S = any> {
1638
1777
  moved(toRoomCode: string, answer: MoveAnswer, sent: Player[]): void;
1639
1778
  /** Reserving rooms and moving players share one limit a minute. */
1640
1779
  private limit;
1780
+ /** The transport's way to the game's other rooms, for a call that needs it: throws room_call_unavailable without it, or once the room ended. */
1781
+ private roomReachFor;
1782
+ /** Room messages this room sends share one limit a second. */
1783
+ private limitRoomSends;
1784
+ /**
1785
+ * A room message for this room (room.sendToRoom, sendToEveryRoom, server.sendToRoom, the API): the game server's
1786
+ * onRoomMessage takes it, or, without one, every player and spectator hears it. False when the room ended, or took
1787
+ * ROOM_MESSAGE_LIMITS.maximumReceivedRoomMessagesPerSec in the last second already (the sender hears room_busy).
1788
+ */
1789
+ receiveRoomMessage(roomMessage: RoomMessage): boolean;
1790
+ /** Who's in the room, players then spectators, as server.getRoom answers: joinedAtMsOf says when each joined (the transport keeps it). */
1791
+ roomPlayers(joinedAtMsOf: (playerId: string) => number): RoomPlayer[];
1641
1792
  /** Remove a player (room.kick). Their onLeave runs once the handler that kicked them is done. */
1642
1793
  kick(playerId: string, kickOptions?: KickOptions): void;
1643
1794
  /** Keep players out, newest last, up to KEPT_OUT_PLAYER_COUNT: well past a full room, so nobody kicked for the match is let back by the cap. */
package/src/room.js CHANGED
@@ -15,6 +15,25 @@ import { newStateTracker } from "./state-sync.js";
15
15
  function shownPlayer(player) {
16
16
  return { id: player.id, name: player.name, kind: player.kind };
17
17
  }
18
+ /** Every RoomMessageSenderKind, in the order the docs give them. */
19
+ export const ROOM_MESSAGE_SENDER_KINDS = ["room", "server", "developer"];
20
+ /** How rooms reach each other: what a room message may hold, and how many go where. */
21
+ export const ROOM_MESSAGE_LIMITS = {
22
+ /** A room message, as JSON, the same as a player's game message. */
23
+ maximumRoomMessageSizeBytes: 16 * 1024,
24
+ /** Room messages one room's game server sends a second (room.sendToRoom, room.sendToEveryRoom). */
25
+ maximumRoomSendsPerSec: 20,
26
+ /** Room messages one room takes a second; past it the sender hears it's busy (room_busy). */
27
+ maximumReceivedRoomMessagesPerSec: 100,
28
+ /** Sends to every room a minute, for the whole game, from its game server, the API and the MCP together. */
29
+ maximumEveryRoomSendsPerMin: 10,
30
+ /** Scheduled sends to every room waiting at once, a game. */
31
+ maximumScheduledRoomMessageCount: 100,
32
+ /** How far ahead a send to every room can be scheduled. */
33
+ maximumScheduleAheadDurationDays: 90,
34
+ /** Rooms one listRooms answers, newest first. */
35
+ maximumListedRoomCount: 500,
36
+ };
18
37
  /**
19
38
  * A restored room's restoreInfo, from when its state was saved and the version that saved it. A state
20
39
  * saved as the room emptied (isSavedEmpty) lost nothing, however long ago: a kept world waking after a week
@@ -98,6 +117,7 @@ export const GAME_SERVER_OPTIONS = [
98
117
  "onRestored",
99
118
  "stateView",
100
119
  "onVersionUpdate",
120
+ "onRoomMessage",
101
121
  "onRequest",
102
122
  "schedules",
103
123
  "onSchedule",
@@ -212,7 +232,7 @@ export class GameServerError extends Error {
212
232
  }
213
233
  }
214
234
  /** The checks every room call goes through, here and in a game's sandbox (room-calls.ts). */
215
- const checks = roomCallChecks(GameServerError, ROOM_LIMITS);
235
+ const checks = roomCallChecks(GameServerError, { ...ROOM_LIMITS, ...ROOM_MESSAGE_LIMITS });
216
236
  /** The error for an option a game server doesn't take, naming the ones it does. */
217
237
  export function unknownOptionError(field) {
218
238
  return new GameServerError("unknown_option", field, `${field} isn't a game server option.`, `A game server takes: ${GAME_SERVER_OPTIONS.join(", ")}.`);
@@ -292,6 +312,7 @@ export const ERROR_TEXT = {
292
312
  too_fast: "Too many messages. Slow down.",
293
313
  bad_message: "The room couldn't read or use that message.",
294
314
  bad_pass: "Your player pass isn't valid for this game.",
315
+ bad_player_token: "This server doesn't know this player. Sign in again.",
295
316
  no_hello: "Send your player pass first.",
296
317
  not_allowed: "Only the game server or the room's host can do that.",
297
318
  game_unavailable: "This game isn't available right now.",
@@ -324,6 +345,49 @@ const MAXIMUM_PURCHASE_ID_CHARACTER_COUNT = 80;
324
345
  const MAXIMUM_TOLD_PURCHASE_COUNT = 200;
325
346
  /** How long a room holds a seat for a player another room is sending. */
326
347
  const SEAT_MS = 60_000;
348
+ /**
349
+ * The game server's server (onRequest, onSchedule, onWebhook) from what the runtime gives it: its calls to the game's
350
+ * rooms checked as a room's are, then made through roomReach, or throwing room_call_unavailable without one.
351
+ */
352
+ export function newServer(serverParts, roomReach) {
353
+ const reachFor = (call) => {
354
+ if (!roomReach)
355
+ throw new GameServerError("room_call_unavailable", call, `${call} isn't available here.`, "Reaching rooms needs Zaisma's rooms or the Node runner.");
356
+ return roomReach;
357
+ };
358
+ const roomCodeOf = (roomCode, call) => {
359
+ if (typeof roomCode !== "string" || !isRoomCode(roomCode))
360
+ throw new GameServerError("bad_room_call", call, `${call} needs a room code.`, "Give the code of a live room of this game, like one server.listRooms answered.");
361
+ return roomCode;
362
+ };
363
+ return {
364
+ database: serverParts.database,
365
+ secrets: serverParts.secrets,
366
+ verifyReceipt: serverParts.verifyReceipt,
367
+ sendToRoom: (roomCode, message) => {
368
+ const reach = reachFor("server.sendToRoom");
369
+ const checked = checks.checkSendToRoom(roomCode, message, null, "server.sendToRoom");
370
+ return reach.sendToRoom(checked.toRoomCode, checked.message);
371
+ },
372
+ sendToEveryRoom: (message, roomSelection) => {
373
+ const reach = reachFor("server.sendToEveryRoom");
374
+ const checked = checks.checkSendToEveryRoom(message, roomSelection, "server.sendToEveryRoom");
375
+ return reach.sendToEveryRoom(checked.message, checked.labels);
376
+ },
377
+ listRooms: (roomSelection) => reachFor("server.listRooms").listRooms(checks.checkRoomSelection(roomSelection, "server.listRooms").labels),
378
+ getRoom: (roomCode) => reachFor("server.getRoom").getRoom(roomCodeOf(roomCode, "server.getRoom")),
379
+ kickFromRoom: (roomCode, playerId, kickOptions) => {
380
+ const reach = reachFor("server.kickFromRoom");
381
+ const code = roomCodeOf(roomCode, "server.kickFromRoom");
382
+ const checked = checks.checkKick(playerId, kickOptions);
383
+ return reach.kickFromRoom(code, checked.playerId, checked.message, checked.isKeptOut);
384
+ },
385
+ endRoom: (roomCode, message) => {
386
+ const reach = reachFor("server.endRoom");
387
+ return reach.endRoom(roomCodeOf(roomCode, "server.endRoom"), checks.checkEnd(message));
388
+ },
389
+ };
390
+ }
327
391
  /** room.secrets and server.secrets where a game has none. */
328
392
  export const NO_SECRETS = Object.freeze({});
329
393
  /** room.verifyReceipt and server.verifyReceipt where the runtime has no receipt keys. */
@@ -348,6 +412,12 @@ export function checkRoomData(given, maximumSizeBytes) {
348
412
  export function errorMessage(code, message, details) {
349
413
  return JSON.stringify({ type: "error", code, message: message || ERROR_TEXT[code], ...details });
350
414
  }
415
+ /**
416
+ * The chat settings of a player your own server let in from outside Zaisma (outside_zaisma): they chat with
417
+ * anyone who chats with anyone, in no chat group, and Zaisma's chat check stops any message between them and
418
+ * an under-18 account. Zaisma's chat groups are chat_group_… IDs, so none is ever this one.
419
+ */
420
+ export const OUTSIDE_ZAISMA_CHAT_SETTINGS = { whoCanChat: "anyone", chatGroupId: "outside_zaisma" };
351
421
  /** A checked pass's chat settings, for RoomCore.setChatSettings: null for a guest. A setting this version doesn't know chats with nobody. */
352
422
  export function chatSettingsOf(claims) {
353
423
  if (claims.kind !== "account")
@@ -552,6 +622,10 @@ export class RoomCore {
552
622
  setupData = null;
553
623
  /** When this room last reserved rooms, for the limit. */
554
624
  reservedAtMs = [];
625
+ /** When this room sent room messages, within the last second (ROOM_MESSAGE_LIMITS.maximumRoomSendsPerSec). */
626
+ roomSentAtMs = [];
627
+ /** When this room took room messages, within the last second (ROOM_MESSAGE_LIMITS.maximumReceivedRoomMessagesPerSec). */
628
+ roomReceivedAtMs = [];
555
629
  buckets = new Map();
556
630
  /** Probes this room has sent, to every player: each probe's number. */
557
631
  probeCount = 0;
@@ -773,6 +847,19 @@ export class RoomCore {
773
847
  this.kick(checked.playerId, { message: checked.message, keepOut: checked.isKeptOut });
774
848
  },
775
849
  moveTo: (toRoomCode, moveOptions) => this.moveTo(toRoomCode, moveOptions),
850
+ sendToRoom: (toRoomCode, message) => {
851
+ const reach = this.roomReachFor("room.sendToRoom");
852
+ const checked = checks.checkSendToRoom(toRoomCode, message, transport.roomCode, "room.sendToRoom");
853
+ this.limitRoomSends("room.sendToRoom");
854
+ return reach.sendToRoom(checked.toRoomCode, checked.message);
855
+ },
856
+ sendToEveryRoom: (message, roomSelection) => {
857
+ const reach = this.roomReachFor("room.sendToEveryRoom");
858
+ const checked = checks.checkSendToEveryRoom(message, roomSelection, "room.sendToEveryRoom");
859
+ this.limitRoomSends("room.sendToEveryRoom");
860
+ return reach.sendToEveryRoom(checked.message, checked.labels);
861
+ },
862
+ listRooms: (roomSelection) => this.roomReachFor("room.listRooms").listRooms(checks.checkRoomSelection(roomSelection, "room.listRooms").labels),
776
863
  database: transport.database ?? NO_DATABASE,
777
864
  secrets: transport.secrets ?? NO_SECRETS,
778
865
  verifyReceipt: transport.verifyReceipt ?? NO_RECEIPT_CHECK,
@@ -891,14 +978,17 @@ export class RoomCore {
891
978
  }
892
979
  /**
893
980
  * Whether one player's chat may reach another, canChat's rule on passes (pass.ts): two accounts in the
894
- * same chat group with no block either way, friends unless both chat with anyone.
981
+ * same chat group with no block either way, friends unless both chat with anyone. A player from outside
982
+ * Zaisma is in no chat group: they chat with anyone who chats with anyone, and Zaisma's chat check stops
983
+ * what would reach an under-18 account or come from one.
895
984
  */
896
985
  mayChat(senderPlayerId, receiverPlayerId) {
897
986
  if (senderPlayerId === receiverPlayerId || !this.canChat(senderPlayerId) || !this.canChat(receiverPlayerId) || this.hasBlockBetween(senderPlayerId, receiverPlayerId))
898
987
  return false;
899
988
  const sender = this.chatSettings.get(senderPlayerId);
900
989
  const receiver = this.chatSettings.get(receiverPlayerId);
901
- if (sender.chatGroupId !== receiver.chatGroupId)
990
+ const isOutside = (settings) => settings.chatGroupId === OUTSIDE_ZAISMA_CHAT_SETTINGS.chatGroupId;
991
+ if (sender.chatGroupId !== receiver.chatGroupId && !isOutside(sender) && !isOutside(receiver))
902
992
  return false;
903
993
  return (sender.whoCanChat === "anyone" && receiver.whoCanChat === "anyone") || this.areFriends(senderPlayerId, receiverPlayerId);
904
994
  }
@@ -1958,6 +2048,49 @@ export class RoomCore {
1958
2048
  throw new GameServerError("room_call_limit", call, `A room can reserve rooms or move players up to ${ROOM_LIMITS.maximumReservesPerMin} times a minute.`, "Reserve one room for a whole group, or wait a minute.");
1959
2049
  this.reservedAtMs.push(nowMs);
1960
2050
  }
2051
+ /** The transport's way to the game's other rooms, for a call that needs it: throws room_call_unavailable without it, or once the room ended. */
2052
+ roomReachFor(call) {
2053
+ if (!this.transport.roomReach)
2054
+ throw new GameServerError("room_call_unavailable", call, `${call} isn't available here.`, "Reaching other rooms needs Zaisma's rooms or the Node runner.");
2055
+ if (this.ended)
2056
+ throw new GameServerError("room_call_unavailable", call, `${call} was called after this room ended.`, "Check room.endsAt, and stop making room calls once you've called room.end().");
2057
+ return this.transport.roomReach;
2058
+ }
2059
+ /** Room messages this room sends share one limit a second. */
2060
+ limitRoomSends(call) {
2061
+ const nowMs = this.now();
2062
+ this.roomSentAtMs = this.roomSentAtMs.filter((sentAtMs) => nowMs - sentAtMs < 1000);
2063
+ if (this.roomSentAtMs.length >= ROOM_MESSAGE_LIMITS.maximumRoomSendsPerSec)
2064
+ throw new GameServerError("room_call_limit", call, `A room can send up to ${ROOM_MESSAGE_LIMITS.maximumRoomSendsPerSec} room messages a second.`, "Gather what you send into fewer messages, or send to every room at once with room.sendToEveryRoom.");
2065
+ this.roomSentAtMs.push(nowMs);
2066
+ }
2067
+ /**
2068
+ * A room message for this room (room.sendToRoom, sendToEveryRoom, server.sendToRoom, the API): the game server's
2069
+ * onRoomMessage takes it, or, without one, every player and spectator hears it. False when the room ended, or took
2070
+ * ROOM_MESSAGE_LIMITS.maximumReceivedRoomMessagesPerSec in the last second already (the sender hears room_busy).
2071
+ */
2072
+ receiveRoomMessage(roomMessage) {
2073
+ if (this.ended)
2074
+ return false;
2075
+ const nowMs = this.now();
2076
+ this.roomReceivedAtMs = this.roomReceivedAtMs.filter((receivedAtMs) => nowMs - receivedAtMs < 1000);
2077
+ if (this.roomReceivedAtMs.length >= ROOM_MESSAGE_LIMITS.maximumReceivedRoomMessagesPerSec)
2078
+ return false;
2079
+ this.roomReceivedAtMs.push(nowMs);
2080
+ if (this.server.onRoomMessage)
2081
+ this.run(() => this.server.onRoomMessage(this.state, { ...roomMessage }, this.api), "onRoomMessage");
2082
+ else
2083
+ this.sendEach(this.everyone(), JSON.stringify({ type: "room_message", roomMessage }));
2084
+ return true;
2085
+ }
2086
+ /** Who's in the room, players then spectators, as server.getRoom answers: joinedAtMsOf says when each joined (the transport keeps it). */
2087
+ roomPlayers(joinedAtMsOf) {
2088
+ const roomPlayer = (player, isSpectator) => {
2089
+ const awayReason = isSpectator ? null : (this.kept.get(player.id) ?? null);
2090
+ return { playerId: player.id, name: player.name, kind: player.kind, isAway: awayReason !== null, awayReason, joinedAt: new Date(joinedAtMsOf(player.id)).toISOString(), isSpectator };
2091
+ };
2092
+ return [...[...this.players.values()].map((player) => roomPlayer(player, false)), ...[...this.spectators.values()].map((spectator) => roomPlayer(spectator, true))];
2093
+ }
1961
2094
  /** Remove a player (room.kick). Their onLeave runs once the handler that kicked them is done. */
1962
2095
  kick(playerId, kickOptions) {
1963
2096
  const player = this.players.get(playerId);
@@ -2602,7 +2735,7 @@ export class RoomCore {
2602
2735
  }
2603
2736
  }
2604
2737
  /** The calls that change the room or send from it: not for canJoin or stateView, which only decide and answer. */
2605
- const CHANGING_CALLS = ["broadcast", "send", "broadcastBytes", "sendBytes", "end", "setListed", "setOpen", "setFriendJoinsEnabled", "setKept", "setInfo", "reserve", "kick", "moveTo", "applyVersionUpdate"];
2738
+ const CHANGING_CALLS = ["broadcast", "send", "broadcastBytes", "sendBytes", "end", "setListed", "setOpen", "setFriendJoinsEnabled", "setKept", "setInfo", "reserve", "kick", "moveTo", "sendToRoom", "sendToEveryRoom", "applyVersionUpdate"];
2606
2739
  /** A room that reads like the room but throws room_call_unavailable for every call that changes it, for the handler named. */
2607
2740
  function readOnlyRoom(room, handlerName) {
2608
2741
  const view = Object.create(room);