@babylonjs-toolkit/server 0.0.0-stage → 9.35.1
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/CUSTOM.md +1690 -0
- package/DEPLOY.md +2012 -0
- package/README.md +1081 -2
- package/lib/AbuseLimits.d.ts +64 -0
- package/lib/AbuseLimits.js +121 -0
- package/lib/Authentication.d.ts +86 -0
- package/lib/Authentication.js +272 -0
- package/lib/BootSessionOwner.d.ts +86 -0
- package/lib/BootSessionOwner.js +116 -0
- package/lib/ColyseusLoggerAdapter.d.ts +32 -0
- package/lib/ColyseusLoggerAdapter.js +73 -0
- package/lib/DedicatedSceneRegistry.d.ts +52 -0
- package/lib/DedicatedSceneRegistry.js +74 -0
- package/lib/DedicatedSession.d.ts +152 -0
- package/lib/DedicatedSession.js +173 -0
- package/lib/DualTransport.d.ts +112 -0
- package/lib/DualTransport.js +197 -0
- package/lib/GameServer.d.ts +64 -0
- package/lib/GameServer.js +647 -0
- package/lib/GameServerConfig.d.ts +387 -0
- package/lib/GameServerConfig.js +1059 -0
- package/lib/GameServerLobbyRoom.d.ts +33 -0
- package/lib/GameServerLobbyRoom.js +56 -0
- package/lib/GameServerLogger.d.ts +111 -0
- package/lib/GameServerLogger.js +190 -0
- package/lib/GameServerMetrics.d.ts +158 -0
- package/lib/GameServerMetrics.js +276 -0
- package/lib/GameServerState.d.ts +164 -0
- package/lib/GameServerState.js +26 -0
- package/lib/HeadlessRuntime.d.ts +316 -0
- package/lib/HeadlessRuntime.js +380 -0
- package/lib/JoinGate.d.ts +34 -0
- package/lib/JoinGate.js +221 -0
- package/lib/NetworkAddresses.d.ts +33 -0
- package/lib/NetworkAddresses.js +90 -0
- package/lib/PlayerTokens.d.ts +17 -0
- package/lib/PlayerTokens.js +24 -0
- package/lib/RoomServerTransport.d.ts +73 -0
- package/lib/RoomServerTransport.js +90 -0
- package/lib/ServerEndpoints.d.ts +133 -0
- package/lib/ServerEndpoints.js +247 -0
- package/lib/ServerInit.d.ts +28 -0
- package/lib/ServerInit.js +113 -0
- package/lib/SessionRoom.d.ts +391 -0
- package/lib/SessionRoom.js +930 -0
- package/lib/TlsCertificates.d.ts +35 -0
- package/lib/TlsCertificates.js +130 -0
- package/lib/admin/AccountRoutes.d.ts +8 -0
- package/lib/admin/AccountRoutes.js +502 -0
- package/lib/admin/AdminAccounts.d.ts +329 -0
- package/lib/admin/AdminAccounts.js +607 -0
- package/lib/admin/AdminApiRouter.d.ts +202 -0
- package/lib/admin/AdminApiRouter.js +465 -0
- package/lib/admin/AdminApiTypes.d.ts +1782 -0
- package/lib/admin/AdminApiTypes.js +4 -0
- package/lib/admin/AdminAudit.d.ts +113 -0
- package/lib/admin/AdminAudit.js +266 -0
- package/lib/admin/AdminCli.d.ts +7 -0
- package/lib/admin/AdminCli.js +351 -0
- package/lib/admin/AdminClock.d.ts +51 -0
- package/lib/admin/AdminClock.js +10 -0
- package/lib/admin/AdminConsole.d.ts +39 -0
- package/lib/admin/AdminConsole.js +325 -0
- package/lib/admin/AdminConstants.d.ts +147 -0
- package/lib/admin/AdminConstants.js +167 -0
- package/lib/admin/AdminCrypto.d.ts +19 -0
- package/lib/admin/AdminCrypto.js +29 -0
- package/lib/admin/AdminExtensionsHost.d.ts +176 -0
- package/lib/admin/AdminExtensionsHost.js +307 -0
- package/lib/admin/AdminPasswords.d.ts +33 -0
- package/lib/admin/AdminPasswords.js +92 -0
- package/lib/admin/AdminPermissions.d.ts +28 -0
- package/lib/admin/AdminPermissions.js +74 -0
- package/lib/admin/AdminRecords.d.ts +196 -0
- package/lib/admin/AdminRecords.js +3 -0
- package/lib/admin/AdminRequestBodies.d.ts +47 -0
- package/lib/admin/AdminRequestBodies.js +84 -0
- package/lib/admin/AdminRequestGate.d.ts +38 -0
- package/lib/admin/AdminRequestGate.js +59 -0
- package/lib/admin/AdminResponses.d.ts +103 -0
- package/lib/admin/AdminResponses.js +164 -0
- package/lib/admin/AdminServices.d.ts +99 -0
- package/lib/admin/AdminServices.js +4 -0
- package/lib/admin/AdminStaticFiles.d.ts +29 -0
- package/lib/admin/AdminStaticFiles.js +97 -0
- package/lib/admin/AdminStore.d.ts +152 -0
- package/lib/admin/AdminStore.js +46 -0
- package/lib/admin/AlertChannels.d.ts +43 -0
- package/lib/admin/AlertChannels.js +120 -0
- package/lib/admin/AlertEngine.d.ts +231 -0
- package/lib/admin/AlertEngine.js +681 -0
- package/lib/admin/AlertRoutes.d.ts +8 -0
- package/lib/admin/AlertRoutes.js +180 -0
- package/lib/admin/ApiKeyRoutes.d.ts +8 -0
- package/lib/admin/ApiKeyRoutes.js +53 -0
- package/lib/admin/ApiKeys.d.ts +63 -0
- package/lib/admin/ApiKeys.js +167 -0
- package/lib/admin/AuditRoutes.d.ts +8 -0
- package/lib/admin/AuditRoutes.js +106 -0
- package/lib/admin/AuthToolRoutes.d.ts +13 -0
- package/lib/admin/AuthToolRoutes.js +164 -0
- package/lib/admin/BanList.d.ts +87 -0
- package/lib/admin/BanList.js +152 -0
- package/lib/admin/CborDecoder.d.ts +18 -0
- package/lib/admin/CborDecoder.js +201 -0
- package/lib/admin/ColyseusCloudClient.d.ts +313 -0
- package/lib/admin/ColyseusCloudClient.js +272 -0
- package/lib/admin/ColyseusCloudRoutes.d.ts +8 -0
- package/lib/admin/ColyseusCloudRoutes.js +91 -0
- package/lib/admin/ColyseusCloudService.d.ts +180 -0
- package/lib/admin/ColyseusCloudService.js +476 -0
- package/lib/admin/ColyseusCloudSettings.d.ts +16 -0
- package/lib/admin/ColyseusCloudSettings.js +29 -0
- package/lib/admin/ColyseusCloudViews.d.ts +105 -0
- package/lib/admin/ColyseusCloudViews.js +371 -0
- package/lib/admin/CommonPasswords.d.ts +6 -0
- package/lib/admin/CommonPasswords.js +19 -0
- package/lib/admin/ConfigOverrides.d.ts +174 -0
- package/lib/admin/ConfigOverrides.js +406 -0
- package/lib/admin/ConfigRoutes.d.ts +8 -0
- package/lib/admin/ConfigRoutes.js +91 -0
- package/lib/admin/ConsoleSessions.d.ts +112 -0
- package/lib/admin/ConsoleSessions.js +201 -0
- package/lib/admin/DedicatedRoutes.d.ts +21 -0
- package/lib/admin/DedicatedRoutes.js +254 -0
- package/lib/admin/ExtensionRoutes.d.ts +8 -0
- package/lib/admin/ExtensionRoutes.js +145 -0
- package/lib/admin/FileAdminStore.d.ts +157 -0
- package/lib/admin/FileAdminStore.js +349 -0
- package/lib/admin/FleetDirectory.d.ts +323 -0
- package/lib/admin/FleetDirectory.js +489 -0
- package/lib/admin/FleetLeader.d.ts +43 -0
- package/lib/admin/FleetLeader.js +89 -0
- package/lib/admin/FleetRoutes.d.ts +55 -0
- package/lib/admin/FleetRoutes.js +465 -0
- package/lib/admin/HealthWarnings.d.ts +10 -0
- package/lib/admin/HealthWarnings.js +95 -0
- package/lib/admin/LeaderboardRoutes.d.ts +8 -0
- package/lib/admin/LeaderboardRoutes.js +569 -0
- package/lib/admin/LogBuffer.d.ts +60 -0
- package/lib/admin/LogBuffer.js +92 -0
- package/lib/admin/LogRedaction.d.ts +15 -0
- package/lib/admin/LogRedaction.js +72 -0
- package/lib/admin/LogRoutes.d.ts +45 -0
- package/lib/admin/LogRoutes.js +307 -0
- package/lib/admin/MaintenanceMode.d.ts +41 -0
- package/lib/admin/MaintenanceMode.js +74 -0
- package/lib/admin/MaintenanceWindows.d.ts +91 -0
- package/lib/admin/MaintenanceWindows.js +207 -0
- package/lib/admin/MetricsRoutes.d.ts +8 -0
- package/lib/admin/MetricsRoutes.js +209 -0
- package/lib/admin/MetricsSampler.d.ts +124 -0
- package/lib/admin/MetricsSampler.js +211 -0
- package/lib/admin/OneTimeCodes.d.ts +60 -0
- package/lib/admin/OneTimeCodes.js +162 -0
- package/lib/admin/OpenIdConnect.d.ts +146 -0
- package/lib/admin/OpenIdConnect.js +335 -0
- package/lib/admin/Passkeys.d.ts +130 -0
- package/lib/admin/Passkeys.js +432 -0
- package/lib/admin/PlayerRoutes.d.ts +20 -0
- package/lib/admin/PlayerRoutes.js +676 -0
- package/lib/admin/RedisAdminStore.d.ts +114 -0
- package/lib/admin/RedisAdminStore.js +258 -0
- package/lib/admin/RolloutCoordinator.d.ts +99 -0
- package/lib/admin/RolloutCoordinator.js +321 -0
- package/lib/admin/ServerEvents.d.ts +57 -0
- package/lib/admin/ServerEvents.js +89 -0
- package/lib/admin/SessionRoutes.d.ts +113 -0
- package/lib/admin/SessionRoutes.js +386 -0
- package/lib/admin/UserRoutes.d.ts +8 -0
- package/lib/admin/UserRoutes.js +214 -0
- package/lib/cli.d.ts +2 -0
- package/lib/cli.js +221 -0
- package/lib/console/assets/index-CI5N7c-T.js +21 -0
- package/lib/console/assets/index-DPitvzLp.css +1 -0
- package/lib/console/favicon.svg +1 -0
- package/lib/console/index.html +16 -0
- package/lib/discord/DiscordOAuth.d.ts +57 -0
- package/lib/discord/DiscordOAuth.js +127 -0
- package/lib/discord/DiscordSessions.d.ts +79 -0
- package/lib/discord/DiscordSessions.js +135 -0
- package/lib/discord/DiscordSignInConstants.d.ts +54 -0
- package/lib/discord/DiscordSignInConstants.js +55 -0
- package/lib/discord/DiscordSignInRoutes.d.ts +51 -0
- package/lib/discord/DiscordSignInRoutes.js +359 -0
- package/lib/index.d.ts +24 -0
- package/lib/index.js +18 -0
- package/lib/leaderboards/FileLeaderboardRankStore.d.ts +293 -0
- package/lib/leaderboards/FileLeaderboardRankStore.js +808 -0
- package/lib/leaderboards/LeaderboardConstants.d.ts +63 -0
- package/lib/leaderboards/LeaderboardConstants.js +70 -0
- package/lib/leaderboards/LeaderboardError.d.ts +15 -0
- package/lib/leaderboards/LeaderboardError.js +20 -0
- package/lib/leaderboards/LeaderboardIdentity.d.ts +44 -0
- package/lib/leaderboards/LeaderboardIdentity.js +72 -0
- package/lib/leaderboards/LeaderboardPublicApi.d.ts +17 -0
- package/lib/leaderboards/LeaderboardPublicApi.js +389 -0
- package/lib/leaderboards/LeaderboardRankStore.d.ts +296 -0
- package/lib/leaderboards/LeaderboardRankStore.js +18 -0
- package/lib/leaderboards/LeaderboardRules.d.ts +122 -0
- package/lib/leaderboards/LeaderboardRules.js +472 -0
- package/lib/leaderboards/LeaderboardScheduler.d.ts +43 -0
- package/lib/leaderboards/LeaderboardScheduler.js +103 -0
- package/lib/leaderboards/LeaderboardService.d.ts +669 -0
- package/lib/leaderboards/LeaderboardService.js +1667 -0
- package/lib/leaderboards/LeaderboardTransfer.d.ts +35 -0
- package/lib/leaderboards/LeaderboardTransfer.js +580 -0
- package/lib/leaderboards/RedisLeaderboardRankStore.d.ts +282 -0
- package/lib/leaderboards/RedisLeaderboardRankStore.js +691 -0
- package/package.json +64 -4
- package/templates/init/Caddyfile +6 -0
- package/templates/init/DEPLOY.md +233 -0
- package/templates/init/Dockerfile +12 -0
- package/templates/init/app.json +21 -0
- package/templates/init/aws/README.md +118 -0
- package/templates/init/aws/ecs-fargate-alb.yml +207 -0
- package/templates/init/aws/ecs-fargate-nlb-quic.yml +210 -0
- package/templates/init/babylon-game-server.config.mjs +158 -0
- package/templates/init/colyseus-cloud/README.md +133 -0
- package/templates/init/colyseus-cloud/babylon-game-server.config.mjs +36 -0
- package/templates/init/colyseus-cloud/ecosystem.config.cjs +35 -0
- package/templates/init/colyseus-cloud/gitignore +6 -0
- package/templates/init/colyseus-cloud/package.json +15 -0
- package/templates/init/digitalocean-app.yaml +30 -0
- package/templates/init/docker-compose.full.yml +45 -0
- package/templates/init/docker-compose.websocket.yml +41 -0
- package/templates/init/fly.toml +56 -0
- package/templates/init/heroku.yml +11 -0
- package/templates/init/kubernetes/deployment.yaml +71 -0
- package/templates/init/kubernetes/redis.yaml +73 -0
- package/templates/init/kubernetes/service.yaml +24 -0
- package/templates/init/monitoring/grafana-dashboard.json +539 -0
- package/templates/init/monitoring/prometheus-alerts.yml +51 -0
- package/templates/init/monitoring/prometheus-scrape.yml +17 -0
- package/templates/init/railway.json +13 -0
- package/templates/init/render.yaml +19 -0
|
@@ -0,0 +1,930 @@
|
|
|
1
|
+
import { CloseCode, getMessageBytes, matchMaker, Protocol, Room, ServerError } from "colyseus";
|
|
2
|
+
import { DedicatedSession } from "./DedicatedSession.js";
|
|
3
|
+
import { getLogger } from "./GameServerLogger.js";
|
|
4
|
+
import { DISCORD_SESSION_ISSUER } from "./discord/DiscordSignInConstants.js";
|
|
5
|
+
import { getGameServerState } from "./GameServerState.js";
|
|
6
|
+
import { getToolkitRuntime } from "./HeadlessRuntime.js";
|
|
7
|
+
import { authorizeJoin, requestedDedicatedScene } from "./JoinGate.js";
|
|
8
|
+
import { normalizeAddress } from "./NetworkAddresses.js";
|
|
9
|
+
import { KICK_REJOIN_BLOCK_SECONDS, ROOM_TRAFFIC_WINDOW_MS } from "./admin/AdminConstants.js";
|
|
10
|
+
import { toLogEntryView } from "./admin/LogBuffer.js";
|
|
11
|
+
/** Milliseconds between session logic updates, which expire reconnect holds (A9). */
|
|
12
|
+
const SESSION_UPDATE_INTERVAL_MS = 250;
|
|
13
|
+
/** Close code of a refused join; the error message names the TOOLKIT.ShutdownReason. */
|
|
14
|
+
const JOIN_REFUSED_CODE = 4011;
|
|
15
|
+
/** Prefix of the control messages the room forwards to the session logic (D11). */
|
|
16
|
+
const CONTROL_MESSAGE_PREFIX = "fusion:";
|
|
17
|
+
/** HTTP status of a session refused because the process is at a session cap (D14) or its scene failed to load. */
|
|
18
|
+
const SERVICE_UNAVAILABLE_STATUS = 503;
|
|
19
|
+
/** HTTP status of a Client join naming a dedicated scene the server cannot start on demand (D6). */
|
|
20
|
+
const NOT_FOUND_STATUS = 404;
|
|
21
|
+
/** Milliseconds per second. */
|
|
22
|
+
const MILLISECONDS_PER_SECOND = 1000;
|
|
23
|
+
/** The game mode name the metrics and the admin listing show before a session's first join. */
|
|
24
|
+
const PENDING_GAME_MODE_NAME = "Pending";
|
|
25
|
+
/** Bytes of one state word in the session logic's stored blocks (the inspector counts words). */
|
|
26
|
+
const STATE_WORD_BYTES = 4;
|
|
27
|
+
/** How many of a room's newest log lines its admin page shows. */
|
|
28
|
+
const ROOM_LOG_LINE_COUNT = 50;
|
|
29
|
+
/** Why Shared-mode inspection may be missing. */
|
|
30
|
+
const NO_STORED_STATE_REASON = "this session keeps no object state on the server (only Shared mode does)";
|
|
31
|
+
/** Why a dedicated session's objects may be missing from the inspector. */
|
|
32
|
+
const DEDICATED_INSPECTION_MISSING_REASON = "the dedicated runner does not list its network objects";
|
|
33
|
+
/**
|
|
34
|
+
* Settles a reconnection window that closed unused or ended because the room is disposing. Nothing is left to do: the
|
|
35
|
+
* player's onLeave (or the room's disposal) already cleans up, and an unhandled rejection would crash strict runtimes.
|
|
36
|
+
*/
|
|
37
|
+
function ignoreEndedReconnection() {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Settles a promise whose failure is already reported elsewhere (an on-demand load failure reaches each joiner through
|
|
42
|
+
* onJoin, and locking a room that is closing has nothing left to do), so it never becomes an unhandled rejection.
|
|
43
|
+
*/
|
|
44
|
+
function ignoreReportedFailure() {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The Colyseus room of one network session ("fusion_session", D10). It hosts TOOLKIT.FusionSessionLogic, the same class
|
|
49
|
+
* the in-process LoopbackHub runs: the room only carries bytes and control messages between the sockets and the logic.
|
|
50
|
+
* Client-to-room frames arrive as bytes type FUSION_BYTES_TO_ROOM; room-to-client frames leave as FUSION_BYTES_FROM_ROOM
|
|
51
|
+
* prefixed with their source player (D11). Unreliable frames travel as WebTransport datagrams (FUSION_UNRELIABLE_TO_ROOM /
|
|
52
|
+
* FUSION_UNRELIABLE_FROM_ROOM) when the client is on WebTransport, else on the same reliable bytes messages (D7).
|
|
53
|
+
*/
|
|
54
|
+
export class SessionRoom extends Room {
|
|
55
|
+
/** The runtime's TOOLKIT namespace (Babylon Toolkit Networking). */
|
|
56
|
+
toolkit;
|
|
57
|
+
/** The session rules. */
|
|
58
|
+
logic;
|
|
59
|
+
/** Seats by client session id, for reconnects. */
|
|
60
|
+
seats = new Map();
|
|
61
|
+
/** The headless Server-mode session a dedicated room runs, or null (T13). */
|
|
62
|
+
dedicatedSession = null;
|
|
63
|
+
/** True for a room that runs a headless Server-mode runner (DedicatedSession). */
|
|
64
|
+
hostsDedicatedRunner = false;
|
|
65
|
+
/** The game server this room belongs to (limits, hooks, metrics). */
|
|
66
|
+
serverState;
|
|
67
|
+
/** The session name, for hooks and logs. */
|
|
68
|
+
sessionName = "";
|
|
69
|
+
/** The TOOLKIT.GameMode of the joiner that created the session, or null. */
|
|
70
|
+
creatorGameMode = null;
|
|
71
|
+
/** Seconds an on-demand room stays open empty, or null for every other room (they never idle-close, D19). */
|
|
72
|
+
idleSeconds = null;
|
|
73
|
+
/** The configured scene an on-demand room runs, for logs. */
|
|
74
|
+
onDemandSceneName = "";
|
|
75
|
+
/** The scene a dedicated room runs (its configured name, else its scene file), or null for other rooms. */
|
|
76
|
+
dedicatedSceneName = null;
|
|
77
|
+
/** Who started this room's dedicated runner (game-server-admin-console T9). */
|
|
78
|
+
dedicatedStartedBy = "code";
|
|
79
|
+
/** The pending idle close of an empty on-demand room, or null. */
|
|
80
|
+
idleTimer = null;
|
|
81
|
+
/** Settles once an on-demand room's runner has loaded (rejects when it failed), or null for other rooms. */
|
|
82
|
+
dedicatedRunnerReady = null;
|
|
83
|
+
/** True once the room was disposed. */
|
|
84
|
+
isDisposed = false;
|
|
85
|
+
/** ISO time the room was created. */
|
|
86
|
+
createdAt = null;
|
|
87
|
+
/** The last session description the logic published, or null before the first. */
|
|
88
|
+
lastSessionInfo = null;
|
|
89
|
+
/** Frame totals since creation (counted whether or not metrics are on). */
|
|
90
|
+
traffic = { framesIn: 0, framesOut: 0, bytesIn: 0, bytesOut: 0 };
|
|
91
|
+
/** Start of the current traffic window, in performance.now() milliseconds. */
|
|
92
|
+
trafficWindowStartedAt = performance.now();
|
|
93
|
+
/** Total bytes at the start of the current traffic window. */
|
|
94
|
+
trafficWindowBytes = 0;
|
|
95
|
+
/** Bytes per second over the last full ROOM_TRAFFIC_WINDOW_MS. */
|
|
96
|
+
bytesPerSecond = 0;
|
|
97
|
+
/** Host migrations seen in updateSession (logic.isMigratingHost turning false). */
|
|
98
|
+
hostMigrationLog = [];
|
|
99
|
+
/** The host seen while no migration ran, or null. */
|
|
100
|
+
lastHostPlayerId = null;
|
|
101
|
+
/** Whether a host migration was running at the last update. */
|
|
102
|
+
wasMigratingHost = false;
|
|
103
|
+
/** True when a dedicated Server-mode runner hosts this session. */
|
|
104
|
+
get isDedicated() {
|
|
105
|
+
return this.hostsDedicatedRunner;
|
|
106
|
+
}
|
|
107
|
+
/** The session's TOOLKIT.GameMode name ("Shared", "Host", "Server", …), or "Pending" before its first join. */
|
|
108
|
+
get gameModeName() {
|
|
109
|
+
const gameMode = (this.logic != null) ? this.logic.gameMode : null;
|
|
110
|
+
if (gameMode == null)
|
|
111
|
+
return PENDING_GAME_MODE_NAME;
|
|
112
|
+
return this.toolkit.GameMode[gameMode] ?? String(gameMode);
|
|
113
|
+
}
|
|
114
|
+
/** Clients connected to the room now. */
|
|
115
|
+
get playerCount() {
|
|
116
|
+
return this.clients.length;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The join gate of sessions (D10): runs at matchmaking, before a seat is reserved.
|
|
120
|
+
* @param token The bearer token the client sent.
|
|
121
|
+
* @param options The join request.
|
|
122
|
+
* @param context Colyseus' matchmaking context.
|
|
123
|
+
* @returns The identity and client address, stored as client.auth.
|
|
124
|
+
* @throws ServerError when the gate refuses the join.
|
|
125
|
+
*/
|
|
126
|
+
static async onAuth(token, options, context) {
|
|
127
|
+
return authorizeJoin(token, options, context, "session");
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Creates the session logic and wires the message handlers. The first join creates the session itself (A10); a room
|
|
131
|
+
* created with `dedicated` (DedicatedSession.start) runs a headless Server-mode runner first, and stays open while
|
|
132
|
+
* empty. A Client-mode join naming a configured on-demand scene (NETWORK_DEDICATED_SCENE_PROPERTY) starts that
|
|
133
|
+
* scene's dedicated runner here, and the room closes once it has been empty for the scene's idleSeconds (D19).
|
|
134
|
+
* @param options The creation options: a joiner's request, or DedicatedRoomOptions.
|
|
135
|
+
* @throws ServerError(503, "MaxCcuReached: …") when the process already hosts limits.maxSessions sessions, or
|
|
136
|
+
* limits.maxDedicatedSessions dedicated ones.
|
|
137
|
+
* @throws ServerError(404, "GameNotFound: …") when a Client join names a scene that is not configured for on demand.
|
|
138
|
+
*/
|
|
139
|
+
async onCreate(options) {
|
|
140
|
+
const state = getGameServerState();
|
|
141
|
+
this.serverState = state;
|
|
142
|
+
this.toolkit = getToolkitRuntime().TOOLKIT;
|
|
143
|
+
if (state.sessionRooms.size >= state.config.limits.maxSessions)
|
|
144
|
+
throw new ServerError(SERVICE_UNAVAILABLE_STATUS, "MaxCcuReached: this server is hosting its most sessions");
|
|
145
|
+
state.sessionRooms.add(this);
|
|
146
|
+
this.createdAt = new Date().toISOString();
|
|
147
|
+
this.maxMessagesPerSecond = state.config.limits.maxMessagesPerSecond;
|
|
148
|
+
this.sessionName = options?.sessionName ?? "";
|
|
149
|
+
this.creatorGameMode = (typeof options?.gameMode === "number") ? options.gameMode : null;
|
|
150
|
+
try {
|
|
151
|
+
const onDemandOptions = this.resolveOnDemandScene(options);
|
|
152
|
+
if (onDemandOptions != null || options?.dedicated === true)
|
|
153
|
+
this.checkDedicatedCap();
|
|
154
|
+
await this.createSession(options, onDemandOptions);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
state.sessionRooms.delete(this);
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
if (this.idleSeconds != null)
|
|
161
|
+
this.startIdleTimer();
|
|
162
|
+
this.callSessionHook("onSessionCreated");
|
|
163
|
+
}
|
|
164
|
+
/** Stops a dedicated room's server runner with the room, and leaves the server's live sessions. */
|
|
165
|
+
async onDispose() {
|
|
166
|
+
this.isDisposed = true;
|
|
167
|
+
this.serverState.sessionRooms.delete(this);
|
|
168
|
+
const dedicatedSession = this.dedicatedSession;
|
|
169
|
+
this.dedicatedSession = null;
|
|
170
|
+
if (dedicatedSession != null)
|
|
171
|
+
await dedicatedSession.stop();
|
|
172
|
+
this.callSessionHook("onSessionClosed");
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Admits a client through the session logic (A10). The requested player count is clamped to the server's cap and the
|
|
176
|
+
* identity is always the one the gate verified, never a client-sent value (D12, D14). A join of an on-demand room waits
|
|
177
|
+
* until its dedicated runner has loaded.
|
|
178
|
+
* @param client The joining client.
|
|
179
|
+
* @param options Its join request.
|
|
180
|
+
* @throws ServerError with JOIN_REFUSED_CODE and the ShutdownReason name when the session refuses the join.
|
|
181
|
+
* @throws ServerError(503, "GameNotFound: …") when the on-demand scene failed to load.
|
|
182
|
+
*/
|
|
183
|
+
async onJoin(client, options) {
|
|
184
|
+
if (this.dedicatedRunnerReady != null)
|
|
185
|
+
await this.dedicatedRunnerReady;
|
|
186
|
+
// Only a runner restarting after a host migration names the player it resumes; the logic accepts that only for
|
|
187
|
+
// the players it is waiting for (D31). Other reconnects go through Colyseus reconnection (onReconnect).
|
|
188
|
+
const reconnectPlayerId = (options != null && options.hostMigration === true) ? options.reconnectPlayerId : 0;
|
|
189
|
+
const gateResult = client.auth;
|
|
190
|
+
const identity = gateResult?.identity ?? null;
|
|
191
|
+
const request = Object.assign({}, options, { reconnectPlayerId: reconnectPlayerId, authIdentity: identity });
|
|
192
|
+
request.playerCount = this.clampPlayerCount(request.playerCount);
|
|
193
|
+
const welcome = this.admitCounted(client.sessionId, request);
|
|
194
|
+
this.idleTimer?.clear();
|
|
195
|
+
this.idleTimer = null;
|
|
196
|
+
const seat = {
|
|
197
|
+
request: Object.assign({}, request, { hostMigration: false }),
|
|
198
|
+
playerId: welcome.playerId,
|
|
199
|
+
clientAddress: gateResult?.clientAddress ?? "",
|
|
200
|
+
transport: transportOf(client),
|
|
201
|
+
joinedAt: new Date().toISOString()
|
|
202
|
+
};
|
|
203
|
+
this.seats.set(client.sessionId, seat);
|
|
204
|
+
this.refreshAutoDispose();
|
|
205
|
+
this.serverState.metrics?.countJoin("accepted", null);
|
|
206
|
+
this.callPlayerHook("onPlayerJoined", welcome.playerId, identity);
|
|
207
|
+
this.reportPlayerSession("joined", seat);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Ends the session at the end of a drain (D17): every client is told GameClosed (FUSION_CONTROL_SHUTDOWN), then the
|
|
211
|
+
* room disconnects them with CloseCode.SERVER_SHUTDOWN.
|
|
212
|
+
*/
|
|
213
|
+
async closeForShutdown() {
|
|
214
|
+
const payload = { reason: this.toolkit.ShutdownReason.GameClosed };
|
|
215
|
+
for (const client of this.clients)
|
|
216
|
+
client.send(this.toolkit.FUSION_CONTROL_SHUTDOWN, payload);
|
|
217
|
+
await this.disconnect(CloseCode.SERVER_SHUTDOWN);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* A client dropped without leaving: the logic holds its slot and the room keeps its seat for the grace period (D8).
|
|
221
|
+
* Colyseus also calls it for a join that onJoin refused; such a client has no seat and nothing to hold.
|
|
222
|
+
* @param client The dropped client.
|
|
223
|
+
*/
|
|
224
|
+
onDrop(client) {
|
|
225
|
+
if (!this.seats.has(client.sessionId))
|
|
226
|
+
return;
|
|
227
|
+
this.logic.drop(client.sessionId);
|
|
228
|
+
this.refreshAutoDispose();
|
|
229
|
+
this.allowReconnection(client, this.toolkit.NETWORK_RECONNECT_GRACE_SECONDS).catch(ignoreEndedReconnection);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* A dropped client came back within the grace period: it resumes its player id.
|
|
233
|
+
* @param client The reconnected client.
|
|
234
|
+
*/
|
|
235
|
+
onReconnect(client) {
|
|
236
|
+
const seat = this.seats.get(client.sessionId);
|
|
237
|
+
if (seat == null)
|
|
238
|
+
return;
|
|
239
|
+
this.admit(client.sessionId, Object.assign({}, seat.request, { reconnectPlayerId: seat.playerId }));
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* A client left for good (or its reconnect window closed).
|
|
243
|
+
* @param client The leaving client.
|
|
244
|
+
*/
|
|
245
|
+
onLeave(client) {
|
|
246
|
+
const seat = this.seats.get(client.sessionId);
|
|
247
|
+
this.seats.delete(client.sessionId);
|
|
248
|
+
this.logic.leave(client.sessionId);
|
|
249
|
+
this.refreshAutoDispose();
|
|
250
|
+
if (seat != null)
|
|
251
|
+
this.callPlayerHook("onPlayerLeft", seat.playerId, seat.request.authIdentity ?? null);
|
|
252
|
+
if (seat != null)
|
|
253
|
+
this.reportPlayerSession("left", seat);
|
|
254
|
+
if (this.idleSeconds != null && this.clients.length === 0)
|
|
255
|
+
this.startIdleTimer();
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* This session as a row of the admin session list (game-server-admin-console R18).
|
|
259
|
+
* @returns The row.
|
|
260
|
+
*/
|
|
261
|
+
adminSummary() {
|
|
262
|
+
return {
|
|
263
|
+
roomId: this.roomId,
|
|
264
|
+
sessionName: this.sessionName,
|
|
265
|
+
gameMode: this.gameModeName,
|
|
266
|
+
players: this.clients.length,
|
|
267
|
+
maxPlayers: this.maxClients,
|
|
268
|
+
processId: matchMaker.processId,
|
|
269
|
+
publicAddress: this.serverState.config.publicAddress,
|
|
270
|
+
createdAt: this.createdAt,
|
|
271
|
+
dedicatedScene: this.dedicatedSceneName,
|
|
272
|
+
hostPlayerId: this.currentHostPlayerId(),
|
|
273
|
+
isOpen: this.lastSessionInfo?.isOpen ?? true,
|
|
274
|
+
isVisible: this.lastSessionInfo?.isVisible ?? true,
|
|
275
|
+
locked: this.locked,
|
|
276
|
+
bytesPerSecond: this.bytesPerSecond
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* This session with its players for the admin session page (DR §6.6).
|
|
281
|
+
* @param includeAddresses False hides each player's address (callers without players.viewAddresses).
|
|
282
|
+
* @returns The session page.
|
|
283
|
+
*/
|
|
284
|
+
adminDescribe(includeAddresses) {
|
|
285
|
+
const firstSeat = this.seats.values().next().value;
|
|
286
|
+
return Object.assign(this.adminSummary(), {
|
|
287
|
+
properties: Object.assign({}, this.lastSessionInfo?.properties ?? {}),
|
|
288
|
+
tickRate: firstSeat?.request.tickRate ?? null,
|
|
289
|
+
playerList: this.describePlayers(includeAddresses),
|
|
290
|
+
traffic: Object.assign({}, this.traffic),
|
|
291
|
+
dedicated: this.describeDedicated(),
|
|
292
|
+
hostMigrations: this.hostMigrationLog.slice(),
|
|
293
|
+
logLines: this.recentLogLines(includeAddresses)
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The verified Discord players of this session (discord-sign-in D17): each seat whose identity is one of this
|
|
298
|
+
* server's Discord sessions. Typed names and other auth identities are left out.
|
|
299
|
+
* @returns The players, by player id.
|
|
300
|
+
*/
|
|
301
|
+
discordPlayers() {
|
|
302
|
+
const players = [];
|
|
303
|
+
for (const seat of this.seats.values()) {
|
|
304
|
+
const identity = seat.request.authIdentity ?? null;
|
|
305
|
+
if (identity == null || identity.userId == null || identity.claims.iss !== DISCORD_SESSION_ISSUER)
|
|
306
|
+
continue;
|
|
307
|
+
const avatarClaim = identity.claims.avatar;
|
|
308
|
+
players.push({ playerId: seat.playerId, userId: identity.userId, displayName: displayNameOf(identity) ?? "", avatar: (typeof avatarClaim === "string") ? avatarClaim : null });
|
|
309
|
+
}
|
|
310
|
+
return players.sort((left, right) => left.playerId - right.playerId);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* The session's network objects (the inspector, spec 4): a dedicated runner's objects, or the object state a
|
|
314
|
+
* Shared-mode session stores for late joiners.
|
|
315
|
+
* @returns The objects, or available false with the reason.
|
|
316
|
+
*/
|
|
317
|
+
adminInspect() {
|
|
318
|
+
if (this.dedicatedSession != null)
|
|
319
|
+
return inspectDedicatedObjects(this.dedicatedSession.runner.getAllNetworkObjects?.() ?? null);
|
|
320
|
+
const storedObjects = this.logic.sharedObjects;
|
|
321
|
+
if (storedObjects == null || this.logic.gameMode !== this.toolkit.GameMode.Shared)
|
|
322
|
+
return { available: false, reason: NO_STORED_STATE_REASON, objects: [] };
|
|
323
|
+
return { available: true, reason: null, objects: Array.from(storedObjects.values()).map(describeSharedObject) };
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Kicks a player (D12): it is told DisconnectedByPluginLogic with the admin's message, refused a fresh join to this
|
|
327
|
+
* session for KICK_REJOIN_BLOCK_SECONDS unless allowRejoin, and disconnected with CloseCode.CONSENTED, so no
|
|
328
|
+
* reconnect seat is held.
|
|
329
|
+
* @param playerId The player.
|
|
330
|
+
* @param message Shown to the player.
|
|
331
|
+
* @param allowRejoin True to let the player join again at once.
|
|
332
|
+
* @returns False when no connected player has that id.
|
|
333
|
+
*/
|
|
334
|
+
async adminKickPlayer(playerId, message, allowRejoin) {
|
|
335
|
+
const sessionId = this.sessionIdOfPlayer(playerId);
|
|
336
|
+
const client = (sessionId != null) ? this.clients.getById(sessionId) : undefined;
|
|
337
|
+
const seat = (sessionId != null) ? this.seats.get(sessionId) : undefined;
|
|
338
|
+
if (client == null || seat == null)
|
|
339
|
+
return false;
|
|
340
|
+
client.send(this.toolkit.FUSION_CONTROL_SHUTDOWN, { reason: this.toolkit.ShutdownReason.DisconnectedByPluginLogic, message: message });
|
|
341
|
+
if (!allowRejoin)
|
|
342
|
+
await this.blockRejoin(seat);
|
|
343
|
+
client.leave(CloseCode.CONSENTED);
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Closes the session from the console (D12): every player is told GameClosed with the admin's message, then the room
|
|
348
|
+
* disconnects them with CloseCode.SERVER_SHUTDOWN.
|
|
349
|
+
* @param message Shown to the players.
|
|
350
|
+
*/
|
|
351
|
+
async adminClose(message) {
|
|
352
|
+
const payload = { reason: this.toolkit.ShutdownReason.GameClosed, message: message };
|
|
353
|
+
for (const client of this.clients)
|
|
354
|
+
client.send(this.toolkit.FUSION_CONTROL_SHUTDOWN, payload);
|
|
355
|
+
await this.disconnect(CloseCode.SERVER_SHUTDOWN);
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Locks or unlocks the session against new joins from the console.
|
|
359
|
+
* @param locked True to lock.
|
|
360
|
+
*/
|
|
361
|
+
async adminSetLocked(locked) {
|
|
362
|
+
if (locked)
|
|
363
|
+
await this.lock();
|
|
364
|
+
else
|
|
365
|
+
await this.unlock();
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Sends a notice to one player or to everyone (FUSION_CONTROL_NOTICE, D13).
|
|
369
|
+
* @param notice The notice.
|
|
370
|
+
* @param playerId The player, or 0 for everyone.
|
|
371
|
+
* @returns How many connected players were targeted.
|
|
372
|
+
*/
|
|
373
|
+
adminSendNotice(notice, playerId) {
|
|
374
|
+
this.logic.sendControlFromServer(this.toolkit.FUSION_CONTROL_NOTICE, notice, playerId);
|
|
375
|
+
const connected = this.logic.players.filter((player) => player.connected);
|
|
376
|
+
if (playerId === 0)
|
|
377
|
+
return connected.length;
|
|
378
|
+
return connected.some((player) => player.playerId === playerId) ? 1 : 0;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Stops a dedicated session from the console (D23, R17): every player is told GameClosed with the admin's message,
|
|
382
|
+
* and the room disconnects on the next turn of the event loop, so this call's reply reaches the caller first.
|
|
383
|
+
* onDispose then stops the runner. Nothing restarts it by itself, boot sessions included (DR §9).
|
|
384
|
+
* @param message Shown to the players.
|
|
385
|
+
* @returns What a restart needs, or null when this room runs no dedicated runner (nothing is stopped).
|
|
386
|
+
*/
|
|
387
|
+
adminStopDedicated(message) {
|
|
388
|
+
if (!this.hostsDedicatedRunner || this.dedicatedSceneName == null)
|
|
389
|
+
return null;
|
|
390
|
+
const payload = { reason: this.toolkit.ShutdownReason.GameClosed, message: message };
|
|
391
|
+
for (const client of this.clients)
|
|
392
|
+
client.send(this.toolkit.FUSION_CONTROL_SHUTDOWN, payload);
|
|
393
|
+
setImmediate(() => {
|
|
394
|
+
this.disconnect(CloseCode.SERVER_SHUTDOWN).catch((error) => getLogger().warn("a stopped dedicated session did not close cleanly", { roomId: this.roomId, error: String(error) }));
|
|
395
|
+
});
|
|
396
|
+
return { sceneName: this.dedicatedSceneName, sessionName: this.sessionName, processId: matchMaker.processId };
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Disconnects the playing clients a new ban matches (D15): each is told ConnectionRefused with the message.
|
|
400
|
+
* @param userIds Banned user ids.
|
|
401
|
+
* @param addresses Banned addresses (concrete addresses of this room's clients; ranges are resolved by the caller).
|
|
402
|
+
* @param message Shown to the players.
|
|
403
|
+
* @returns How many clients were disconnected.
|
|
404
|
+
*/
|
|
405
|
+
adminDisconnectBanned(userIds, addresses, message) {
|
|
406
|
+
const bannedAddresses = new Set(addresses.map(normalizeAddress));
|
|
407
|
+
let disconnected = 0;
|
|
408
|
+
for (const client of Array.from(this.clients)) {
|
|
409
|
+
const seat = this.seats.get(client.sessionId);
|
|
410
|
+
if (seat == null)
|
|
411
|
+
continue;
|
|
412
|
+
const userId = seat.request.authIdentity?.userId ?? null;
|
|
413
|
+
const isBanned = (userId != null && userIds.includes(userId)) || bannedAddresses.has(normalizeAddress(seat.clientAddress));
|
|
414
|
+
if (!isBanned)
|
|
415
|
+
continue;
|
|
416
|
+
client.send(this.toolkit.FUSION_CONTROL_SHUTDOWN, { reason: this.toolkit.ShutdownReason.ConnectionRefused, message: message });
|
|
417
|
+
client.leave(CloseCode.CONSENTED);
|
|
418
|
+
disconnected += 1;
|
|
419
|
+
}
|
|
420
|
+
return disconnected;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Builds the session logic and its message handlers. A room created by DedicatedSession.start loads its headless
|
|
424
|
+
* runner before it is listed; an on-demand room starts loading and is listed at once, so concurrent joinOrCreate
|
|
425
|
+
* calls for the same session find this room instead of starting a second runner (Colyseus waits only about half a
|
|
426
|
+
* second for a concurrent create, less than a scene takes to load). Its joins wait for the runner in onJoin.
|
|
427
|
+
* @param options The creation options: a joiner's request, or DedicatedRoomOptions.
|
|
428
|
+
* @param onDemandOptions The runner options of an on-demand scene, or null.
|
|
429
|
+
*/
|
|
430
|
+
async createSession(options, onDemandOptions) {
|
|
431
|
+
this.logic = new this.toolkit.FusionSessionLogic(this.createOutputs());
|
|
432
|
+
this.clock.setInterval(() => this.updateSession(), SESSION_UPDATE_INTERVAL_MS);
|
|
433
|
+
this.onMessageBytes(this.toolkit.FUSION_BYTES_TO_ROOM, (client, bytes) => this.receiveFrame(client, bytes, this.toolkit.RpcChannel.Reliable));
|
|
434
|
+
this.onMessage(this.toolkit.FUSION_UNRELIABLE_TO_ROOM, (client, payload) => {
|
|
435
|
+
if (payload instanceof Uint8Array)
|
|
436
|
+
this.receiveFrame(client, payload, this.toolkit.RpcChannel.Unreliable);
|
|
437
|
+
});
|
|
438
|
+
this.onMessage("*", (client, type, payload) => this.receiveControl(client, type, payload));
|
|
439
|
+
if (onDemandOptions != null) {
|
|
440
|
+
this.rememberDedicatedScene(onDemandOptions);
|
|
441
|
+
this.hostsDedicatedRunner = true;
|
|
442
|
+
this.autoDispose = false;
|
|
443
|
+
this.dedicatedRunnerReady = this.loadOnDemandRunner(onDemandOptions);
|
|
444
|
+
// A failed load is reported to each joiner by onJoin; this handler only keeps it from being unhandled.
|
|
445
|
+
this.dedicatedRunnerReady.catch(ignoreReportedFailure);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (options == null || options.dedicated !== true)
|
|
449
|
+
return;
|
|
450
|
+
this.rememberDedicatedScene(options);
|
|
451
|
+
this.hostsDedicatedRunner = true;
|
|
452
|
+
this.autoDispose = false;
|
|
453
|
+
this.dedicatedSession = await DedicatedSession.attach(this.logic, options);
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Loads an on-demand room's headless runner. A scene that fails to load is logged with its cause, the room is locked
|
|
457
|
+
* (a later join creates a fresh room and tries again) and it closes after its idle time (§Error policy).
|
|
458
|
+
* @param options The runner options from the registry.
|
|
459
|
+
* @throws ServerError(503, "GameNotFound: the dedicated scene <name> failed to load").
|
|
460
|
+
*/
|
|
461
|
+
async loadOnDemandRunner(options) {
|
|
462
|
+
let dedicatedSession;
|
|
463
|
+
try {
|
|
464
|
+
dedicatedSession = await DedicatedSession.attach(this.logic, options);
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
getLogger().error("the dedicated scene " + this.onDemandSceneName + " failed to load", { sessionName: this.sessionName, roomId: this.roomId, error: String(error) });
|
|
468
|
+
this.lock().catch(ignoreReportedFailure);
|
|
469
|
+
throw new ServerError(SERVICE_UNAVAILABLE_STATUS, "GameNotFound: the dedicated scene " + this.onDemandSceneName + " failed to load");
|
|
470
|
+
}
|
|
471
|
+
// The room may have closed while the scene loaded (every joiner left and the idle time passed).
|
|
472
|
+
if (this.isDisposed)
|
|
473
|
+
await dedicatedSession.stop();
|
|
474
|
+
else
|
|
475
|
+
this.dedicatedSession = dedicatedSession;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* The dedicated room options of a Client-mode join that names an on-demand scene (D19), built from the registry entry,
|
|
479
|
+
* never from client-sent locations.
|
|
480
|
+
* @param options The creation options.
|
|
481
|
+
* @returns The options, or null when the join names no dedicated scene.
|
|
482
|
+
* @throws ServerError(404, "GameNotFound: …") when the scene is not configured, or not for on demand.
|
|
483
|
+
*/
|
|
484
|
+
resolveOnDemandScene(options) {
|
|
485
|
+
if (options == null || options.dedicated === true)
|
|
486
|
+
return null;
|
|
487
|
+
const sceneName = requestedDedicatedScene(options);
|
|
488
|
+
if (sceneName == null)
|
|
489
|
+
return null;
|
|
490
|
+
const scene = this.serverState.registry.find(sceneName);
|
|
491
|
+
if (scene == null || !scene.onDemand)
|
|
492
|
+
throw new ServerError(NOT_FOUND_STATUS, "GameNotFound: no on-demand dedicated scene named " + sceneName);
|
|
493
|
+
this.onDemandSceneName = sceneName;
|
|
494
|
+
this.idleSeconds = scene.idleSeconds;
|
|
495
|
+
this.creatorGameMode = this.toolkit.GameMode.Server;
|
|
496
|
+
return {
|
|
497
|
+
sessionName: this.sessionName,
|
|
498
|
+
sceneRootUrl: scene.sceneRootUrl,
|
|
499
|
+
sceneFile: scene.sceneFile,
|
|
500
|
+
projectScripts: scene.projectScripts,
|
|
501
|
+
playerCount: scene.maxPlayers,
|
|
502
|
+
sceneName: sceneName,
|
|
503
|
+
startedBy: "onDemand",
|
|
504
|
+
gameMode: this.toolkit.GameMode.Server,
|
|
505
|
+
dedicated: true
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* The newest buffered log lines of this room for its admin page (R18).
|
|
510
|
+
* @param includeAddresses False removes client addresses (callers without players.viewAddresses).
|
|
511
|
+
* @returns The lines, oldest first (none while the console is off).
|
|
512
|
+
*/
|
|
513
|
+
recentLogLines(includeAddresses) {
|
|
514
|
+
const entries = this.serverState.logBuffer?.read({ roomId: this.roomId }, 0, ROOM_LOG_LINE_COUNT) ?? [];
|
|
515
|
+
return entries.map((entry) => toLogEntryView(entry, matchMaker.processId, includeAddresses));
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Keeps a dedicated room's scene and starter for the console.
|
|
519
|
+
* @param options The dedicated room options.
|
|
520
|
+
*/
|
|
521
|
+
rememberDedicatedScene(options) {
|
|
522
|
+
this.dedicatedSceneName = options.sceneName ?? options.sceneFile;
|
|
523
|
+
this.dedicatedStartedBy = options.startedBy ?? "code";
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* The dedicated part of the session page (D23): the scene, who started it and the frame history (empty while an
|
|
527
|
+
* on-demand runner still loads).
|
|
528
|
+
* @returns The part, or null for a session without a dedicated runner.
|
|
529
|
+
*/
|
|
530
|
+
describeDedicated() {
|
|
531
|
+
if (this.dedicatedSceneName == null)
|
|
532
|
+
return null;
|
|
533
|
+
const frameStatistics = this.dedicatedSession?.statistics.buckets() ?? [];
|
|
534
|
+
return { sceneName: this.dedicatedSceneName, startedBy: this.dedicatedStartedBy, frameStatistics: frameStatistics };
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Refuses another dedicated room once the process runs limits.maxDedicatedSessions of them (D14). This room is
|
|
538
|
+
* already in the live set, so only the others count.
|
|
539
|
+
* @throws ServerError(503, "MaxCcuReached: …").
|
|
540
|
+
*/
|
|
541
|
+
checkDedicatedCap() {
|
|
542
|
+
const otherDedicatedRooms = Array.from(this.serverState.sessionRooms).filter((room) => room !== this && room.isDedicated).length;
|
|
543
|
+
if (otherDedicatedRooms < this.serverState.config.limits.maxDedicatedSessions)
|
|
544
|
+
return;
|
|
545
|
+
throw new ServerError(SERVICE_UNAVAILABLE_STATUS, "MaxCcuReached: this server is running its most dedicated sessions");
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Closes an on-demand room after idleSeconds without clients (D19); a join before then cancels it (onJoin).
|
|
549
|
+
*/
|
|
550
|
+
startIdleTimer() {
|
|
551
|
+
this.idleTimer?.clear();
|
|
552
|
+
const idleMilliseconds = (this.idleSeconds ?? 0) * MILLISECONDS_PER_SECOND;
|
|
553
|
+
this.idleTimer = this.clock.setTimeout(() => {
|
|
554
|
+
if (this.clients.length === 0)
|
|
555
|
+
void this.disconnect();
|
|
556
|
+
}, idleMilliseconds);
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Expires reconnect holds (A9). A room kept open for a host migration that ended with nobody left (the elected host
|
|
560
|
+
* never came back) closes.
|
|
561
|
+
*/
|
|
562
|
+
updateSession() {
|
|
563
|
+
this.logic.update(performance.now());
|
|
564
|
+
this.sampleTraffic();
|
|
565
|
+
this.watchHostMigration();
|
|
566
|
+
const wasKeptOpen = !this.autoDispose && !this.hostsDedicatedRunner;
|
|
567
|
+
this.refreshAutoDispose();
|
|
568
|
+
if (wasKeptOpen && this.autoDispose && this.clients.length === 0)
|
|
569
|
+
void this.disconnect();
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Keeps the room open while a host migration waits for the restarted runners (they all leave and rejoin, D31); a
|
|
573
|
+
* dedicated room never closes by itself.
|
|
574
|
+
*/
|
|
575
|
+
refreshAutoDispose() {
|
|
576
|
+
this.autoDispose = !this.hostsDedicatedRunner && !this.logic.isMigratingHost;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Joins a client to the session logic, turning a refusal into a join error the client's join call rejects with.
|
|
580
|
+
* @param sessionId The client's session id.
|
|
581
|
+
* @param request The join request.
|
|
582
|
+
* @returns The welcome (the logic has already sent it to the client).
|
|
583
|
+
* @throws ServerError with JOIN_REFUSED_CODE and the ShutdownReason name.
|
|
584
|
+
*/
|
|
585
|
+
admit(sessionId, request) {
|
|
586
|
+
try {
|
|
587
|
+
return this.logic.join(sessionId, request);
|
|
588
|
+
}
|
|
589
|
+
catch (error) {
|
|
590
|
+
const reasonName = (error instanceof Error) ? error.message : String(error);
|
|
591
|
+
throw new ServerError(JOIN_REFUSED_CODE, reasonName);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Joins a new client to the session logic, counting a refusal in the metrics by its ShutdownReason name.
|
|
596
|
+
* @param sessionId The client's session id.
|
|
597
|
+
* @param request The join request.
|
|
598
|
+
* @returns The welcome.
|
|
599
|
+
* @throws ServerError with JOIN_REFUSED_CODE and the ShutdownReason name.
|
|
600
|
+
*/
|
|
601
|
+
admitCounted(sessionId, request) {
|
|
602
|
+
try {
|
|
603
|
+
return this.admit(sessionId, request);
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
const reasonName = (error instanceof Error) ? error.message : String(error);
|
|
607
|
+
this.serverState.metrics?.countJoin("refused", reasonName);
|
|
608
|
+
throw error;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Forwards a client's session control message (fusion:*) to the session logic; other message types are ignored.
|
|
613
|
+
* @param client The sending client.
|
|
614
|
+
* @param type The message type.
|
|
615
|
+
* @param payload The payload.
|
|
616
|
+
*/
|
|
617
|
+
receiveControl(client, type, payload) {
|
|
618
|
+
if (typeof type !== "string" || !type.startsWith(CONTROL_MESSAGE_PREFIX))
|
|
619
|
+
return;
|
|
620
|
+
this.serverState.metrics?.countMessage?.("in");
|
|
621
|
+
this.logic.receiveControl(client.sessionId, type, payload);
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* The session logic's outputs, bound to this room's clients and listing.
|
|
625
|
+
* @returns The outputs.
|
|
626
|
+
*/
|
|
627
|
+
createOutputs() {
|
|
628
|
+
return {
|
|
629
|
+
sendFrame: (clientKey, sourcePlayerId, frame, channel) => this.sendFrameTo(clientKey, sourcePlayerId, frame, channel),
|
|
630
|
+
sendControl: (clientKey, name, payload) => this.sendControlTo(clientKey, name, payload),
|
|
631
|
+
setSessionMetadata: (session) => this.publishSession(session),
|
|
632
|
+
lockSession: (locked) => this.setLocked(locked),
|
|
633
|
+
nowMilliseconds: () => performance.now()
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Passes a client's frame to the session logic on the channel it arrived on (D7), counting it.
|
|
638
|
+
* @param client The sending client.
|
|
639
|
+
* @param payload The envelope (route byte + uint16 target + frame).
|
|
640
|
+
* @param channel The TOOLKIT.RpcChannel it arrived on.
|
|
641
|
+
*/
|
|
642
|
+
receiveFrame(client, payload, channel) {
|
|
643
|
+
this.countFrame("in", client, channel, payload.byteLength);
|
|
644
|
+
this.serverState.metrics?.countMessage?.("in");
|
|
645
|
+
this.logic.receiveFrame(client.sessionId, payload, channel);
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Sends a frame to one client, prefixed with its source player (D11). An Unreliable frame goes as a WebTransport
|
|
649
|
+
* datagram when the client has one and the prefixed frame fits NETWORK_MAX_UNRELIABLE_FRAME_BYTES; every other frame
|
|
650
|
+
* uses the reliable bytes message (D7).
|
|
651
|
+
* @param clientKey The receiving client's session id.
|
|
652
|
+
* @param sourcePlayerId The sending player.
|
|
653
|
+
* @param frame The frame.
|
|
654
|
+
* @param channel The TOOLKIT.RpcChannel the frame was sent on (default Reliable).
|
|
655
|
+
*/
|
|
656
|
+
sendFrameTo(clientKey, sourcePlayerId, frame, channel = this.toolkit.RpcChannel.Reliable) {
|
|
657
|
+
const client = this.clients.getById(clientKey);
|
|
658
|
+
if (client == null)
|
|
659
|
+
return;
|
|
660
|
+
const prefixed = this.toolkit.NetworkProtocol.prefixSource(sourcePlayerId, frame);
|
|
661
|
+
const sendsAsDatagram = channel === this.toolkit.RpcChannel.Unreliable && client.rawUnreliable != null && prefixed.byteLength <= this.toolkit.NETWORK_MAX_UNRELIABLE_FRAME_BYTES;
|
|
662
|
+
this.countFrame("out", client, sendsAsDatagram ? channel : this.toolkit.RpcChannel.Reliable, prefixed.byteLength);
|
|
663
|
+
this.serverState.metrics?.countMessage?.("out");
|
|
664
|
+
if (sendsAsDatagram && client.rawUnreliable != null) {
|
|
665
|
+
client.rawUnreliable(getMessageBytes.raw(Protocol.ROOM_DATA, this.toolkit.FUSION_UNRELIABLE_FROM_ROOM, prefixed));
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
client.sendBytes(this.toolkit.FUSION_BYTES_FROM_ROOM, prefixed);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Counts one frame in the server metrics, when they are on (D16).
|
|
672
|
+
* @param direction "in" from the client, "out" to it.
|
|
673
|
+
* @param client The client.
|
|
674
|
+
* @param channel The TOOLKIT.RpcChannel it travels on.
|
|
675
|
+
* @param byteLength The frame size in bytes.
|
|
676
|
+
*/
|
|
677
|
+
countFrame(direction, client, channel, byteLength) {
|
|
678
|
+
if (direction === "in") {
|
|
679
|
+
this.traffic.framesIn += 1;
|
|
680
|
+
this.traffic.bytesIn += byteLength;
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
this.traffic.framesOut += 1;
|
|
684
|
+
this.traffic.bytesOut += byteLength;
|
|
685
|
+
}
|
|
686
|
+
const metrics = this.serverState.metrics;
|
|
687
|
+
if (metrics == null)
|
|
688
|
+
return;
|
|
689
|
+
const transportName = transportOf(client);
|
|
690
|
+
const channelName = (channel === this.toolkit.RpcChannel.Unreliable) ? "unreliable" : "reliable";
|
|
691
|
+
metrics.countFrame(direction, transportName, channelName, byteLength);
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Sends a control message to one client.
|
|
695
|
+
* @param clientKey The receiving client's session id.
|
|
696
|
+
* @param name The control message name.
|
|
697
|
+
* @param payload The payload.
|
|
698
|
+
*/
|
|
699
|
+
sendControlTo(clientKey, name, payload) {
|
|
700
|
+
const client = this.clients.getById(clientKey);
|
|
701
|
+
if (client == null)
|
|
702
|
+
return;
|
|
703
|
+
this.serverState.metrics?.countMessage?.("out");
|
|
704
|
+
client.send(name, payload);
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Publishes the session description to the lobby listing: metadata, visibility and capacity in one update. The
|
|
708
|
+
* metadata keeps the sessionName filter value: Colyseus matches filterBy fields against the listing's metadata, which
|
|
709
|
+
* setMatchmaking replaces rather than merges.
|
|
710
|
+
* @param session The description.
|
|
711
|
+
*/
|
|
712
|
+
publishSession(session) {
|
|
713
|
+
this.lastSessionInfo = session;
|
|
714
|
+
const metadata = Object.assign({}, session, { sessionName: session.name, gameMode: this.gameModeName, isDedicated: this.hostsDedicatedRunner });
|
|
715
|
+
this.setMatchmaking({ metadata: metadata, unlisted: !session.isVisible, maxClients: session.maxPlayers }).catch((error) => getLogger().error("session listing update failed", { roomId: this.roomId, sessionName: this.sessionName, error: String(error) }));
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Locks or unlocks the room against new joins (full or closed sessions).
|
|
719
|
+
* @param locked True to lock.
|
|
720
|
+
*/
|
|
721
|
+
setLocked(locked) {
|
|
722
|
+
const change = locked ? this.lock() : this.unlock();
|
|
723
|
+
change.catch((error) => getLogger().error("session lock update failed", { roomId: this.roomId, sessionName: this.sessionName, error: String(error) }));
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Clamps a requested player count to limits.maxPlayersPerSession (D14).
|
|
727
|
+
* @param requestedPlayerCount The count the joiner asked for.
|
|
728
|
+
* @returns The count the session may use.
|
|
729
|
+
*/
|
|
730
|
+
clampPlayerCount(requestedPlayerCount) {
|
|
731
|
+
const maxPlayers = this.serverState.config.limits.maxPlayersPerSession;
|
|
732
|
+
if (requestedPlayerCount <= maxPlayers)
|
|
733
|
+
return requestedPlayerCount;
|
|
734
|
+
getLogger().debug("player count clamped", { sessionName: this.sessionName, roomId: this.roomId, requested: requestedPlayerCount, maxPlayers: maxPlayers });
|
|
735
|
+
return maxPlayers;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Measures the room's byte rate once per ROOM_TRAFFIC_WINDOW_MS (R18).
|
|
739
|
+
*/
|
|
740
|
+
sampleTraffic() {
|
|
741
|
+
const now = performance.now();
|
|
742
|
+
const elapsedMilliseconds = now - this.trafficWindowStartedAt;
|
|
743
|
+
if (elapsedMilliseconds < ROOM_TRAFFIC_WINDOW_MS)
|
|
744
|
+
return;
|
|
745
|
+
const totalBytes = this.traffic.bytesIn + this.traffic.bytesOut;
|
|
746
|
+
this.bytesPerSecond = (totalBytes - this.trafficWindowBytes) / (elapsedMilliseconds / MILLISECONDS_PER_SECOND);
|
|
747
|
+
this.trafficWindowStartedAt = now;
|
|
748
|
+
this.trafficWindowBytes = totalBytes;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Records a host migration when logic.isMigratingHost turns false with a different host than before (R18).
|
|
752
|
+
*/
|
|
753
|
+
watchHostMigration() {
|
|
754
|
+
const isMigrating = this.logic.isMigratingHost;
|
|
755
|
+
const hostPlayerId = this.currentHostPlayerId();
|
|
756
|
+
const migrationEnded = this.wasMigratingHost && !isMigrating;
|
|
757
|
+
if (migrationEnded && hostPlayerId != null && this.lastHostPlayerId != null && hostPlayerId !== this.lastHostPlayerId) {
|
|
758
|
+
this.hostMigrationLog.push({ at: new Date().toISOString(), fromPlayerId: this.lastHostPlayerId, toPlayerId: hostPlayerId });
|
|
759
|
+
}
|
|
760
|
+
this.wasMigratingHost = isMigrating;
|
|
761
|
+
if (!isMigrating && hostPlayerId != null)
|
|
762
|
+
this.lastHostPlayerId = hostPlayerId;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* The Host-mode host's player id.
|
|
766
|
+
* @returns The id, or null when no player is the host.
|
|
767
|
+
*/
|
|
768
|
+
currentHostPlayerId() {
|
|
769
|
+
if (this.logic == null)
|
|
770
|
+
return null;
|
|
771
|
+
const host = this.logic.players.find((player) => player.isHost);
|
|
772
|
+
return host?.playerId ?? null;
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* The client session id of a player.
|
|
776
|
+
* @param playerId The player.
|
|
777
|
+
* @returns The session id, or null when no seat has that player.
|
|
778
|
+
*/
|
|
779
|
+
sessionIdOfPlayer(playerId) {
|
|
780
|
+
for (const [sessionId, seat] of this.seats)
|
|
781
|
+
if (seat.playerId === playerId)
|
|
782
|
+
return sessionId;
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* The players of the session page, from the seats and the logic's player list.
|
|
787
|
+
* @param includeAddresses False hides the addresses.
|
|
788
|
+
* @returns The players, by player id.
|
|
789
|
+
*/
|
|
790
|
+
describePlayers(includeAddresses) {
|
|
791
|
+
const logicPlayers = this.logic.players;
|
|
792
|
+
const masterClientPlayerId = this.logic.masterClientPlayerId;
|
|
793
|
+
const players = [];
|
|
794
|
+
for (const [sessionId, seat] of this.seats) {
|
|
795
|
+
const logicPlayer = logicPlayers.find((player) => player.clientKey === sessionId);
|
|
796
|
+
const identity = seat.request.authIdentity ?? null;
|
|
797
|
+
players.push({
|
|
798
|
+
playerId: seat.playerId,
|
|
799
|
+
userId: identity?.userId ?? null,
|
|
800
|
+
displayName: displayNameOf(identity),
|
|
801
|
+
clientAddress: includeAddresses ? seat.clientAddress : null,
|
|
802
|
+
transport: seat.transport,
|
|
803
|
+
joinedAt: seat.joinedAt,
|
|
804
|
+
connected: logicPlayer?.connected ?? false,
|
|
805
|
+
isHost: logicPlayer?.isHost ?? false,
|
|
806
|
+
isMasterClient: seat.playerId === masterClientPlayerId
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
return players.sort((left, right) => left.playerId - right.playerId);
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Refuses a kicked player's fresh joins to this session (R19); a presence failure is logged and the kick goes on.
|
|
813
|
+
* @param seat The kicked player's seat.
|
|
814
|
+
*/
|
|
815
|
+
async blockRejoin(seat) {
|
|
816
|
+
const userId = seat.request.authIdentity?.userId ?? null;
|
|
817
|
+
try {
|
|
818
|
+
await this.serverState.kickBlocks.block(this.sessionName, { userId: userId, clientAddress: seat.clientAddress }, KICK_REJOIN_BLOCK_SECONDS);
|
|
819
|
+
}
|
|
820
|
+
catch (error) {
|
|
821
|
+
getLogger().warn("the kick block could not be stored", { sessionName: this.sessionName, roomId: this.roomId, error: String(error) });
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Tells the admin console and the leaderboard service a player joined or left (player history and session presence,
|
|
826
|
+
* game-server-leaderboards D10).
|
|
827
|
+
* @param kind "joined" or "left".
|
|
828
|
+
* @param seat The player's seat.
|
|
829
|
+
*/
|
|
830
|
+
reportPlayerSession(kind, seat) {
|
|
831
|
+
const userId = seat.request.authIdentity?.userId ?? null;
|
|
832
|
+
getLogger().info("player " + kind, { roomId: this.roomId, sessionName: this.sessionName, playerId: seat.playerId, userId: userId, clientAddress: seat.clientAddress });
|
|
833
|
+
const event = { kind: kind, roomId: this.roomId, sessionName: this.sessionName, processId: matchMaker.processId, playerId: seat.playerId, userId: userId, clientAddress: seat.clientAddress, at: new Date().toISOString() };
|
|
834
|
+
this.serverState.onPlayerSessionEvent?.(event);
|
|
835
|
+
this.serverState.onLeaderboardSessionPresence?.(event);
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* This session's description for the hooks.
|
|
839
|
+
* @returns The event.
|
|
840
|
+
*/
|
|
841
|
+
sessionEvent() {
|
|
842
|
+
return { roomId: this.roomId, sessionName: this.sessionName, gameMode: this.creatorGameMode, isDedicated: this.hostsDedicatedRunner };
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Calls a session hook, if the server has it.
|
|
846
|
+
* @param hookName The hook.
|
|
847
|
+
*/
|
|
848
|
+
callSessionHook(hookName) {
|
|
849
|
+
const event = this.sessionEvent();
|
|
850
|
+
this.callHook(hookName, (hooks) => hooks[hookName]?.(event));
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Calls a player hook, if the server has it.
|
|
854
|
+
* @param hookName The hook.
|
|
855
|
+
* @param playerId The player.
|
|
856
|
+
* @param identity The player's verified identity, or null.
|
|
857
|
+
*/
|
|
858
|
+
callPlayerHook(hookName, playerId, identity) {
|
|
859
|
+
const event = Object.assign(this.sessionEvent(), { playerId: playerId, identity: identity });
|
|
860
|
+
this.callHook(hookName, (hooks) => hooks[hookName]?.(event));
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Runs a hook call; a hook that throws is logged with its name and the session carries on (§Error policy).
|
|
864
|
+
* @param hookName The hook, for the log.
|
|
865
|
+
* @param invoke Calls the hook on the server's hooks.
|
|
866
|
+
*/
|
|
867
|
+
callHook(hookName, invoke) {
|
|
868
|
+
try {
|
|
869
|
+
invoke(this.serverState.hooks);
|
|
870
|
+
}
|
|
871
|
+
catch (error) {
|
|
872
|
+
getLogger().error("hook " + hookName + " threw", { sessionName: this.sessionName, roomId: this.roomId, error: String(error) });
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* How a client is connected.
|
|
878
|
+
* @param client The client.
|
|
879
|
+
* @returns "webtransport" when it has a datagram channel, else "websocket".
|
|
880
|
+
*/
|
|
881
|
+
function transportOf(client) {
|
|
882
|
+
return (client.rawUnreliable != null) ? "webtransport" : "websocket";
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* A player's display name from its verified claims: name, else preferred_username.
|
|
886
|
+
* @param identity The verified identity, or null.
|
|
887
|
+
* @returns The name, or null.
|
|
888
|
+
*/
|
|
889
|
+
function displayNameOf(identity) {
|
|
890
|
+
const claims = identity?.claims ?? {};
|
|
891
|
+
const name = claims.name ?? claims.preferred_username;
|
|
892
|
+
return (typeof name === "string") ? name : null;
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* The inspector rows of a dedicated runner's network objects.
|
|
896
|
+
* @param objects The runner's objects, or null when the runner cannot list them.
|
|
897
|
+
* @returns The inspector view.
|
|
898
|
+
*/
|
|
899
|
+
function inspectDedicatedObjects(objects) {
|
|
900
|
+
if (objects == null)
|
|
901
|
+
return { available: false, reason: DEDICATED_INSPECTION_MISSING_REASON, objects: [] };
|
|
902
|
+
return {
|
|
903
|
+
available: true,
|
|
904
|
+
reason: null,
|
|
905
|
+
objects: objects.map((networkObject) => ({
|
|
906
|
+
networkId: String(networkObject.id),
|
|
907
|
+
kind: (networkObject.prefabGuid !== "") ? "prefab" : "scene",
|
|
908
|
+
name: networkObject.name,
|
|
909
|
+
stateAuthorityPlayerId: (networkObject.stateAuthority.playerId > 0) ? networkObject.stateAuthority.playerId : null,
|
|
910
|
+
lastChangedTick: null,
|
|
911
|
+
stateWordCount: networkObject.stateBuffer?.wordCount ?? 0
|
|
912
|
+
}))
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* The inspector row of one object a Shared-mode session stores.
|
|
917
|
+
* @param stored The stored object.
|
|
918
|
+
* @returns The row.
|
|
919
|
+
*/
|
|
920
|
+
function describeSharedObject(stored) {
|
|
921
|
+
const isPrefab = stored.spawnHeader.prefabId > 0;
|
|
922
|
+
return {
|
|
923
|
+
networkId: String(stored.spawnHeader.networkId),
|
|
924
|
+
kind: isPrefab ? "prefab" : "scene",
|
|
925
|
+
name: isPrefab ? "prefab " + stored.spawnHeader.prefabId : "scene object " + stored.spawnHeader.networkId,
|
|
926
|
+
stateAuthorityPlayerId: (stored.authorityPlayerId > 0) ? stored.authorityPlayerId : null,
|
|
927
|
+
lastChangedTick: stored.senderTick,
|
|
928
|
+
stateWordCount: Math.ceil(stored.deltaBytes.byteLength / STATE_WORD_BYTES)
|
|
929
|
+
};
|
|
930
|
+
}
|