@zaisma/game-server 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +35 -1
- package/bin/node-parts.js +311 -0
- package/bin/node.d.ts +43 -0
- package/bin/node.js +615 -0
- package/package.json +42 -1
- package/src/chat.d.ts +65 -0
- package/src/chat.js +108 -0
- package/src/database.d.ts +150 -0
- package/src/database.js +244 -0
- package/src/dump.d.ts +61 -0
- package/src/dump.js +282 -0
- package/src/index.d.ts +10 -0
- package/src/index.js +12 -0
- package/src/internal.d.ts +13 -0
- package/src/internal.js +15 -0
- package/src/names.d.ts +4 -0
- package/src/names.js +18 -0
- package/src/outside-calls.d.ts +46 -0
- package/src/outside-calls.js +30 -0
- package/src/pass.d.ts +194 -0
- package/src/pass.js +231 -0
- package/src/relay.d.ts +7 -0
- package/src/relay.js +20 -0
- package/src/requests.d.ts +89 -0
- package/src/requests.js +127 -0
- package/src/room-calls.d.ts +92 -0
- package/src/room-calls.js +194 -0
- package/src/room.d.ts +1462 -0
- package/src/room.js +2192 -0
- package/src/schedules.d.ts +82 -0
- package/src/schedules.js +200 -0
- package/src/state-sync.d.ts +57 -0
- package/src/state-sync.js +175 -0
- package/src/webhooks.d.ts +112 -0
- package/src/webhooks.js +102 -0
package/src/room.d.ts
ADDED
|
@@ -0,0 +1,1462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The game server API and the room logic shared by Zaisma rooms (Durable Objects)
|
|
3
|
+
* and the Node runner, so a game behaves the same in both places.
|
|
4
|
+
*
|
|
5
|
+
* Words: the *client* is the game in the player's browser. The *game server* is the
|
|
6
|
+
* developer's code that holds the official game state. A *room* is one live match,
|
|
7
|
+
* running one copy of the game server. Names follow docs/NAMING.md.
|
|
8
|
+
*/
|
|
9
|
+
import { type GameDatabase } from "./database.ts";
|
|
10
|
+
import type { PlayerKind, ReceiptClaims } from "./pass.ts";
|
|
11
|
+
import type { Server } from "./requests.ts";
|
|
12
|
+
import { type Schedule, type ScheduledRun } from "./schedules.ts";
|
|
13
|
+
import { type StateChange } from "./state-sync.ts";
|
|
14
|
+
import type { WebhookEvent } from "./webhooks.ts";
|
|
15
|
+
export interface Player {
|
|
16
|
+
/** An account's player ID is the same in every game; a guest's lasts only for their pass. */
|
|
17
|
+
id: string;
|
|
18
|
+
/** The account's name, or a guest's generated one like "Swift Fox 42". */
|
|
19
|
+
name: string;
|
|
20
|
+
/** A signed-in Zaisma account, or a guest. */
|
|
21
|
+
kind: PlayerKind;
|
|
22
|
+
/**
|
|
23
|
+
* The permanent items the account owns in this game, by item_name, from the pass it joined with (its
|
|
24
|
+
* owned_item_names): check one with player.ownedItemNames?.includes("skin_red-dragon"). Absent for guests
|
|
25
|
+
* and when the player owns nothing here. Only the game server sees it; other players' games never do.
|
|
26
|
+
*/
|
|
27
|
+
ownedItemNames?: string[];
|
|
28
|
+
}
|
|
29
|
+
export interface Room {
|
|
30
|
+
/**
|
|
31
|
+
* This room's code, like K7Q2MX: the code players join it by. Use it to treat rooms differently, like
|
|
32
|
+
* keeping only your one shared world (setKept in "WRLD22", canJoin refusing elsewhere).
|
|
33
|
+
*/
|
|
34
|
+
readonly roomCode: string;
|
|
35
|
+
/** Players in the room right now, in join order. */
|
|
36
|
+
readonly players: readonly Player[];
|
|
37
|
+
/**
|
|
38
|
+
* Spectators watching the room (maximumSpectatorCount), in join order: they get what the room sends
|
|
39
|
+
* everyone, send nothing and don't count as playing. Never in players.
|
|
40
|
+
*/
|
|
41
|
+
readonly spectators: readonly Player[];
|
|
42
|
+
/**
|
|
43
|
+
* Send a message to every player and spectator (audience), or everyone but one (exceptPlayerId).
|
|
44
|
+
* delivery "latest_only" lets a player whose connection is backed up skip it for a newer one (SendOptions).
|
|
45
|
+
*/
|
|
46
|
+
broadcast(message: unknown, broadcastOptions?: BroadcastOptions): void;
|
|
47
|
+
/** Send a message to one player or spectator. */
|
|
48
|
+
send(playerId: string, message: unknown, sendOptions?: SendOptions): void;
|
|
49
|
+
/**
|
|
50
|
+
* Send bytes to every player, or everyone but one (exceptPlayerId), like a compact snapshot of the
|
|
51
|
+
* world. Each player's game gets them as the "bytes" event, with fromPlayerId or null.
|
|
52
|
+
*/
|
|
53
|
+
broadcastBytes(bytes: Uint8Array, broadcastOptions?: BroadcastOptions): void;
|
|
54
|
+
/** Send bytes to one player or spectator. */
|
|
55
|
+
sendBytes(playerId: string, bytes: Uint8Array, sendOptions?: SendOptions): void;
|
|
56
|
+
/** End the match: everyone is told why (message, with endReason "game") and disconnected, and the room closes. */
|
|
57
|
+
end(message?: string): void;
|
|
58
|
+
/**
|
|
59
|
+
* When Zaisma will end this match, an ISO time, or null. Set once the game's hosting on Zaisma rooms
|
|
60
|
+
* pauses for the month (its free allowance, or its developer's budget or credit, is used), an hour after
|
|
61
|
+
* the pause, so your game can save and tell its players (onEnding runs then); never set on your own server.
|
|
62
|
+
*/
|
|
63
|
+
readonly endsAt: string | null;
|
|
64
|
+
/** Shown in room lists (the game page and Zaisma.rooms.list). A hidden room still works by code and link. */
|
|
65
|
+
readonly isListed: boolean;
|
|
66
|
+
setListed(isListed: boolean): void;
|
|
67
|
+
/** Taking new players. A closed room lets back only players who were already in it. */
|
|
68
|
+
readonly isOpen: boolean;
|
|
69
|
+
setOpen(isOpen: boolean): void;
|
|
70
|
+
/**
|
|
71
|
+
* Whether Zaisma's Join offers this room: a player's friends on zaisma.com, and Zaisma.friends. Off
|
|
72
|
+
* shows their Join grayed as "Private room". It isn't a lock: a code or link still reaches the room,
|
|
73
|
+
* so close it (setOpen) or answer false from canJoin to keep players out.
|
|
74
|
+
*/
|
|
75
|
+
readonly friendJoinsEnabled: boolean;
|
|
76
|
+
setFriendJoinsEnabled(isEnabled: boolean): void;
|
|
77
|
+
/**
|
|
78
|
+
* A kept room doesn't end when everyone leaves: it sleeps with its state saved and wakes when someone
|
|
79
|
+
* joins, for worlds that last. Its saved state counts against the game's storage. room.end() ends it
|
|
80
|
+
* for good. setKept(false) makes it an ordinary room again, whose state goes a day after it empties.
|
|
81
|
+
*/
|
|
82
|
+
readonly isKept: boolean;
|
|
83
|
+
setKept(isKept: boolean): void;
|
|
84
|
+
/** The room's name, like "Ana's castle", or null. Up to 40 characters. */
|
|
85
|
+
readonly name: string | null;
|
|
86
|
+
/** Up to 4 labels players can filter by, like { Mode: "Classic", Map: "Docks" }. */
|
|
87
|
+
readonly labels: Readonly<Record<string, string>>;
|
|
88
|
+
/**
|
|
89
|
+
* Name and label the room, from the game or from what a player typed. Names and labels a player
|
|
90
|
+
* typed show on zaisma.com only if they pass Zaisma's text filter; your game is responsible for
|
|
91
|
+
* them inside the game. Leave a field out to keep it; null clears it.
|
|
92
|
+
*/
|
|
93
|
+
setInfo(nameAndLabels: RoomInfo): void;
|
|
94
|
+
/**
|
|
95
|
+
* Make a new room for some of this room's players, like a lobby starting a match, and move them
|
|
96
|
+
* there. The new room runs this same game server: its setup(setupData) gets your setupData, only
|
|
97
|
+
* the players named can join it (a closed room, until it opens), and it's hidden from room lists
|
|
98
|
+
* unless you list it. Each player gets a "move" event in the game and joins when the game says so.
|
|
99
|
+
* Returns the new room's code. setupData is plain JSON, up to 4 KB; players can read it but not change it.
|
|
100
|
+
*/
|
|
101
|
+
reserve(reserveOptions: ReserveOptions): string;
|
|
102
|
+
/**
|
|
103
|
+
* Remove a player from the room, telling them why (message, up to 200 characters; your game's
|
|
104
|
+
* words, so keep them within the content rules). keepOut keeps them out of this room for the rest
|
|
105
|
+
* of the match, and out of any room it reserves after. onLeave runs once the current handler is done.
|
|
106
|
+
*/
|
|
107
|
+
kick(playerId: string, kickOptions?: KickOptions): void;
|
|
108
|
+
/**
|
|
109
|
+
* Send some of this room's players to another room of this game that already exists, like back to
|
|
110
|
+
* their party's match or into the next area of an open world. The other room's canJoin decides with
|
|
111
|
+
* your joinData (plain JSON, up to 4 KB) and holds a seat for each player it takes for a minute,
|
|
112
|
+
* even if it's closed to new players; its onJoin gets the same joinData. Each player taken gets a
|
|
113
|
+
* "move" event in the game, which follows by default. onMoved then says who was taken and who was
|
|
114
|
+
* turned away; they stay here.
|
|
115
|
+
*/
|
|
116
|
+
moveTo(roomCode: string, moveOptions: MoveOptions): void;
|
|
117
|
+
/**
|
|
118
|
+
* The game's own SQLite database, shared by all its rooms and kept between matches: for what the
|
|
119
|
+
* game server owns or must trust, like inventories, leaderboards and bans. Plain SQL, awaited in
|
|
120
|
+
* any handler (which then runs as async). In Zaisma rooms it's part of the game's hosting; on your
|
|
121
|
+
* own server the Node runner keeps it in a local file (--database), so the same code runs in both.
|
|
122
|
+
*/
|
|
123
|
+
readonly database: GameDatabase;
|
|
124
|
+
/**
|
|
125
|
+
* The game's secrets, by name, like room.secrets.STRIPE_SECRET_KEY: set per game on Zaisma
|
|
126
|
+
* (set_secret) and never shown again, or the environment variables on your own server. Calls to
|
|
127
|
+
* other servers are plain fetch(); on Zaisma they reach the hosts the game lists in
|
|
128
|
+
* game_server_allowed_hosts, and anything else throws an OutsideCallError.
|
|
129
|
+
*/
|
|
130
|
+
readonly secrets: Readonly<Record<string, string>>;
|
|
131
|
+
/**
|
|
132
|
+
* Check a purchase's receipt, as the game sent it, before delivering: its claims, or an error named
|
|
133
|
+
* ReceiptError with a code (wrong_game, bad_signature, ...); check error.name, not instanceof, since in
|
|
134
|
+
* Zaisma rooms it isn't your copy's class. The runtime fills in Zaisma's published keys and this game, in
|
|
135
|
+
* Zaisma rooms and on your own server alike. Any other error means the check couldn't run just now (keys
|
|
136
|
+
* that didn't load, a time-out): try again later, never treat the receipt as refused. Deliver once per purchase_id, and only when purchase_status
|
|
137
|
+
* is completed.
|
|
138
|
+
*/
|
|
139
|
+
verifyReceipt(receipt: string): Promise<ReceiptClaims>;
|
|
140
|
+
/**
|
|
141
|
+
* Whether two players in the room have a block between them, either way (never which one made it):
|
|
142
|
+
* from their Zaisma accounts, or a block a player just made in the game. A block is a wall both ways,
|
|
143
|
+
* so don't deliver chat, voice or emotes from one to the other, and keep them apart in your own
|
|
144
|
+
* matchmaking when you can, but never keep either one out: who plays is your call. Each player's game
|
|
145
|
+
* hides the other's emotes and phrases by itself (room.isMuted).
|
|
146
|
+
*/
|
|
147
|
+
hasBlockBetween(firstPlayerId: string, secondPlayerId: string): boolean;
|
|
148
|
+
/**
|
|
149
|
+
* Whether two players in the room are Zaisma friends: both accounts, as their friendship was when the
|
|
150
|
+
* later of them joined. Use it for friends-only chat, teams or a "friend" mark; guests are never
|
|
151
|
+
* anyone's friend. It never says who else a player's friends are.
|
|
152
|
+
*/
|
|
153
|
+
areFriends(firstPlayerId: string, secondPlayerId: string): boolean;
|
|
154
|
+
/**
|
|
155
|
+
* A player's round trip to the room in milliseconds, smoothed, or null for a player who isn't in the
|
|
156
|
+
* room or hasn't been measured yet (the room probes each player right after they join). The player's
|
|
157
|
+
* game reads the same number as room.pingDurationMs. The room measures it itself, so a player can only
|
|
158
|
+
* make theirs look worse by answering late, never better: cap any lag compensation that uses it.
|
|
159
|
+
*/
|
|
160
|
+
pingDurationMs(playerId: string): number | null;
|
|
161
|
+
/**
|
|
162
|
+
* The room's clock now, in epoch milliseconds, like Date.now(). Players' games estimate the same
|
|
163
|
+
* clock with room.serverTimeMs(), so a time the game server sends, like when a race starts, means
|
|
164
|
+
* the same moment for everyone.
|
|
165
|
+
*/
|
|
166
|
+
serverTimeMs(): number;
|
|
167
|
+
/**
|
|
168
|
+
* Why a player in the room isn't playing, or null while they play: "idle" or "hidden" (your onAway
|
|
169
|
+
* kept them), or "lost" (their connection dropped and they have reconnectTimeoutSec to come back).
|
|
170
|
+
* Every player's game sees the same in its player list (awayReason).
|
|
171
|
+
*/
|
|
172
|
+
awayReason(playerId: string): AwayReason | null;
|
|
173
|
+
/**
|
|
174
|
+
* Switch to the new version now, with onVersionUpdate: at a good moment, like the end of a round. The
|
|
175
|
+
* room saves, every player's game reconnects (reloading at the new version when its files changed) and
|
|
176
|
+
* the new version's onRestored carries on from the state. Does nothing when no update is coming, and on
|
|
177
|
+
* your own server.
|
|
178
|
+
*/
|
|
179
|
+
applyVersionUpdate(): void;
|
|
180
|
+
}
|
|
181
|
+
/** Who a broadcast reaches: players and spectators (everyone), only the players, or only the spectators. */
|
|
182
|
+
export type Audience = "everyone" | "players" | "spectators";
|
|
183
|
+
/**
|
|
184
|
+
* How a message goes: "reliable" (the default) reaches every player in order. "latest_only" may be
|
|
185
|
+
* dropped: a player whose connection is backed up skips older ones and gets the newest once they catch
|
|
186
|
+
* up, like positions sent every tick. In Zaisma rooms it rides the WebSocket today, and moves to real
|
|
187
|
+
* datagrams the day Cloudflare carries them, with no change to your game.
|
|
188
|
+
*/
|
|
189
|
+
export type Delivery = "reliable" | "latest_only";
|
|
190
|
+
export interface SendOptions {
|
|
191
|
+
delivery?: Delivery;
|
|
192
|
+
/**
|
|
193
|
+
* For "latest_only", and needed there: which messages replace each other while a player is backed
|
|
194
|
+
* up, up to 40 characters, like "positions".
|
|
195
|
+
*/
|
|
196
|
+
latestKey?: string;
|
|
197
|
+
/**
|
|
198
|
+
* The player this message or these bytes are from, when you pass one player's on; each player's game
|
|
199
|
+
* hears it with the message (or bytes), and null when it's left out: from the game server. The game
|
|
200
|
+
* server vouches for it.
|
|
201
|
+
*/
|
|
202
|
+
fromPlayerId?: string;
|
|
203
|
+
}
|
|
204
|
+
export interface BroadcastOptions extends SendOptions {
|
|
205
|
+
/** A player who doesn't get it, like the one whose move this is. */
|
|
206
|
+
exceptPlayerId?: string;
|
|
207
|
+
/** Who gets it: "everyone" (the default: players and spectators), "players" or "spectators". */
|
|
208
|
+
audience?: Audience;
|
|
209
|
+
}
|
|
210
|
+
export interface MoveOptions {
|
|
211
|
+
playerIds: string[];
|
|
212
|
+
joinData?: unknown;
|
|
213
|
+
}
|
|
214
|
+
/** A player another room turned away (room.moveTo), with why: an error code and its message. */
|
|
215
|
+
export interface Refusal {
|
|
216
|
+
player: Player;
|
|
217
|
+
code: RoomErrorCode;
|
|
218
|
+
message: string;
|
|
219
|
+
}
|
|
220
|
+
/** Who another room took (room.moveTo) and who it turned away. */
|
|
221
|
+
export interface MoveResult {
|
|
222
|
+
roomCode: string;
|
|
223
|
+
movedPlayers: Player[];
|
|
224
|
+
refusals: Refusal[];
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* How a player came in: fromRoomCode, the code of the room that sent them (room.moveTo), or null.
|
|
228
|
+
* isReturning: they're coming back to the seat the room held for them, after walking away or losing their connection.
|
|
229
|
+
* isSpectator: they come to watch (joinRoom({ isSpectator: true })), not to play.
|
|
230
|
+
*/
|
|
231
|
+
export interface JoinInfo {
|
|
232
|
+
fromRoomCode: string | null;
|
|
233
|
+
isReturning: boolean;
|
|
234
|
+
isSpectator: boolean;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Why a player isn't playing: no input (idle), the game hidden in a real-time room, or lost: their
|
|
238
|
+
* connection dropped, and the room keeps them for reconnectTimeoutSec.
|
|
239
|
+
*/
|
|
240
|
+
export type AwayReason = "idle" | "hidden" | "lost";
|
|
241
|
+
/** A player as players' games see them: who they are, never what they own (that's the game server's, from the pass). */
|
|
242
|
+
export type ShownPlayer = Omit<Player, "ownedItemNames">;
|
|
243
|
+
/** A player as the room lists them to every player's game: awayReason says why they aren't playing, or null while they play. */
|
|
244
|
+
export interface ListedPlayer extends ShownPlayer {
|
|
245
|
+
awayReason: AwayReason | null;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Why a player left: left (closed the game or left the room), lost (their connection dropped and they
|
|
249
|
+
* weren't back within reconnectTimeoutSec, and the room holds their seat), idle or hidden (they walked
|
|
250
|
+
* away, and the room holds their seat), kicked.
|
|
251
|
+
*/
|
|
252
|
+
export type LeaveReason = "left" | AwayReason | "kicked";
|
|
253
|
+
export interface LeaveInfo {
|
|
254
|
+
reason: LeaveReason;
|
|
255
|
+
}
|
|
256
|
+
/** What onAway hears: why the player isn't playing. */
|
|
257
|
+
export interface AwayInfo {
|
|
258
|
+
reason: AwayReason;
|
|
259
|
+
}
|
|
260
|
+
/** What one room asks another when moving players there; fromRoomCode is the asking room's, added by the transport. */
|
|
261
|
+
export interface MoveAsk {
|
|
262
|
+
fromRoomCode: string;
|
|
263
|
+
players: Player[];
|
|
264
|
+
joinData: unknown;
|
|
265
|
+
}
|
|
266
|
+
/** The other room's answer to a MoveAsk: who it took, and who it turned away and why. */
|
|
267
|
+
export interface MoveAnswer {
|
|
268
|
+
movedPlayerIds: string[];
|
|
269
|
+
refusals: {
|
|
270
|
+
playerId: string;
|
|
271
|
+
code: RoomErrorCode;
|
|
272
|
+
message: string;
|
|
273
|
+
}[];
|
|
274
|
+
}
|
|
275
|
+
export interface KickOptions {
|
|
276
|
+
message?: string;
|
|
277
|
+
keepOut?: boolean;
|
|
278
|
+
}
|
|
279
|
+
/** A room made for some players: who, the data its game server starts with, and how it shows. */
|
|
280
|
+
export interface ReserveOptions extends RoomInfo {
|
|
281
|
+
playerIds: string[];
|
|
282
|
+
setupData?: unknown;
|
|
283
|
+
isListed?: boolean;
|
|
284
|
+
/** false: friends can't Join the new room from Zaisma ("Private room"). */
|
|
285
|
+
friendJoinsEnabled?: boolean;
|
|
286
|
+
/** true: the new room is kept (room.setKept). */
|
|
287
|
+
isKept?: boolean;
|
|
288
|
+
}
|
|
289
|
+
/** How a reserved room starts, as the room that made it sends it on. */
|
|
290
|
+
export interface RoomStart extends Pick<RoomSettings, "isListed" | "name" | "labels">, Partial<Pick<RoomSettings, "friendJoinsEnabled">> {
|
|
291
|
+
playerIds: string[];
|
|
292
|
+
setupData: unknown;
|
|
293
|
+
/** Players kept out of the room that made this one, and why: they stay out of this one too. */
|
|
294
|
+
keptOut?: Record<string, string>;
|
|
295
|
+
/** The code of the room that made this one, where awayMode "lobby" sends players. Added by the transport. */
|
|
296
|
+
lobbyRoomCode?: string;
|
|
297
|
+
/** Kept (room.setKept) from the start. */
|
|
298
|
+
isKept?: boolean;
|
|
299
|
+
}
|
|
300
|
+
/** A room's name and labels, as a game sets them. */
|
|
301
|
+
export interface RoomInfo {
|
|
302
|
+
name?: string | null;
|
|
303
|
+
labels?: Record<string, string | number> | null;
|
|
304
|
+
}
|
|
305
|
+
/** Who checks for players who give no input: the room (server), each player's game (client), or nobody (off). */
|
|
306
|
+
export type IdleCheck = "server" | "client" | "off";
|
|
307
|
+
/** What happens to a player who walks away: their seat is held (hold), or they're sent to the room that made this one (lobby). */
|
|
308
|
+
export type AwayMode = "hold" | "lobby";
|
|
309
|
+
/**
|
|
310
|
+
* A handler answers the new state (a plain object or an array), or nothing to keep it; an async handler
|
|
311
|
+
* (one that awaits room.database) answers it later. Any other answer, like a number or null, is a mistake:
|
|
312
|
+
* the state stays and the game's logs say bad_handler_answer.
|
|
313
|
+
*/
|
|
314
|
+
export type HandlerResult<S> = void | S | Promise<void | S>;
|
|
315
|
+
export interface GameServer<S = any> {
|
|
316
|
+
/** Up to 100. Defaults to 8. */
|
|
317
|
+
maximumPlayerCount?: number;
|
|
318
|
+
/**
|
|
319
|
+
* Spectators a room takes, up to 100: they watch a match without playing (joinRoom({ isSpectator: true })),
|
|
320
|
+
* get what the room sends everyone, send nothing, don't count as playing or toward maximumPlayerCount,
|
|
321
|
+
* and are free (off the player-hour meter). Leave it out, or 0, and nobody can watch.
|
|
322
|
+
*/
|
|
323
|
+
maximumSpectatorCount?: number;
|
|
324
|
+
/** Ticks a second, up to 60. Leave out for turn-based games so the room can sleep between moves. */
|
|
325
|
+
ticksPerSec?: number;
|
|
326
|
+
/**
|
|
327
|
+
* The room's first player is its host: they may hide, show, open, close and name the room, and
|
|
328
|
+
* kick players, from the game. When the host leaves, the next player takes over. The relay has this on.
|
|
329
|
+
*/
|
|
330
|
+
hostControlsEnabled?: boolean;
|
|
331
|
+
/**
|
|
332
|
+
* Who checks for players who give no input. "server" (the default): after idleTimeoutSec, the
|
|
333
|
+
* player's game asks "Still there?", and 30 seconds later they walk away. "client": each player's
|
|
334
|
+
* game decides (joinRoom({ idleTimeoutSec, idleCheckEnabled })), as the relay does. "off": nobody.
|
|
335
|
+
*/
|
|
336
|
+
idleCheck?: IdleCheck;
|
|
337
|
+
/** Seconds without input before "Still there?", 30 to 86400. Defaults to 600 (10 minutes). */
|
|
338
|
+
idleTimeoutSec?: number;
|
|
339
|
+
/**
|
|
340
|
+
* What happens when a player walks away (idle, or the game hidden for 2 minutes in a real-time
|
|
341
|
+
* room). "hold" (the default) takes them out of the match (onLeave) and holds their seat for
|
|
342
|
+
* seatHoldDurationSec, so they come straight back to it. "lobby" sends them to the room that made
|
|
343
|
+
* this one (room.reserve), holding their seat if there isn't one or it won't take them. Give onAway
|
|
344
|
+
* to handle it yourself instead.
|
|
345
|
+
*/
|
|
346
|
+
awayMode?: AwayMode;
|
|
347
|
+
/**
|
|
348
|
+
* How long the room holds a seat for a player who walked away or lost their connection, in
|
|
349
|
+
* seconds: up to 1800, 0 for none. Defaults to 300. Held seats count toward maximumPlayerCount.
|
|
350
|
+
*/
|
|
351
|
+
seatHoldDurationSec?: number;
|
|
352
|
+
/**
|
|
353
|
+
* How long a player whose connection dropped stays in the match as away (reason "lost") before they
|
|
354
|
+
* leave it, in seconds: 0 to 300, 0 to take them out at once. Defaults to 30. Your onAway and onBack
|
|
355
|
+
* run, so you can freeze their character; held seats come after (seatHoldDurationSec).
|
|
356
|
+
*/
|
|
357
|
+
reconnectTimeoutSec?: number;
|
|
358
|
+
/**
|
|
359
|
+
* Send each player the state, then only what changes: a full copy when they join (and after their
|
|
360
|
+
* connection was backed up), then the changes after each tick, or after each handler in a turn-based
|
|
361
|
+
* game. Their game reads it as room.state, or room.smoothedState() to move evenly between updates.
|
|
362
|
+
* Off by default. The state goes as JSON, so keep it plain; cut what each player sees with stateView.
|
|
363
|
+
*/
|
|
364
|
+
stateSyncEnabled?: boolean;
|
|
365
|
+
/**
|
|
366
|
+
* How often a real-time room saves its state, in seconds: 1 to 60, default 5. A restarted room comes
|
|
367
|
+
* back with the last saved state (onRestored). Turn-based rooms save after every change and ignore it.
|
|
368
|
+
*/
|
|
369
|
+
stateSaveIntervalSec?: number;
|
|
370
|
+
/**
|
|
371
|
+
* The starting state. It must be plain JSON so a sleeping room can be saved and woken. setupData
|
|
372
|
+
* is what room.reserve() passed when another room made this one, and null otherwise. Rooms need it;
|
|
373
|
+
* a game server that only answers requests (onRequest) or runs on schedules can leave it out.
|
|
374
|
+
*/
|
|
375
|
+
setup?(setupData: unknown): S;
|
|
376
|
+
/**
|
|
377
|
+
* A request from the player's game outside any room (Zaisma.server.request(request)), like a
|
|
378
|
+
* leaderboard, a shop or a daily reward in a single-player game. Answer what the game gets back:
|
|
379
|
+
* plain JSON, up to 1 MB, within 10 seconds. request is plain JSON from the player's browser, up
|
|
380
|
+
* to 16 KB, so check it like any message; player is who sent it, from their player pass. There's no
|
|
381
|
+
* room and no state: keep what lasts in server.database; server.secrets has the game's secrets. If it throws, the game gets
|
|
382
|
+
* error.code "game_server_error" and you see the error with the game server's others.
|
|
383
|
+
*/
|
|
384
|
+
onRequest?(player: Player, request: unknown, server: Server): unknown;
|
|
385
|
+
/**
|
|
386
|
+
* When the game server runs by itself, outside rooms and requests: daily resets, a weekly
|
|
387
|
+
* leaderboard, a persistent world's turns. Up to 10, each a name and a cron in UTC (minute hour
|
|
388
|
+
* day-of-month month day-of-week), so at most once a minute: [{ name: "daily-reset", cron: "0 0 * * *" }].
|
|
389
|
+
*/
|
|
390
|
+
schedules?: Schedule[];
|
|
391
|
+
/**
|
|
392
|
+
* A schedule is due: run.name says which, run.scheduledAt the minute it was due (an ISO time, UTC).
|
|
393
|
+
* There's no room, no player and no state: keep what lasts in server.database; server.secrets has the game's secrets. It has 30 seconds.
|
|
394
|
+
* A run can rarely come twice or a little late (after a restart), so make it safe to repeat, like
|
|
395
|
+
* a reset that checks run.scheduledAt against the last one it did. If it throws, you see the
|
|
396
|
+
* error with the game server's others.
|
|
397
|
+
*/
|
|
398
|
+
onSchedule?(run: ScheduledRun, server: Server): unknown;
|
|
399
|
+
/**
|
|
400
|
+
* An event from one of the developer's webhooks: a webhook whose destination is this game's game
|
|
401
|
+
* server (create_webhook with destination_kind "game_server"), or, on your own server, one whose
|
|
402
|
+
* endpoint_url is this server's /v1/webhooks. event is what any webhook is sent: event.kind, and
|
|
403
|
+
* event.game, event.version and event.bug_report as the developer API answers them
|
|
404
|
+
* (event.account_deletion for account_deleted: Zaisma erases the player's rows itself unless the game
|
|
405
|
+
* turned that off). Turn it into a
|
|
406
|
+
* Discord post or a GitHub issue with fetch() and the tokens in server.secrets. There's no room, no
|
|
407
|
+
* player and no state. Finishing without an error counts as delivered; throwing (or taking over 10
|
|
408
|
+
* seconds) tries it again later, up to 6 times, so make it safe to repeat: event.id is the same on
|
|
409
|
+
* every try. Each event counts as one of the game's requests.
|
|
410
|
+
*/
|
|
411
|
+
onWebhook?(event: WebhookEvent, server: Server): unknown;
|
|
412
|
+
/**
|
|
413
|
+
* joinData is what the player's game passed to joinRoom({ joinData }), like the character they
|
|
414
|
+
* picked, or null. It comes from the player's browser, so check it like any message. For a player
|
|
415
|
+
* another room sent (room.moveTo), joinData is what that room's game server passed, and
|
|
416
|
+
* joinInfo.fromRoomCode is its code.
|
|
417
|
+
*/
|
|
418
|
+
onJoin?(state: S, player: Player, room: Room, joinData: unknown, joinInfo: JoinInfo): HandlerResult<S>;
|
|
419
|
+
/**
|
|
420
|
+
* Say who may come in, before they join: answer true to let them in, false to turn them
|
|
421
|
+
* away, or { refusalMessage } to turn them away with a message they see (up to 200 characters), like
|
|
422
|
+
* a wrong room password in joinData or a full team. Answering nothing, or anything else, is a mistake
|
|
423
|
+
* and turns them away too, logged as a game server error (bad_handler_answer). Players coming back after a dropped connection don't ask
|
|
424
|
+
* again. Only decide here; change the state in onJoin. If it throws, the player is turned away.
|
|
425
|
+
* Players another room sends are asked when it sends them, with its joinData and
|
|
426
|
+
* joinInfo.fromRoomCode, and not again when they arrive.
|
|
427
|
+
*/
|
|
428
|
+
canJoin?(state: S, player: Player, room: Room, joinData: unknown, joinInfo: JoinInfo): JoinAnswer | Promise<JoinAnswer>;
|
|
429
|
+
/** After room.moveTo: who the other room took, and who it turned away and why. */
|
|
430
|
+
onMoved?(state: S, moveResult: MoveResult, room: Room): HandlerResult<S>;
|
|
431
|
+
/**
|
|
432
|
+
* leaveInfo.reason says why they left; a player who lost their connection or walked away gets their
|
|
433
|
+
* seat held. A dropped connection ("lost") first comes to onAway, and only here if they aren't back
|
|
434
|
+
* within reconnectTimeoutSec.
|
|
435
|
+
*/
|
|
436
|
+
onLeave?(state: S, player: Player, room: Room, leaveInfo: LeaveInfo): HandlerResult<S>;
|
|
437
|
+
/** A spectator came to watch: send them what they need to follow the match, like the whole state once. */
|
|
438
|
+
onSpectatorJoin?(state: S, spectator: Player, room: Room): HandlerResult<S>;
|
|
439
|
+
/** A spectator stopped watching (left, lost their connection or was kicked). No seat is held. */
|
|
440
|
+
onSpectatorLeave?(state: S, spectator: Player, room: Room): HandlerResult<S>;
|
|
441
|
+
/**
|
|
442
|
+
* A player walked away (idle, hidden) or their connection dropped (lost). For idle and hidden, with
|
|
443
|
+
* onAway they stay in the room and your game decides what happens: kick them, move them
|
|
444
|
+
* (room.moveTo) or keep them. For lost, every game hears it, and the room decides when time's up:
|
|
445
|
+
* they stay for reconnectTimeoutSec, then leave (onLeave, reason "lost"); freeze their character
|
|
446
|
+
* meanwhile. They don't count as playing while away, and a room where everyone is away stops
|
|
447
|
+
* ticking. onBack runs when they return: send them what they missed.
|
|
448
|
+
*/
|
|
449
|
+
onAway?(state: S, player: Player, room: Room, awayInfo: AwayInfo): HandlerResult<S>;
|
|
450
|
+
onBack?(state: S, player: Player, room: Room): HandlerResult<S>;
|
|
451
|
+
onMessage?(state: S, player: Player, room: Room, message: unknown): HandlerResult<S>;
|
|
452
|
+
/**
|
|
453
|
+
* Bytes a player's game sent (room.sendBytes), like a compact input, under the same limits as
|
|
454
|
+
* messages: up to 16 KB each, 30 a second. Read them with a DataView; they're a copy, yours to keep.
|
|
455
|
+
*/
|
|
456
|
+
onBytes?(state: S, player: Player, room: Room, bytes: Uint8Array): HandlerResult<S>;
|
|
457
|
+
/** tickDurationSec: seconds since the last tick, about 1 / ticksPerSec (more when a tick was skipped). */
|
|
458
|
+
onTick?(state: S, tickDurationSec: number, room: Room): HandlerResult<S>;
|
|
459
|
+
/**
|
|
460
|
+
* Zaisma will end this match at room.endsAt, because the game's hosting paused for the month: save
|
|
461
|
+
* what should outlive it and tell your players. Runs once; each player's game also gets
|
|
462
|
+
* the "ending" event. Never runs on your own server.
|
|
463
|
+
*/
|
|
464
|
+
onEnding?(state: S, room: Room): HandlerResult<S>;
|
|
465
|
+
/**
|
|
466
|
+
* The room restarted (Zaisma restarts its rooms on every update, and your own server may restart) and
|
|
467
|
+
* came back with the state it last saved: restoreInfo.savedAt says when, and restoreInfo.lostDurationSec
|
|
468
|
+
* how many seconds of play since then are lost, so you can resync clocks or tell your players. Runs
|
|
469
|
+
* once, instead of setup. The players who were in the match come back as away (reason "lost"): onAway
|
|
470
|
+
* runs for each after this, and onBack as each reconnects, or onLeave if they aren't back within
|
|
471
|
+
* reconnectTimeoutSec.
|
|
472
|
+
*/
|
|
473
|
+
onRestored?(state: S, restoreInfo: RestoreInfo, room: Room): HandlerResult<S>;
|
|
474
|
+
/**
|
|
475
|
+
* With stateSyncEnabled: what this player sees of the state, like only what's near them in a big
|
|
476
|
+
* world, or without the other team's hidden cards. Leave it out and everyone sees all of it. Answer
|
|
477
|
+
* plain JSON, a new object or parts of the state; don't change the state here. Keep it cheap: it runs
|
|
478
|
+
* for every player on every tick. viewer is the player it's for.
|
|
479
|
+
*/
|
|
480
|
+
stateView?(state: S, viewer: Player, room: Room): unknown;
|
|
481
|
+
/**
|
|
482
|
+
* The developer published a version with match_update_mode "now", and this match moves to it. With
|
|
483
|
+
* this handler the room waits for you: call room.applyVersionUpdate() at a good moment, like the end of
|
|
484
|
+
* a round, or it switches at versionUpdate.appliesAt, 5 minutes on. Without it, it switches 10 seconds
|
|
485
|
+
* after the notice. Each player's game hears it too (the room's "versionUpdate" event), to show a
|
|
486
|
+
* countdown or save. Never runs on your own server.
|
|
487
|
+
*/
|
|
488
|
+
onVersionUpdate?(state: S, versionUpdate: VersionUpdate, room: Room): HandlerResult<S>;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* What onVersionUpdate hears, as players' games do: the version the match moves to, when it switches at
|
|
492
|
+
* the latest (an ISO time), and whether the game's files changed, so players' games reload.
|
|
493
|
+
*/
|
|
494
|
+
export interface VersionUpdate {
|
|
495
|
+
versionId: string;
|
|
496
|
+
appliesAt: string;
|
|
497
|
+
isReloading: boolean;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* What onRestored hears: when the state it came back with was saved (an ISO time), how many seconds of
|
|
501
|
+
* play since then are lost, and the version of the game that saved it (null on your own server), which
|
|
502
|
+
* differs from the one running after an update, so the game can bring an older state up to date.
|
|
503
|
+
*/
|
|
504
|
+
export interface RestoreInfo {
|
|
505
|
+
savedAt: string;
|
|
506
|
+
lostDurationSec: number;
|
|
507
|
+
savedVersionId: string | null;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* A restored room's restoreInfo, from when its state was saved and the version that saved it. A state
|
|
511
|
+
* saved as the room emptied (isSavedEmpty) lost nothing, however long ago: a kept world waking after a week
|
|
512
|
+
* reads savedAt for the time that passed.
|
|
513
|
+
*/
|
|
514
|
+
export declare function restoreInfoOf(savedAtMs: number, nowMs?: number, savedVersionId?: string | null, isSavedEmpty?: boolean): RestoreInfo;
|
|
515
|
+
/**
|
|
516
|
+
* What canJoin answers: true lets the player in; false turns them away; { refusalMessage } turns them
|
|
517
|
+
* away with that message. Answering nothing turns them away too, and is logged as a mistake.
|
|
518
|
+
*/
|
|
519
|
+
export type JoinAnswer = boolean | {
|
|
520
|
+
refusalMessage: string;
|
|
521
|
+
};
|
|
522
|
+
/**
|
|
523
|
+
* canJoin's answer as the room acts on it: let in, or turn away with a refusalMessage (null for the
|
|
524
|
+
* usual one). Answering nothing or anything else is a mistake (bad_handler_answer), described for the game server's errors.
|
|
525
|
+
*/
|
|
526
|
+
export declare function readJoinAnswer(answer: unknown): {
|
|
527
|
+
isAllowed: true;
|
|
528
|
+
} | {
|
|
529
|
+
isAllowed: false;
|
|
530
|
+
refusalMessage: string | null;
|
|
531
|
+
} | {
|
|
532
|
+
mistake: GameServerError;
|
|
533
|
+
};
|
|
534
|
+
/** Everything a game server object may have, in the order the docs give them. Anything else is refused. */
|
|
535
|
+
export declare const GAME_SERVER_OPTIONS: readonly ["maximumPlayerCount", "maximumSpectatorCount", "ticksPerSec", "hostControlsEnabled", "idleCheck", "idleTimeoutSec", "awayMode", "seatHoldDurationSec", "stateSyncEnabled", "reconnectTimeoutSec", "stateSaveIntervalSec", "setup", "onJoin", "canJoin", "onMoved", "onLeave", "onSpectatorJoin", "onSpectatorLeave", "onAway", "onBack", "onMessage", "onBytes", "onTick", "onEnding", "onRestored", "stateView", "onVersionUpdate", "onRequest", "schedules", "onSchedule", "onWebhook"];
|
|
536
|
+
export declare const ROOM_LIMITS: {
|
|
537
|
+
/** Players in one room: what one room carries at 60 ticks a second (test/load/room-load.test.ts). */
|
|
538
|
+
readonly maximumPlayerCount: 100;
|
|
539
|
+
readonly defaultMaximumPlayerCount: 8;
|
|
540
|
+
/** Spectators in one room: each send costs the room time, so its audience is bounded like its players. Bigger audiences are a stream. */
|
|
541
|
+
readonly maximumSpectatorCount: 100;
|
|
542
|
+
readonly maximumTicksPerSec: 60;
|
|
543
|
+
/** A room's name, its labels, and each label's name and value. */
|
|
544
|
+
readonly maximumNameCharacterCount: 40;
|
|
545
|
+
readonly maximumLabelCount: 4;
|
|
546
|
+
readonly maximumLabelNameCharacterCount: 16;
|
|
547
|
+
readonly maximumLabelValueCharacterCount: 24;
|
|
548
|
+
/** Join data a player brings to a room, and setup data a reserved room starts with, as JSON. */
|
|
549
|
+
readonly maximumJoinDataSizeBytes: number;
|
|
550
|
+
readonly maximumSetupDataSizeBytes: number;
|
|
551
|
+
/** Rooms one room may reserve or move players to a minute: enough for a full lobby, not for a bug that does it every tick. */
|
|
552
|
+
readonly maximumReservesPerMin: 20;
|
|
553
|
+
/** A kick's message, and a refusal's (canJoin's refusalMessage). */
|
|
554
|
+
readonly maximumKickMessageCharacterCount: 200;
|
|
555
|
+
readonly maximumRefusalMessageCharacterCount: 200;
|
|
556
|
+
/** Largest game message (or bytes) a client may send. */
|
|
557
|
+
readonly maximumGameMessageSizeBytes: number;
|
|
558
|
+
/**
|
|
559
|
+
* Sustained game messages (and bytes) per second per player, with a short burst allowance. A game at
|
|
560
|
+
* 60 ticks sends two inputs in one message: incoming messages are what a room is billed for.
|
|
561
|
+
*/
|
|
562
|
+
readonly maximumGameMessagesPerSec: 30;
|
|
563
|
+
readonly maximumGameMessageBurstCount: 60;
|
|
564
|
+
/** The idle check's default and range (idleTimeoutSec, joinRoom({ idleTimeoutSec })). */
|
|
565
|
+
readonly defaultIdleTimeoutSec: 600;
|
|
566
|
+
readonly minimumIdleTimeoutSec: 30;
|
|
567
|
+
readonly maximumIdleTimeoutSec: number;
|
|
568
|
+
/** How long a game hidden in a real-time room counts before its player is away. */
|
|
569
|
+
readonly hiddenTimeoutSec: 120;
|
|
570
|
+
/** How long a seat is held by default, and at most (seatHoldDurationSec). */
|
|
571
|
+
readonly defaultSeatHoldDurationSec: 300;
|
|
572
|
+
readonly maximumSeatHoldDurationSec: 1800;
|
|
573
|
+
/** How long a player whose connection dropped stays in the match by default, and at most (reconnectTimeoutSec). */
|
|
574
|
+
readonly defaultReconnectTimeoutSec: 30;
|
|
575
|
+
readonly maximumReconnectTimeoutSec: 300;
|
|
576
|
+
/** How often a real-time room saves its state by default, and the range (stateSaveIntervalSec). */
|
|
577
|
+
readonly defaultStateSaveIntervalSec: 5;
|
|
578
|
+
readonly minimumStateSaveIntervalSec: 1;
|
|
579
|
+
readonly maximumStateSaveIntervalSec: 60;
|
|
580
|
+
/** A match moving to a version published "now": its notice without onVersionUpdate, and the longest onVersionUpdate can hold it. */
|
|
581
|
+
readonly defaultVersionUpdateDelaySec: 10;
|
|
582
|
+
readonly maximumVersionUpdateDelaySec: 300;
|
|
583
|
+
/** A real-time room's state bigger than this as JSON isn't saved, and a restart starts it fresh. */
|
|
584
|
+
readonly maximumSavedStateSizeBytes: number;
|
|
585
|
+
/** A player's view of the state as JSON (stateSyncEnabled): bigger views aren't sent, and the game's errors say so once. */
|
|
586
|
+
readonly maximumStateViewSizeBytes: number;
|
|
587
|
+
/** A latest-only message's latestKey. */
|
|
588
|
+
readonly maximumLatestKeyCharacterCount: 40;
|
|
589
|
+
/** Latest-only messages waiting for a backed-up player, one per latestKey; past it, the oldest is dropped. */
|
|
590
|
+
readonly maximumLatestOnlySlotCount: 64;
|
|
591
|
+
};
|
|
592
|
+
/**
|
|
593
|
+
* How rooms watch connections: when they probe, and when a player counts as backed up or dropped. Tuning,
|
|
594
|
+
* not contract: unlike ROOM_LIMITS, these may change in any version.
|
|
595
|
+
*/
|
|
596
|
+
export declare const ROOM_TUNING: {
|
|
597
|
+
/** How often a ticking room probes each player, for their ping and the room's clock. */
|
|
598
|
+
readonly probeIntervalSec: 2;
|
|
599
|
+
/** A probe left unanswered this long drops the player: their connection stopped or fell too far behind. */
|
|
600
|
+
readonly probeAnswerTimeoutSec: 8;
|
|
601
|
+
/** A player is also probed each time this much has gone out to them since their last probe. */
|
|
602
|
+
readonly probeSentSizeBytes: number;
|
|
603
|
+
/** Bytes sent to a player since the last probe they answered; past it, they're dropped. */
|
|
604
|
+
readonly maximumUnansweredSentSizeBytes: number;
|
|
605
|
+
/** Unanswered probes the room remembers per player; the oldest is forgotten past it. */
|
|
606
|
+
readonly maximumUnansweredProbeCount: 16;
|
|
607
|
+
/**
|
|
608
|
+
* A player is backed up with this much sent and not yet through: on your own server, the socket's
|
|
609
|
+
* bufferedAmount; in Zaisma rooms, sent since the last probe they answered, while a probe of theirs is
|
|
610
|
+
* backedUpDelayMs later than their ping. State sync skips them, and latest-only messages wait for them in slots.
|
|
611
|
+
*/
|
|
612
|
+
readonly backedUpSizeBytes: number;
|
|
613
|
+
readonly backedUpDelayMs: 250;
|
|
614
|
+
};
|
|
615
|
+
/** The room protocol's version: hello and welcome say it, so either side can tell what the other speaks. */
|
|
616
|
+
export declare const PROTOCOL_VERSION_NUMBER = 1;
|
|
617
|
+
/**
|
|
618
|
+
* What a GameServerError's code says: unknown_option and bad_option (a game server object that breaks
|
|
619
|
+
* the rules), bad_room_call (a room call's arguments are wrong), room_call_limit (too many reserves a
|
|
620
|
+
* minute, or room calls in one go), room_call_unavailable (that call can't run here or now) and
|
|
621
|
+
* bad_handler_answer (a handler answered something that isn't a state, or canJoin answered nothing).
|
|
622
|
+
*/
|
|
623
|
+
export type GameServerErrorCode = "unknown_option" | "bad_option" | "bad_room_call" | "room_call_limit" | "room_call_unavailable" | "bad_handler_answer";
|
|
624
|
+
/**
|
|
625
|
+
* A game server object that breaks the rules, with a code, the option at fault and what to do
|
|
626
|
+
* (like the developer API's errors, docs/NAMING.md).
|
|
627
|
+
*/
|
|
628
|
+
export declare class GameServerError extends Error {
|
|
629
|
+
readonly code: GameServerErrorCode;
|
|
630
|
+
readonly field: string;
|
|
631
|
+
readonly fix: string;
|
|
632
|
+
constructor(code: GameServerErrorCode, field: string, message: string, fix: string);
|
|
633
|
+
/** True for any error named GameServerError with a known code, so instanceof works for the copy a game's sandbox has too. */
|
|
634
|
+
static [Symbol.hasInstance](candidate: unknown): boolean;
|
|
635
|
+
}
|
|
636
|
+
/** The error for an option a game server doesn't take, naming the ones it does. */
|
|
637
|
+
export declare function unknownOptionError(field: string): GameServerError;
|
|
638
|
+
/** The error for a room whose game server has no setup(): it only answers requests, runs on schedules or takes webhooks. */
|
|
639
|
+
export declare function noSetupError(): GameServerError;
|
|
640
|
+
/** A game server's schedules, checked (readSchedules), or GameServerError for the field schedules. */
|
|
641
|
+
export declare function checkSchedules(schedules: unknown, hasOnSchedule: boolean): Schedule[];
|
|
642
|
+
/** Check a game server's options once, when it's defined: unknown options and values out of range throw. */
|
|
643
|
+
export declare function defineGameServer<S>(server: GameServer<S>): GameServer<S>;
|
|
644
|
+
/** Whether a room shows in room lists, takes new players and is offered by Zaisma's Join, and its name and labels. */
|
|
645
|
+
export interface RoomSettings {
|
|
646
|
+
isListed: boolean;
|
|
647
|
+
isOpen: boolean;
|
|
648
|
+
friendJoinsEnabled: boolean;
|
|
649
|
+
name: string | null;
|
|
650
|
+
labels: Record<string, string>;
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Messages from the room to a client. Every message says its type; a game's own messages carry
|
|
654
|
+
* theirs in data (docs/NAMING.md).
|
|
655
|
+
*
|
|
656
|
+
* Clients send {type: "hello", protocolVersionNumber, pass, joinData?, room?: {isListed?, friendJoinsEnabled?, name?, labels?}, idleTimeoutSec?,
|
|
657
|
+
* idleCheckEnabled?} first (ClientMessage; joinData is for canJoin and onJoin; the room's settings only count from
|
|
658
|
+
* the player who makes it, and a room is listed only when its first player's hello says isListed: true;
|
|
659
|
+
* the idle fields only where the game server's idleCheck is "client"), then {type: "message", data}.
|
|
660
|
+
* With hostControlsEnabled, the host may also send {type: "room", room: {isListed?, isOpen?,
|
|
661
|
+
* friendJoinsEnabled?, name?, labels?}} and {type: "kick", playerId, keepOut?}. hostPlayerId is null without host controls.
|
|
662
|
+
*
|
|
663
|
+
* Versions: welcome says the room's protocolVersionNumber (PROTOCOL_VERSION_NUMBER). A message with a type
|
|
664
|
+
* the room doesn't know is counted and ignored, never refused, so a newer SDK still talks to an older room.
|
|
665
|
+
* A game's messages reach players as {type: "message", data, fromPlayerId}: the player the game server
|
|
666
|
+
* says it's from (SendOptions.fromPlayerId), or null.
|
|
667
|
+
*
|
|
668
|
+
* Blocks (docs/DECISIONS.md, "How blocks work"): a player's game sends {type: "block", playerId} for a
|
|
669
|
+
* player in the room it blocked (a guest's, or one just made) and {type: "unblock", playerId} when it
|
|
670
|
+
* takes one back; blocks between accounts the room finds itself. Each player gets {type: "blocks",
|
|
671
|
+
* playerIds}, the players in the room they have a block with, either way, whenever that changes. A
|
|
672
|
+
* hello from Zaisma.rooms.quickJoin may say avoidBlockedPlayers: true, and a Zaisma room then turns the
|
|
673
|
+
* player away with blocked_player_here when someone they blocked is in it, so quickJoin tries another.
|
|
674
|
+
* Every hello from quickJoin says isQuickJoin: true, so Zaisma knows the game joins players by itself.
|
|
675
|
+
*
|
|
676
|
+
* Players who walk away (docs/DECISIONS.md): clients send the text "ping" every 30 seconds or so (the
|
|
677
|
+
* room answers "pong"; a connection with no ping or message for about a minute is dropped), and
|
|
678
|
+
* {type: "here"} now and then while the player gives input but the game sends nothing. They send
|
|
679
|
+
* {type: "idle"} when the player didn't answer "Still there?", {type: "hidden"} and {type: "shown"} as
|
|
680
|
+
* the game is hidden and shown (where welcome's awayChecks.hiddenTimeoutSec says it counts), and
|
|
681
|
+
* {type: "back", joinData?, idleTimeoutSec?, idleCheckEnabled?} when the player returns. A player out
|
|
682
|
+
* of the match with their seat held gets {type: "away"}, and once the seat is no longer held, their
|
|
683
|
+
* connection closes with SEAT_GONE_CLOSE_CODE.
|
|
684
|
+
*
|
|
685
|
+
* Ping and the room's clock (docs/DECISIONS.md, "Rooms under lag"): the room sends {type: "probe",
|
|
686
|
+
* probeNumber, serverTimeMs, pingDurationMs} right after welcome and again once it's answered (so the
|
|
687
|
+
* client hears its first ping), every welcome.probeIntervalSec while
|
|
688
|
+
* the room ticks (null when it doesn't), and each time ROOM_TUNING.probeSentSizeBytes has gone out to
|
|
689
|
+
* the player. The client answers at once with {type: "probe_answer", probeNumber}. Answers aren't game
|
|
690
|
+
* messages: they don't count toward the 30 a second or the idle check. A probe left unanswered for
|
|
691
|
+
* ROOM_TUNING.probeAnswerTimeoutSec, or ROOM_TUNING.maximumUnansweredSentSizeBytes sent since the last
|
|
692
|
+
* answered one, closes the connection with NO_ANSWER_CLOSE_CODE.
|
|
693
|
+
*
|
|
694
|
+
* Bytes: after hello, a client may also send binary frames (room.sendBytes), each up to
|
|
695
|
+
* ROOM_LIMITS.maximumGameMessageSizeBytes and counted with its messages, for the game server's onBytes.
|
|
696
|
+
* Every binary frame the room sends starts with one byte, the length in UTF-8 bytes of the player ID
|
|
697
|
+
* they came from (0 for the game server), then that ID, then the bytes (bytesFrame).
|
|
698
|
+
*
|
|
699
|
+
* State sync (stateSyncEnabled): {type: "state", serverTimeMs, full} with the player's whole view, the
|
|
700
|
+
* first time and after their connection was backed up, then {type: "state", serverTimeMs, changes} with
|
|
701
|
+
* what changed since (StateChange, state-sync.ts). serverTimeMs is the room's clock as it was sent.
|
|
702
|
+
*
|
|
703
|
+
* Spectators (maximumSpectatorCount): a hello with isSpectator: true watches. A spectator's welcome says
|
|
704
|
+
* isSpectator: true, and they get the players, the state and every message sent to everyone, but they send
|
|
705
|
+
* nothing: only probe answers, and anything else gets spectator_cannot_send. welcome and every "room"
|
|
706
|
+
* message carry spectatorCount, and a "room" message goes out whenever it changes.
|
|
707
|
+
*/
|
|
708
|
+
export type ServerMessage = {
|
|
709
|
+
type: "welcome";
|
|
710
|
+
me: ShownPlayer;
|
|
711
|
+
isSpectator: boolean;
|
|
712
|
+
players: ListedPlayer[];
|
|
713
|
+
room: RoomSettings;
|
|
714
|
+
spectatorCount: number;
|
|
715
|
+
hostPlayerId: string | null;
|
|
716
|
+
awayChecks: AwayChecks;
|
|
717
|
+
endsAt: string | null;
|
|
718
|
+
probeIntervalSec: number | null;
|
|
719
|
+
protocolVersionNumber: number;
|
|
720
|
+
} | {
|
|
721
|
+
type: "probe";
|
|
722
|
+
probeNumber: number;
|
|
723
|
+
serverTimeMs: number;
|
|
724
|
+
pingDurationMs: number | null;
|
|
725
|
+
} | {
|
|
726
|
+
type: "ending";
|
|
727
|
+
endsAt: string;
|
|
728
|
+
} | {
|
|
729
|
+
type: "version_update";
|
|
730
|
+
step: "coming" | "now";
|
|
731
|
+
versionId: string;
|
|
732
|
+
appliesAt: string;
|
|
733
|
+
isReloading: boolean;
|
|
734
|
+
} | {
|
|
735
|
+
type: "players";
|
|
736
|
+
players: ListedPlayer[];
|
|
737
|
+
hostPlayerId: string | null;
|
|
738
|
+
} | {
|
|
739
|
+
type: "away";
|
|
740
|
+
awayReason: Exclude<AwayReason, "lost">;
|
|
741
|
+
seatHoldDurationSec: number;
|
|
742
|
+
} | {
|
|
743
|
+
type: "kicked";
|
|
744
|
+
message: string;
|
|
745
|
+
isKeptOut: boolean;
|
|
746
|
+
} | {
|
|
747
|
+
type: "room";
|
|
748
|
+
room: RoomSettings;
|
|
749
|
+
spectatorCount: number;
|
|
750
|
+
} | {
|
|
751
|
+
type: "move";
|
|
752
|
+
roomCode: string;
|
|
753
|
+
} | {
|
|
754
|
+
type: "message";
|
|
755
|
+
data: unknown;
|
|
756
|
+
fromPlayerId: string | null;
|
|
757
|
+
} | {
|
|
758
|
+
type: "state";
|
|
759
|
+
serverTimeMs: number;
|
|
760
|
+
full: unknown;
|
|
761
|
+
} | {
|
|
762
|
+
type: "state";
|
|
763
|
+
serverTimeMs: number;
|
|
764
|
+
changes: StateChange[];
|
|
765
|
+
} | {
|
|
766
|
+
type: "blocks";
|
|
767
|
+
playerIds: string[];
|
|
768
|
+
} | {
|
|
769
|
+
type: "error";
|
|
770
|
+
code: RoomErrorCode;
|
|
771
|
+
message: string;
|
|
772
|
+
versionId?: string;
|
|
773
|
+
} | {
|
|
774
|
+
type: "ended";
|
|
775
|
+
message: string;
|
|
776
|
+
endReason: EndReason;
|
|
777
|
+
};
|
|
778
|
+
/**
|
|
779
|
+
* Messages from a client to the room. A room ignores a type it doesn't know (it counts it, like any
|
|
780
|
+
* message), so a client may send newer ones.
|
|
781
|
+
*/
|
|
782
|
+
export type ClientMessage = {
|
|
783
|
+
type: "hello";
|
|
784
|
+
protocolVersionNumber: number;
|
|
785
|
+
pass: string;
|
|
786
|
+
joinData?: unknown;
|
|
787
|
+
room?: Partial<Pick<RoomSettings, "isListed" | "friendJoinsEnabled">> & RoomInfo;
|
|
788
|
+
idleTimeoutSec?: number;
|
|
789
|
+
idleCheckEnabled?: boolean;
|
|
790
|
+
isSpectator?: boolean;
|
|
791
|
+
isQuickJoin?: boolean;
|
|
792
|
+
avoidBlockedPlayers?: boolean;
|
|
793
|
+
} | {
|
|
794
|
+
type: "message";
|
|
795
|
+
data: unknown;
|
|
796
|
+
} | {
|
|
797
|
+
type: "room";
|
|
798
|
+
room: Partial<RoomSettings>;
|
|
799
|
+
} | {
|
|
800
|
+
type: "kick";
|
|
801
|
+
playerId: string;
|
|
802
|
+
keepOut?: boolean;
|
|
803
|
+
} | {
|
|
804
|
+
type: "block";
|
|
805
|
+
playerId: string;
|
|
806
|
+
} | {
|
|
807
|
+
type: "unblock";
|
|
808
|
+
playerId: string;
|
|
809
|
+
} | {
|
|
810
|
+
type: "probe_answer";
|
|
811
|
+
probeNumber: number;
|
|
812
|
+
} | {
|
|
813
|
+
type: "here";
|
|
814
|
+
} | {
|
|
815
|
+
type: "idle";
|
|
816
|
+
} | {
|
|
817
|
+
type: "hidden";
|
|
818
|
+
} | {
|
|
819
|
+
type: "shown";
|
|
820
|
+
} | {
|
|
821
|
+
type: "back";
|
|
822
|
+
joinData?: unknown;
|
|
823
|
+
idleTimeoutSec?: number;
|
|
824
|
+
idleCheckEnabled?: boolean;
|
|
825
|
+
};
|
|
826
|
+
/**
|
|
827
|
+
* Why a match ended: the game ended it (room.end), its hosting paused, its game server stopped working,
|
|
828
|
+
* the game became unavailable, its kept room was deleted, or no players were left for its spectators.
|
|
829
|
+
*/
|
|
830
|
+
export type EndReason = "game" | "hosting_paused" | "game_server_stopped" | "game_unavailable" | "kept_room_deleted" | "no_players";
|
|
831
|
+
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";
|
|
832
|
+
export declare const ERROR_TEXT: Record<RoomErrorCode, string>;
|
|
833
|
+
/** What players are told when Zaisma ends a match at room.endsAt. */
|
|
834
|
+
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.";
|
|
835
|
+
/**
|
|
836
|
+
* isListed, isOpen, the players who have been in the room, the players kept out of it and a reserved
|
|
837
|
+
* room's setup data, saved with a sleeping room's state.
|
|
838
|
+
*/
|
|
839
|
+
export interface SavedRoom extends RoomSettings {
|
|
840
|
+
knownPlayerIds: string[];
|
|
841
|
+
setupData?: unknown;
|
|
842
|
+
keptOut?: Record<string, string>;
|
|
843
|
+
expected?: Record<string, Expected>;
|
|
844
|
+
held?: Record<string, Held>;
|
|
845
|
+
kept?: Record<string, AwayReason>;
|
|
846
|
+
/** Players whose connection dropped, with when their time to come back runs out (reconnectTimeoutSec). */
|
|
847
|
+
reconnecting?: Record<string, Reconnecting>;
|
|
848
|
+
lobbyRoomCode?: string;
|
|
849
|
+
/** Blocks between the room's players from their accounts, as pairs of player IDs. */
|
|
850
|
+
accountBlocks?: [string, string][];
|
|
851
|
+
/** Blocks players' games said they made, each the blocker's player ID and then the blocked one's. */
|
|
852
|
+
sessionBlocks?: [string, string][];
|
|
853
|
+
/** Friendships between the room's players, as pairs of player IDs. */
|
|
854
|
+
friendships?: [string, string][];
|
|
855
|
+
/** When Zaisma will end the match (room.endsAt), once it's set. */
|
|
856
|
+
endsAt?: string;
|
|
857
|
+
/** Kept (room.setKept): it sleeps with its state when everyone leaves, instead of going a day later. */
|
|
858
|
+
isKept?: boolean;
|
|
859
|
+
}
|
|
860
|
+
/** A seat held for a player out of the match: who, why they're out, and until when. */
|
|
861
|
+
export interface Held {
|
|
862
|
+
player: Player;
|
|
863
|
+
reason: AwayReason;
|
|
864
|
+
untilMs: number;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* A player whose connection dropped, still in the match: who, and when their time to come back runs out.
|
|
868
|
+
* Saved with the room, so a room that sleeps and wakes, which finds its players by their connections, keeps them.
|
|
869
|
+
*/
|
|
870
|
+
export interface Reconnecting {
|
|
871
|
+
player: Player;
|
|
872
|
+
untilMs: number;
|
|
873
|
+
}
|
|
874
|
+
/** When a room counts its players as away, or null when it doesn't: no input (idle), and the game hidden. */
|
|
875
|
+
export interface AwayChecks {
|
|
876
|
+
idleTimeoutSec: number | null;
|
|
877
|
+
hiddenTimeoutSec: number | null;
|
|
878
|
+
}
|
|
879
|
+
/** A player another room is sending here: the join data it sent, where from, and until when their seat is held. */
|
|
880
|
+
export interface Expected {
|
|
881
|
+
joinData: unknown;
|
|
882
|
+
fromRoomCode: string;
|
|
883
|
+
untilMs: number;
|
|
884
|
+
}
|
|
885
|
+
/** The idle check a player's game asked for, for idleCheck "client": its timeout, or none at all. */
|
|
886
|
+
export interface ClientIdle {
|
|
887
|
+
idleTimeoutSec?: number;
|
|
888
|
+
idleCheckEnabled?: boolean;
|
|
889
|
+
}
|
|
890
|
+
/** How the room reaches players: a Durable Object's sockets, or a Node server's. */
|
|
891
|
+
export interface Transport {
|
|
892
|
+
send(playerId: string, text: string): void;
|
|
893
|
+
/** A binary frame (bytesFrame). Without it, rooms can't send bytes. */
|
|
894
|
+
sendBytes?(playerId: string, frame: Uint8Array): void;
|
|
895
|
+
close(playerId: string, closeCode: number, reason: string): void;
|
|
896
|
+
/** Set up a reserved room, then send each of its players a "move" message. Without it, rooms can't reserve. */
|
|
897
|
+
reserve?(roomCode: string, start: RoomStart): void;
|
|
898
|
+
/**
|
|
899
|
+
* Ask another room of the game to take some players (its expect()), send each player it takes a
|
|
900
|
+
* "move" message, then hand the answer to this room's moved(). Without it, rooms can't move players.
|
|
901
|
+
*/
|
|
902
|
+
moveTo?(roomCode: string, ask: Omit<MoveAsk, "fromRoomCode">): void;
|
|
903
|
+
/** This room's code, as room.roomCode. */
|
|
904
|
+
roomCode: string;
|
|
905
|
+
/** The game's database, as room.database. Without it, its calls fail with database_unavailable. */
|
|
906
|
+
database?: GameDatabase;
|
|
907
|
+
/** The game's secrets, as room.secrets. Without them, there are none. */
|
|
908
|
+
secrets?: Readonly<Record<string, string>>;
|
|
909
|
+
/** room.verifyReceipt, with Zaisma's keys and this game filled in. Without it, it throws, saying so. */
|
|
910
|
+
verifyReceipt?(receipt: string): Promise<ReceiptClaims>;
|
|
911
|
+
/** room.applyVersionUpdate(): switch to the version coming now. Without it (your own server), it does nothing. */
|
|
912
|
+
applyVersionUpdate?(): void;
|
|
913
|
+
/** Whether a player's connection holds ROOM_TUNING.backedUpSizeBytes or more not yet sent. Without it, the room judges by its probes. */
|
|
914
|
+
isBackedUp?(playerId: string): boolean;
|
|
915
|
+
}
|
|
916
|
+
/** room.secrets and server.secrets where a game has none. */
|
|
917
|
+
export declare const NO_SECRETS: Readonly<Record<string, string>>;
|
|
918
|
+
/** room.verifyReceipt and server.verifyReceipt where the runtime has no receipt keys. */
|
|
919
|
+
export declare const NO_RECEIPT_CHECK: () => Promise<ReceiptClaims>;
|
|
920
|
+
/**
|
|
921
|
+
* Plain JSON within maximumSizeBytes (ROOM_LIMITS.maximumJoinDataSizeBytes or maximumSetupDataSizeBytes), copied, or isValid false if it isn't.
|
|
922
|
+
* Nothing (undefined or null) is null. Players' data that fails this is refused.
|
|
923
|
+
*/
|
|
924
|
+
export declare function checkRoomData(given: unknown, maximumSizeBytes: number): {
|
|
925
|
+
isValid: true;
|
|
926
|
+
checkedData: unknown;
|
|
927
|
+
} | {
|
|
928
|
+
isValid: false;
|
|
929
|
+
};
|
|
930
|
+
/**
|
|
931
|
+
* An error for a client. message replaces the usual text, like the game's reason for turning a player away;
|
|
932
|
+
* details add what the code needs, like the room's versionId with other_version.
|
|
933
|
+
*/
|
|
934
|
+
export declare function errorMessage(code: RoomErrorCode, message?: string, details?: {
|
|
935
|
+
versionId: string;
|
|
936
|
+
}): string;
|
|
937
|
+
/** Whether a player may come in, and if not, why. */
|
|
938
|
+
export type Admission = {
|
|
939
|
+
isAllowed: true;
|
|
940
|
+
} | {
|
|
941
|
+
isAllowed: false;
|
|
942
|
+
code: RoomErrorCode;
|
|
943
|
+
message?: string;
|
|
944
|
+
};
|
|
945
|
+
/** WebSocket close code for a match that ended normally. */
|
|
946
|
+
export declare const ENDED_CLOSE_CODE = 4000;
|
|
947
|
+
/** Close code for a player refused or removed by the room: the SDK doesn't reconnect. */
|
|
948
|
+
export declare const REMOVED_CLOSE_CODE = 4001;
|
|
949
|
+
/** Close code for a player who walked away, once the room stops holding their seat. They may try to come back. */
|
|
950
|
+
export declare const SEAT_GONE_CLOSE_CODE = 4002;
|
|
951
|
+
/** Close code for a connection that stopped answering or fell too far behind; the SDK reconnects. */
|
|
952
|
+
export declare const NO_ANSWER_CLOSE_CODE = 4003;
|
|
953
|
+
/** Close code for a match switching to a version published "now"; the SDK reconnects, or the page reloads the game first. */
|
|
954
|
+
export declare const VERSION_UPDATE_CLOSE_CODE = 4004;
|
|
955
|
+
/** Close code (the WebSocket standard's "try again later") for rooms too busy to take the connection; the SDK tries again. */
|
|
956
|
+
export declare const BUSY_CLOSE_CODE = 1013;
|
|
957
|
+
/**
|
|
958
|
+
* A binary frame as the room sends it: one byte, the length of fromPlayerId in UTF-8 bytes (0 for the
|
|
959
|
+
* game server), then fromPlayerId, then the bytes.
|
|
960
|
+
*/
|
|
961
|
+
export declare function bytesFrame(bytes: Uint8Array, fromPlayerId: string | null): Uint8Array;
|
|
962
|
+
/**
|
|
963
|
+
* Marks a game server whose onMessage and onBytes only queue the input for its next call, like Zaisma's
|
|
964
|
+
* sandboxed real-time ones: the room hands each input over as it comes, outside the handlers waiting
|
|
965
|
+
* their turn, so ticks never wait behind input.
|
|
966
|
+
*/
|
|
967
|
+
export declare const QUEUES_INPUT: unique symbol;
|
|
968
|
+
/**
|
|
969
|
+
* On a room's API object, for Zaisma's sandbox: a count that goes up each time a block or friendship
|
|
970
|
+
* between the room's players may have changed, so the sandbox is sent the pairs again only then.
|
|
971
|
+
*/
|
|
972
|
+
export declare const PAIR_CHANGE_COUNT: unique symbol;
|
|
973
|
+
/** A room's method for a game server in a sandbox to reserve a room with the room code it already told the game. Zaisma's own: not on Room's type. */
|
|
974
|
+
export declare const RESERVE_WITH_ROOM_CODE: unique symbol;
|
|
975
|
+
/**
|
|
976
|
+
* Marks a game server that runs state sync itself, like Zaisma's sandboxed ones, whose state lives in the
|
|
977
|
+
* sandbox: its calls send their frames through the room's STATE_SYNC, and the room calls this, with the
|
|
978
|
+
* state and its API, when a round is due outside them (a player reconnecting, or catching up, in a turn-based room).
|
|
979
|
+
*/
|
|
980
|
+
export declare const SYNCS_STATE: unique symbol;
|
|
981
|
+
/**
|
|
982
|
+
* On a room's API object, for a game server that runs state sync itself (SYNCS_STATE): viewers() says
|
|
983
|
+
* which players are backed up and which need a full copy (taken: each is said once), and send() sends a
|
|
984
|
+
* frame to some of them.
|
|
985
|
+
*/
|
|
986
|
+
export declare const STATE_SYNC: unique symbol;
|
|
987
|
+
/** What STATE_SYNC hands a game server that runs state sync itself. */
|
|
988
|
+
export interface StateSyncHooks {
|
|
989
|
+
viewers(): {
|
|
990
|
+
backedUpPlayerIds: string[];
|
|
991
|
+
fullCopyPlayerIds: string[];
|
|
992
|
+
};
|
|
993
|
+
send(playerIds: unknown, frame: unknown): void;
|
|
994
|
+
}
|
|
995
|
+
/** Messages a room's traffic counts by type: their data.type, like "move", or one of these. */
|
|
996
|
+
export declare const NO_MESSAGE_TYPE = "(none)";
|
|
997
|
+
export declare const BYTES_MESSAGE_TYPE = "(bytes)";
|
|
998
|
+
export declare const ZAISMA_MESSAGE_TYPE = "(zaisma)";
|
|
999
|
+
export declare const OTHER_MESSAGE_TYPE = "(other)";
|
|
1000
|
+
/** State sync's updates (stateSyncEnabled). */
|
|
1001
|
+
export declare const STATE_MESSAGE_TYPE = "(state)";
|
|
1002
|
+
export type TrafficDirection = "inbound" | "outbound";
|
|
1003
|
+
/**
|
|
1004
|
+
* One message type's traffic in one direction: how many, their bytes and the largest, and how many of
|
|
1005
|
+
* those sent reliably went to a player whose connection was backed up (for the latest_only hint).
|
|
1006
|
+
*/
|
|
1007
|
+
export interface MessageTypeTraffic {
|
|
1008
|
+
messageType: string;
|
|
1009
|
+
direction: TrafficDirection;
|
|
1010
|
+
messageCount: number;
|
|
1011
|
+
sizeBytes: number;
|
|
1012
|
+
largestSizeBytes: number;
|
|
1013
|
+
backedUpMessageCount: number;
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* A room's traffic since it was last taken (takeTraffic): bytes in, messages and bytes out (each
|
|
1017
|
+
* frame once per player it went to), and per message type. Messages in are RoomCore.messageCount.
|
|
1018
|
+
*/
|
|
1019
|
+
export interface RoomTraffic {
|
|
1020
|
+
inboundSizeBytes: number;
|
|
1021
|
+
outboundMessageCount: number;
|
|
1022
|
+
outboundSizeBytes: number;
|
|
1023
|
+
messageTypes: MessageTypeTraffic[];
|
|
1024
|
+
}
|
|
1025
|
+
/** No traffic yet. */
|
|
1026
|
+
export declare function noTraffic(): RoomTraffic;
|
|
1027
|
+
/** Two rooms' (or two hours') traffic added up, message types matched by type and direction. */
|
|
1028
|
+
export declare function addTraffic(first: RoomTraffic, second: RoomTraffic): RoomTraffic;
|
|
1029
|
+
export declare class RoomCore<S = any> {
|
|
1030
|
+
state: S;
|
|
1031
|
+
readonly players: Map<string, Player>;
|
|
1032
|
+
/** Spectators watching (maximumSpectatorCount): never in players, never counted as playing. */
|
|
1033
|
+
readonly spectators: Map<string, Player>;
|
|
1034
|
+
ended: boolean;
|
|
1035
|
+
/** When Zaisma will end this match (room.endsAt), or null. */
|
|
1036
|
+
endsAt: string | null;
|
|
1037
|
+
/** Messages received from clients, for usage counts. */
|
|
1038
|
+
messageCount: number;
|
|
1039
|
+
/** The rest of the room's traffic since takeTraffic, by "<direction> <type>". */
|
|
1040
|
+
private traffic;
|
|
1041
|
+
/** Goes up whenever a block or friendship between players may have changed (PAIR_CHANGE_COUNT). */
|
|
1042
|
+
private pairChangeCount;
|
|
1043
|
+
/** Set when the state changed and a sleeping room should save it. */
|
|
1044
|
+
dirty: boolean;
|
|
1045
|
+
/** A version published "now" this match is moving to, once the transport started it (startVersionUpdate). */
|
|
1046
|
+
versionUpdate: {
|
|
1047
|
+
versionId: string;
|
|
1048
|
+
appliesAt: string;
|
|
1049
|
+
isReloading: boolean;
|
|
1050
|
+
} | null;
|
|
1051
|
+
/** When a real-time room's state was last saved, set by the transport after each save; null until it has been. */
|
|
1052
|
+
savedAtMs: number | null;
|
|
1053
|
+
/** Hidden from room lists until its first player's hello asks for it (joinRoom's isListed, quickJoin), or a reserve does. */
|
|
1054
|
+
isListed: boolean;
|
|
1055
|
+
isOpen: boolean;
|
|
1056
|
+
friendJoinsEnabled: boolean;
|
|
1057
|
+
name: string | null;
|
|
1058
|
+
labels: Record<string, string>;
|
|
1059
|
+
/** Kept (room.setKept): the transport keeps its state when it empties. */
|
|
1060
|
+
isKept: boolean;
|
|
1061
|
+
/**
|
|
1062
|
+
* Set when the player count, isListed, isOpen or isKept changed, so the room list can be brought up to date,
|
|
1063
|
+
* and when the room's blocks changed, since a sleeping room's saved room is written with its listing.
|
|
1064
|
+
*/
|
|
1065
|
+
listingChanged: boolean;
|
|
1066
|
+
/** Players who have been in this room, newest last: they may come back after it closes. */
|
|
1067
|
+
readonly known: Set<string>;
|
|
1068
|
+
/** Players kicked and kept out for the rest of the match, with the message they were given. */
|
|
1069
|
+
readonly keptOut: Map<string, string>;
|
|
1070
|
+
/** Players other rooms are sending here (room.moveTo), with a seat each until they arrive. */
|
|
1071
|
+
readonly expected: Map<string, Expected>;
|
|
1072
|
+
/** Seats held for players out of the match (walked away, or lost their connection) until they come back or it runs out. */
|
|
1073
|
+
readonly held: Map<string, Held>;
|
|
1074
|
+
/** Players who walked away that onAway kept in the room, and players whose connection dropped (lost), and why. */
|
|
1075
|
+
readonly kept: Map<string, AwayReason>;
|
|
1076
|
+
/** Players whose connection dropped (kept as "lost"): when their time to reconnect runs out. */
|
|
1077
|
+
readonly reconnectUntilMs: Map<string, number>;
|
|
1078
|
+
/** The room that made this one, where awayMode "lobby" sends players who walk away. */
|
|
1079
|
+
lobbyRoomCode: string | null;
|
|
1080
|
+
/** Blocks between players from their accounts (Zaisma's own record, or passes on your own server), either way: pairKey(first, second). */
|
|
1081
|
+
private readonly accountBlocks;
|
|
1082
|
+
/** Blocks players' games said they made, a guest's or one just made: blockKey(blocker, blocked). */
|
|
1083
|
+
private readonly sessionBlocks;
|
|
1084
|
+
/** Friendships between players from their accounts (Zaisma's own record, or passes on your own server): pairKey(first, second). */
|
|
1085
|
+
private readonly friendships;
|
|
1086
|
+
/** The blocks each player was last told of, so they're told again only when theirs change. */
|
|
1087
|
+
private readonly toldBlocks;
|
|
1088
|
+
/** When each player in the match was last heard from, when their game was hidden, and the idle check their game asked for. */
|
|
1089
|
+
private presence;
|
|
1090
|
+
/** Players this room is sending to its lobby for walking away, so onMoved doesn't hear of it. */
|
|
1091
|
+
private sentAway;
|
|
1092
|
+
/** What a reserved room started with, kept so a room that starts over starts the same way. */
|
|
1093
|
+
private setupData;
|
|
1094
|
+
/** When this room last reserved rooms, for the limit. */
|
|
1095
|
+
private reservedAtMs;
|
|
1096
|
+
private buckets;
|
|
1097
|
+
/** Probes this room has sent, to every player: each probe's number. */
|
|
1098
|
+
private probeCount;
|
|
1099
|
+
/** What each player was last sent of the state (stateSyncEnabled), unless the game server runs state sync itself (SYNCS_STATE). */
|
|
1100
|
+
private readonly stateTracker;
|
|
1101
|
+
/** For a game server that runs state sync itself: players with a new connection, who need a full copy. */
|
|
1102
|
+
private readonly fullCopyPlayerIds;
|
|
1103
|
+
/** A turn-based room's players state sync skipped as backed up, who get a full copy once they catch up. */
|
|
1104
|
+
private readonly stateSkippedPlayerIds;
|
|
1105
|
+
/** Latest-only messages waiting for each backed-up player, by "<kind>:<latestKey>", least recently replaced first. */
|
|
1106
|
+
private readonly latestSlots;
|
|
1107
|
+
private readonly now;
|
|
1108
|
+
readonly maximumPlayerCount: number;
|
|
1109
|
+
readonly maximumSpectatorCount: number;
|
|
1110
|
+
private readonly api;
|
|
1111
|
+
/** The room as canJoin and stateView see it: reading works, and any call that changes or sends throws room_call_unavailable. */
|
|
1112
|
+
private readonly canJoinRoom;
|
|
1113
|
+
private readonly stateViewRoom;
|
|
1114
|
+
readonly server: GameServer<S>;
|
|
1115
|
+
private readonly transport;
|
|
1116
|
+
/**
|
|
1117
|
+
* Set once a handler returns a promise (a game server in a sandbox): from then on handlers run
|
|
1118
|
+
* one after another, each with the state the last one left.
|
|
1119
|
+
*/
|
|
1120
|
+
private queue;
|
|
1121
|
+
/** Handlers waiting in the queue, so ticks don't pile up behind a slow game server. */
|
|
1122
|
+
private waitingCount;
|
|
1123
|
+
/** Handlers running right now, and what waits for them to finish (a kicked player's onLeave). */
|
|
1124
|
+
private depth;
|
|
1125
|
+
private afterHandler;
|
|
1126
|
+
/**
|
|
1127
|
+
* startOptions.start makes a reserved room: closed, open only to the players it was made for, and
|
|
1128
|
+
* set up with its setup data. A saved room (startOptions.room) wakes as it was, with
|
|
1129
|
+
* startOptions.presence saying when each of its players was last heard from and the idle check
|
|
1130
|
+
* their game asked for. A restarted real-time room (startOptions.restoreInfo, with its saved state)
|
|
1131
|
+
* skips setup: startOptions.lostPlayers, the players in its match at the save who aren't connected,
|
|
1132
|
+
* come back as away (reason "lost"), and onRestored runs once. startOptions.spectators are spectators
|
|
1133
|
+
* still connected to a room waking from sleep.
|
|
1134
|
+
*/
|
|
1135
|
+
constructor(server: GameServer<S>, transport: Transport, startOptions?: {
|
|
1136
|
+
state?: S;
|
|
1137
|
+
players?: Player[];
|
|
1138
|
+
spectators?: Player[];
|
|
1139
|
+
room?: SavedRoom;
|
|
1140
|
+
start?: RoomStart;
|
|
1141
|
+
presence?: Record<string, {
|
|
1142
|
+
seenAtMs?: number;
|
|
1143
|
+
clientIdle?: ClientIdle;
|
|
1144
|
+
}>;
|
|
1145
|
+
restoreInfo?: RestoreInfo;
|
|
1146
|
+
lostPlayers?: Player[];
|
|
1147
|
+
now?: () => number;
|
|
1148
|
+
});
|
|
1149
|
+
/**
|
|
1150
|
+
* A restarted room's players who aren't connected are away as lost, with the whole reconnectTimeoutSec
|
|
1151
|
+
* from now, whatever they had before the restart; then onRestored runs, and onAway for each.
|
|
1152
|
+
*/
|
|
1153
|
+
private restored;
|
|
1154
|
+
/** What a sleeping room saves besides the game's state. */
|
|
1155
|
+
get saved(): SavedRoom;
|
|
1156
|
+
/** The blocks worth keeping while the room sleeps: those between players it still remembers. */
|
|
1157
|
+
private savedBlocks;
|
|
1158
|
+
/** Whether two players are friends, from their accounts as the later of them joined. */
|
|
1159
|
+
areFriends(firstPlayerId: string, secondPlayerId: string): boolean;
|
|
1160
|
+
/**
|
|
1161
|
+
* A player's friends among the others this room remembers, from their accounts, as the room found them
|
|
1162
|
+
* as the player joined (Zaisma's own record, or the passes on your own server): they replace what the
|
|
1163
|
+
* room knew of that player's, so a friend removed since is gone. Call before join.
|
|
1164
|
+
*/
|
|
1165
|
+
setFriends(playerId: string, friendPlayerIds: readonly string[]): void;
|
|
1166
|
+
/** Whether two players have a block between them, either way: from their accounts, or one a player's game said it made. */
|
|
1167
|
+
hasBlockBetween(firstPlayerId: string, secondPlayerId: string): boolean;
|
|
1168
|
+
/**
|
|
1169
|
+
* The blocks between a player and the others this room remembers, from their accounts, as the room
|
|
1170
|
+
* found them as the player joined (Zaisma's own record, or the passes on your own server): they replace
|
|
1171
|
+
* what the room knew of that player's, so a block taken back since is gone. Call before join.
|
|
1172
|
+
*/
|
|
1173
|
+
setAccountBlocks(playerId: string, blockPlayerIds: readonly string[]): void;
|
|
1174
|
+
/** The players in the room this one has a block with, either way. */
|
|
1175
|
+
private blockedWith;
|
|
1176
|
+
/** Tell each player the players in the room they have a block with, when that changed. */
|
|
1177
|
+
private tellBlocks;
|
|
1178
|
+
/** A player's game blocked a player in the room (a guest's block, or one just made), or took one back. */
|
|
1179
|
+
private sessionBlock;
|
|
1180
|
+
/** Players in the match who haven't walked away: what room lists and "playing now" count. */
|
|
1181
|
+
get playingCount(): number;
|
|
1182
|
+
/** Seats the room holds for players on their way (room.moveTo) or out of the match for now. */
|
|
1183
|
+
get heldSeatCount(): number;
|
|
1184
|
+
/** Resolves once every handler started so far has finished (at once for game servers that answer at once). */
|
|
1185
|
+
settled(): Promise<void>;
|
|
1186
|
+
get ticking(): boolean;
|
|
1187
|
+
/** How many are watching. */
|
|
1188
|
+
get spectatorCount(): number;
|
|
1189
|
+
/**
|
|
1190
|
+
* Whether a player may come in: the room's own rules (over, kept out, closed, full), then the game
|
|
1191
|
+
* server's canJoin with the join data the player brought. Call before join; join checks the room's rules again.
|
|
1192
|
+
* admitOptions.isSpectator: they come to watch, which a closed or full room still takes (spectatorDoor).
|
|
1193
|
+
*/
|
|
1194
|
+
admit(player: Player, joinData?: unknown, admitOptions?: {
|
|
1195
|
+
isSpectator?: boolean;
|
|
1196
|
+
}): Promise<Admission>;
|
|
1197
|
+
/** The game server's canJoin, then the room's rules again, since things may change while it decides. */
|
|
1198
|
+
private ask;
|
|
1199
|
+
/**
|
|
1200
|
+
* The room's own rules for a player coming in, or null if they may. A player already in may always
|
|
1201
|
+
* come back, and a player another room sent, or whose seat is held, has a seat. isMoving: the game is
|
|
1202
|
+
* sending this player from another room, which a closed room still takes (closed keeps out
|
|
1203
|
+
* strangers, not the game).
|
|
1204
|
+
*/
|
|
1205
|
+
private door;
|
|
1206
|
+
/**
|
|
1207
|
+
* The room's own rules for a spectator coming in, or null if they may: a match that's over, and a player
|
|
1208
|
+
* kept out, keep them out, and so does a full audience; a closed or full room doesn't, since watching a
|
|
1209
|
+
* match under way is the point. A player in the match can't watch it too.
|
|
1210
|
+
*/
|
|
1211
|
+
private spectatorDoor;
|
|
1212
|
+
/** Whether this room is holding a place for a player: they've been in it, another room is sending them, or their seat is held. */
|
|
1213
|
+
expects(id: string): boolean;
|
|
1214
|
+
/** The seat held for a player out of the match, if it hasn't run out. */
|
|
1215
|
+
private holding;
|
|
1216
|
+
/** The seat held for a player another room is sending, if it hasn't run out. */
|
|
1217
|
+
private seat;
|
|
1218
|
+
/** Seats held for players other rooms are sending and players out of the match, dropping those that ran out. */
|
|
1219
|
+
private countHeldSeats;
|
|
1220
|
+
/**
|
|
1221
|
+
* Another room of the game wants to send players here (its room.moveTo). Each is checked like a
|
|
1222
|
+
* newcomer, with the room's rules and canJoin given the sending room's join data; those taken get a
|
|
1223
|
+
* seat for a minute. Says who was taken and who wasn't, and why.
|
|
1224
|
+
*/
|
|
1225
|
+
expect(ask: MoveAsk): Promise<MoveAnswer>;
|
|
1226
|
+
/**
|
|
1227
|
+
* A player arrives. Joining again with the same ID (a reconnect) keeps their place. The player who
|
|
1228
|
+
* makes the room can hide it from the start (a private match), turn friends' Join off, and name and label it. joinData is
|
|
1229
|
+
* what the player's game sent along, for onJoin; clientIdle is the idle check it asked for (idleCheck "client").
|
|
1230
|
+
* isSpectator: they come to watch (watch).
|
|
1231
|
+
*/
|
|
1232
|
+
join(player: Player, joinOptions?: {
|
|
1233
|
+
isListed?: boolean;
|
|
1234
|
+
friendJoinsEnabled?: boolean;
|
|
1235
|
+
joinData?: unknown;
|
|
1236
|
+
clientIdle?: ClientIdle;
|
|
1237
|
+
isSpectator?: boolean;
|
|
1238
|
+
} & RoomInfo): {
|
|
1239
|
+
isAllowed: true;
|
|
1240
|
+
isRejoin: boolean;
|
|
1241
|
+
} | {
|
|
1242
|
+
isAllowed: false;
|
|
1243
|
+
code: RoomErrorCode;
|
|
1244
|
+
message?: string;
|
|
1245
|
+
};
|
|
1246
|
+
/**
|
|
1247
|
+
* A spectator comes to watch: welcomed with the players and the room, probed for their ping and the
|
|
1248
|
+
* room's clock like a player, and the game hears it (onSpectatorJoin). Nothing they do counts as playing.
|
|
1249
|
+
*/
|
|
1250
|
+
private watch;
|
|
1251
|
+
/** A spectator stops watching: gone at once, and the game hears it (onSpectatorLeave) once the handler running now is done. */
|
|
1252
|
+
private stopWatching;
|
|
1253
|
+
/**
|
|
1254
|
+
* A player's connection closed: they left (closed the game, or followed a move), or it was lost. A
|
|
1255
|
+
* lost player stays in the match as away for reconnectTimeoutSec (onAway, reason "lost"), then
|
|
1256
|
+
* leaves it with their seat held for a while, so they can still come straight back. A spectator just goes.
|
|
1257
|
+
*/
|
|
1258
|
+
leave(playerId: string, reason?: "left" | "lost"): void;
|
|
1259
|
+
/** Take a player out of the match, forgetting what the room knew about them while they were in. */
|
|
1260
|
+
private out;
|
|
1261
|
+
/** Hold a seat for a player out of the match, for the game's seatHoldDurationSec. */
|
|
1262
|
+
private hold;
|
|
1263
|
+
/** When this room counts a player as away (welcome's awayChecks), by the game's settings and what their game asked for. */
|
|
1264
|
+
private checks;
|
|
1265
|
+
/** Seconds without input before a player counts as idle, or null if nobody checks for them. */
|
|
1266
|
+
private idleTimeoutSecFor;
|
|
1267
|
+
/**
|
|
1268
|
+
* A player walked away: idle (they didn't answer "Still there?", or the room heard nothing for
|
|
1269
|
+
* longer), or their game hidden too long in a real-time room. The game's onAway decides; otherwise
|
|
1270
|
+
* they leave the match with their seat held, and with awayMode "lobby" are sent to the lobby.
|
|
1271
|
+
*/
|
|
1272
|
+
private walkAway;
|
|
1273
|
+
/** A player who walked away is back: kept in the room by onAway, or coming back to their held seat. */
|
|
1274
|
+
private back;
|
|
1275
|
+
/** A player kept away is playing again: the game hears it (onBack), and every player's list shows them playing. */
|
|
1276
|
+
private cameBack;
|
|
1277
|
+
/** Players whose time to reconnect ran out leave the match, with their seat held (onLeave, reason "lost"). */
|
|
1278
|
+
private expireReconnects;
|
|
1279
|
+
/** When the first player whose connection dropped runs out of time to reconnect, or null if nobody is reconnecting. */
|
|
1280
|
+
nextReconnectDeadlineMs(): number | null;
|
|
1281
|
+
/** Whether a player is in the match without a connection: dropped, and still within reconnectTimeoutSec. */
|
|
1282
|
+
private isLost;
|
|
1283
|
+
/**
|
|
1284
|
+
* Check on the room's players, every half a minute or so: players the room hasn't heard from in
|
|
1285
|
+
* longer than the idle check allows (a game that doesn't ask "Still there?"), and players whose
|
|
1286
|
+
* game has been hidden too long in a real-time room, walk away; held seats that ran out are freed,
|
|
1287
|
+
* and those players' connections close.
|
|
1288
|
+
*/
|
|
1289
|
+
sweep(): void;
|
|
1290
|
+
/** A raw text message from a client. Returns an error code if it was refused. */
|
|
1291
|
+
receive(playerId: string, text: string): RoomErrorCode | null;
|
|
1292
|
+
/** Bytes from a client (a binary frame). Returns an error code if they were refused. */
|
|
1293
|
+
receiveBytes(playerId: string, bytes: Uint8Array): RoomErrorCode | null;
|
|
1294
|
+
/**
|
|
1295
|
+
* A spectator's text: answers to the room's probes, and nothing else (spectator_cannot_send). Kept out of
|
|
1296
|
+
* the room's message count and traffic, so watching stays off the meter, and limited like anyone's messages.
|
|
1297
|
+
*/
|
|
1298
|
+
private receiveFromSpectator;
|
|
1299
|
+
/**
|
|
1300
|
+
* A player's message or bytes for the game server: through the queue of handlers, or at once for a
|
|
1301
|
+
* game server that only queues them for its next call (QUEUES_INPUT).
|
|
1302
|
+
*/
|
|
1303
|
+
private input;
|
|
1304
|
+
/** Count messages in or out for the room's traffic: how many, their bytes and the largest of them, and how many went to a backed-up player. */
|
|
1305
|
+
private countTraffic;
|
|
1306
|
+
/** The room's traffic since the last time it was taken, and start counting afresh. */
|
|
1307
|
+
takeTraffic(): RoomTraffic;
|
|
1308
|
+
/**
|
|
1309
|
+
* The host: the room's first player, when the game server has host controls on. A host whose
|
|
1310
|
+
* connection dropped hands over to the next player meanwhile, so host controls never wait on them.
|
|
1311
|
+
*/
|
|
1312
|
+
get hostPlayerId(): string | null;
|
|
1313
|
+
/**
|
|
1314
|
+
* The host kicks a player, when the game server allows it. Players can't type the message (no free
|
|
1315
|
+
* text between strangers), so the kicked player is told the host removed them.
|
|
1316
|
+
*/
|
|
1317
|
+
private hostKick;
|
|
1318
|
+
/** The host hides, shows, opens, closes, turns friends' Join on or off, names or labels the room, when the game server allows it. */
|
|
1319
|
+
private hostChange;
|
|
1320
|
+
private settings;
|
|
1321
|
+
/** Change isListed, isOpen, friendJoinsEnabled, the name or labels, and tell everyone in the room. */
|
|
1322
|
+
private settle;
|
|
1323
|
+
/** Tell every player and spectator the room's settings and how many are watching. */
|
|
1324
|
+
private tellRoom;
|
|
1325
|
+
/** The players and the spectators: everyone the room tells what happens in it. */
|
|
1326
|
+
private everyone;
|
|
1327
|
+
/** Whether the room sends to this player: they're in the match, or watching. */
|
|
1328
|
+
private isViewer;
|
|
1329
|
+
/** Who a broadcast reaches (BroadcastOptions.audience, already checked). */
|
|
1330
|
+
private audienceOf;
|
|
1331
|
+
/**
|
|
1332
|
+
* Keep the room, or stop keeping it (room.setKept). The transport may turn a keep back (a game past its
|
|
1333
|
+
* free storage), so it hears through listingChanged; players are told nothing.
|
|
1334
|
+
*/
|
|
1335
|
+
setKept(isKept: boolean): void;
|
|
1336
|
+
/** A room made for some of this room's players (room.reserve). Mistakes in the game's call throw. newRoomCode is the one a game server in a sandbox chose. */
|
|
1337
|
+
private reserve;
|
|
1338
|
+
/** Send players to another room of the game (room.moveTo). Mistakes in the game's call throw. */
|
|
1339
|
+
private moveTo;
|
|
1340
|
+
/** A call that needs the transport to offer it and the room to be going: otherwise it throws room_call_unavailable. */
|
|
1341
|
+
private requireCall;
|
|
1342
|
+
/**
|
|
1343
|
+
* The other room's answer to room.moveTo, for onMoved. Players it took who already left, or who
|
|
1344
|
+
* aren't here, are left out.
|
|
1345
|
+
*/
|
|
1346
|
+
moved(toRoomCode: string, answer: MoveAnswer, sent: Player[]): void;
|
|
1347
|
+
/** Reserving rooms and moving players share one limit a minute. */
|
|
1348
|
+
private limit;
|
|
1349
|
+
/** Remove a player (room.kick). Their onLeave runs once the handler that kicked them is done. */
|
|
1350
|
+
kick(playerId: string, kickOptions?: KickOptions): void;
|
|
1351
|
+
/** Keep players out, newest last, up to as many as the room remembers. */
|
|
1352
|
+
private keepOut;
|
|
1353
|
+
/** Run now, or once the handler running now is done, so a handler never runs inside another. */
|
|
1354
|
+
private soon;
|
|
1355
|
+
private flush;
|
|
1356
|
+
private remember;
|
|
1357
|
+
tick(tickDurationSec: number): void;
|
|
1358
|
+
/**
|
|
1359
|
+
* Players who caught up: their latest-only messages go now, and in a turn-based room, players state sync
|
|
1360
|
+
* skipped get a full copy. Ticking rooms check each tick and at each probe answer; your own server's
|
|
1361
|
+
* runner also every 50 ms while isCatchingUp.
|
|
1362
|
+
*/
|
|
1363
|
+
catchUp(): void;
|
|
1364
|
+
/** Whether anything waits for a player to catch up: latest-only messages, or a turn-based room's state. */
|
|
1365
|
+
get isCatchingUp(): boolean;
|
|
1366
|
+
/**
|
|
1367
|
+
* Whether a player's connection is backed up: the transport says (the socket's bufferedAmount on your
|
|
1368
|
+
* own server), or, in Zaisma rooms, a probe of theirs is ROOM_TUNING.backedUpDelayMs later than their
|
|
1369
|
+
* ping with ROOM_TUNING.backedUpSizeBytes or more sent since the last probe they answered.
|
|
1370
|
+
*/
|
|
1371
|
+
isBackedUp(playerId: string): boolean;
|
|
1372
|
+
/** The players who see the state: everyone in the match with a connection, and the spectators. */
|
|
1373
|
+
private stateViewerIds;
|
|
1374
|
+
/** Which viewers are backed up this round; a turn-based room remembers them, to send a full copy once they catch up. */
|
|
1375
|
+
private backedUpViewers;
|
|
1376
|
+
/** A round of state sync (stateSyncEnabled): each player's view, in full or what changed since their last. */
|
|
1377
|
+
private syncState;
|
|
1378
|
+
/** A round of state sync once the handler running now is done, for a room with no handler about to run. */
|
|
1379
|
+
private syncSoon;
|
|
1380
|
+
/** For a game server that runs state sync itself: who's backed up this round, and who needs a full copy (said once). */
|
|
1381
|
+
private stateViewers;
|
|
1382
|
+
/** A frame from a game server that runs state sync itself, checked like anything a game hands the room. */
|
|
1383
|
+
private sendStateFrom;
|
|
1384
|
+
/** Send a state frame to players in the match, with the room's clock, counted as state sync's traffic. */
|
|
1385
|
+
private sendState;
|
|
1386
|
+
/**
|
|
1387
|
+
* A game's message or bytes for some players. Reliable ones go to all of them, counting those that went to
|
|
1388
|
+
* a backed-up player; latest-only ones wait in a slot for each backed-up player, replacing an older one there.
|
|
1389
|
+
*/
|
|
1390
|
+
private deliver;
|
|
1391
|
+
/** A player's waiting latest-only messages, least recently replaced first. */
|
|
1392
|
+
private sendLatest;
|
|
1393
|
+
private sendOut;
|
|
1394
|
+
/**
|
|
1395
|
+
* The match moves to a version published "now": every player hears it's coming at appliesAt, and when
|
|
1396
|
+
* isHeld (a game server with onVersionUpdate) the game server hears it too and may switch sooner.
|
|
1397
|
+
*/
|
|
1398
|
+
startVersionUpdate(versionUpdate: {
|
|
1399
|
+
versionId: string;
|
|
1400
|
+
appliesAt: string;
|
|
1401
|
+
isReloading: boolean;
|
|
1402
|
+
}, isHeld: boolean): void;
|
|
1403
|
+
/** The switch is happening: every player hears it just before the room closes their connections. */
|
|
1404
|
+
tellVersionUpdateNow(): void;
|
|
1405
|
+
/**
|
|
1406
|
+
* Zaisma will end this match at endsAt (an ISO time): every player hears it and onEnding runs, once.
|
|
1407
|
+
* Only Zaisma rooms call it, when the game's hosting pauses.
|
|
1408
|
+
*/
|
|
1409
|
+
startEnding(endsAt: string): void;
|
|
1410
|
+
/** End the match: everyone hears message and why (endReason), and their connections close. */
|
|
1411
|
+
end(message: string, endReason: EndReason): void;
|
|
1412
|
+
/** The players as every player's game lists them, with why each isn't playing. */
|
|
1413
|
+
private list;
|
|
1414
|
+
/** Send a binary frame to some players, counting it like text: toward each one's backlog, and the room's traffic. */
|
|
1415
|
+
private sendFrame;
|
|
1416
|
+
private broadcastPlayers;
|
|
1417
|
+
/**
|
|
1418
|
+
* Send one text to several players, counting its bytes once. A player it drops for falling behind
|
|
1419
|
+
* leaves once everyone has it, so nobody hears of the leave before the rest of this text.
|
|
1420
|
+
*/
|
|
1421
|
+
private sendEach;
|
|
1422
|
+
/**
|
|
1423
|
+
* Send a player a text, counting the bytes that went out to them: past
|
|
1424
|
+
* ROOM_TUNING.maximumUnansweredSentSizeBytes since the last probe they answered, they're dropped;
|
|
1425
|
+
* every ROOM_TUNING.probeSentSizeBytes, they're probed, so a backed-up connection shows as a late answer.
|
|
1426
|
+
* A player whose connection dropped gets nothing: nothing is sent, counted or probed until they're back.
|
|
1427
|
+
*/
|
|
1428
|
+
private sendText;
|
|
1429
|
+
/** Bytes that went out to a player, for the probes and the backlog. */
|
|
1430
|
+
private sent;
|
|
1431
|
+
/** Probe a player for their ping and a clock sample. Probes aren't counted as bytes sent, so a probe never sets off another. */
|
|
1432
|
+
private probe;
|
|
1433
|
+
/** A ticking room probes each player in the match, and each spectator, every ROOM_TUNING.probeIntervalSec, and drops those who stopped answering. */
|
|
1434
|
+
private probeDue;
|
|
1435
|
+
/** A player whose oldest unanswered probe is ROOM_TUNING.probeAnswerTimeoutSec old is dropped. */
|
|
1436
|
+
private checkAnswers;
|
|
1437
|
+
/** A probe answer: the player's ping moves a quarter of the way to this sample. False if it answers no probe of theirs. */
|
|
1438
|
+
private answer;
|
|
1439
|
+
/**
|
|
1440
|
+
* A player whose connection can't keep up: closed at once, so the room holds no growing backlog for
|
|
1441
|
+
* them, and out of the match like any lost connection, once the handler running now is done. Their
|
|
1442
|
+
* game reconnects by itself. The runtime's close event comes later and finds them gone.
|
|
1443
|
+
*/
|
|
1444
|
+
private dropBehind;
|
|
1445
|
+
/** Token bucket per player: a short burst is fine, a flood is dropped. */
|
|
1446
|
+
private take;
|
|
1447
|
+
/** A handler's answer becomes the state if it is one (a plain object or an array); nothing keeps the state, and anything else is a mistake that leaves it. */
|
|
1448
|
+
private keepAnswer;
|
|
1449
|
+
/**
|
|
1450
|
+
* Run a game server handler. A crash in game code is logged and never takes the room down. State sync's
|
|
1451
|
+
* round follows a turn-based room's handlers and a real-time room's tick (isTick).
|
|
1452
|
+
*/
|
|
1453
|
+
private run;
|
|
1454
|
+
}
|
|
1455
|
+
/** Whether a handler's answer can be the state: a plain object (prototype Object.prototype or null) or an array. */
|
|
1456
|
+
export declare function isStateAnswer(given: unknown): boolean;
|
|
1457
|
+
/** The mistake of a handler answering something that isn't a state; the state stays as it was. */
|
|
1458
|
+
export declare function badHandlerAnswer(handlerName: string, answer: unknown): GameServerError;
|
|
1459
|
+
/** The idle check a player's game asked for, from its hello or back message: a timeout within the limits, and whether to check at all. */
|
|
1460
|
+
export declare function clientIdle(given: unknown): ClientIdle;
|
|
1461
|
+
/** A name and labels within the limits, with hidden characters dropped. Fields left out stay out. */
|
|
1462
|
+
export declare function tidyRoomInfo(nameAndLabels: RoomInfo): Partial<Pick<RoomSettings, "name" | "labels">>;
|