@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/bench/brand/favicon-32.png +0 -0
- package/bench/brand/favicon.ico +0 -0
- package/bench/brand/favicon.svg +1 -1
- package/bench/glass.js +8955 -757
- package/bin/local-play-runner.js +27 -1
- package/bin/local-play.js +25 -0
- package/bin/node-parts.js +5 -1
- package/bin/node.d.ts +10 -1
- package/bin/node.js +167 -36
- package/package.json +1 -1
- package/sdk/zaisma.d.ts +80 -8
- package/sdk/zaisma.js +77 -18
- package/src/chat.d.ts +9 -4
- package/src/chat.js +20 -4
- package/src/index.d.ts +2 -1
- package/src/index.js +2 -1
- package/src/internal.d.ts +1 -0
- package/src/internal.js +1 -0
- package/src/outside-players.d.ts +65 -0
- package/src/outside-players.js +162 -0
- package/src/pass.d.ts +7 -2
- package/src/pass.js +1 -1
- package/src/requests.d.ts +45 -4
- package/src/requests.js +15 -8
- package/src/room-calls.d.ts +16 -0
- package/src/room-calls.js +31 -1
- package/src/room.d.ts +155 -4
- package/src/room.js +137 -4
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
|
|
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
|
-
|
|
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);
|