@babylonjs-toolkit/server 0.0.0-stage → 9.35.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/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,116 @@
|
|
|
1
|
+
// Decides which process of a numbered deploy runs the boot dedicated sessions. A rolling deploy gives no process number
|
|
2
|
+
// a fixed role (Colyseus Cloud brings a new generation up on free numbers and restarts process 0 only after it drained,
|
|
3
|
+
// or not at all), so a store lease picks the process. Without Redis the file store grants every lease, so a deploy of
|
|
4
|
+
// one process always boots its sessions, whatever its number.
|
|
5
|
+
import { LEADER_LEASE_TTL_MS, LEADER_RENEW_INTERVAL_MS } from "./admin/AdminConstants.js";
|
|
6
|
+
import { getLogger } from "./GameServerLogger.js";
|
|
7
|
+
/** The store lease whose holder runs the boot dedicated sessions. */
|
|
8
|
+
export const BOOT_SESSION_LEASE_NAME = "dedicated-boot";
|
|
9
|
+
/**
|
|
10
|
+
* Holds the boot session lease and starts the boot dedicated sessions on the process that gets it. A process that does
|
|
11
|
+
* not hold the lease keeps asking; a draining holder stops renewing, so a process of the next generation takes over
|
|
12
|
+
* within LEADER_LEASE_TTL_MS.
|
|
13
|
+
*/
|
|
14
|
+
export class BootSessionOwner {
|
|
15
|
+
/** What it needs from the running server. */
|
|
16
|
+
dependencies;
|
|
17
|
+
/** The renewal timer, while started. */
|
|
18
|
+
renewTimer = null;
|
|
19
|
+
/** What the last look at the lease found; logged only when it changes. */
|
|
20
|
+
leaseState = "unknown";
|
|
21
|
+
/** True once this process runs every boot session. */
|
|
22
|
+
areSessionsStarted = false;
|
|
23
|
+
/** True while a start runs: a scene can load for longer than a tick, and the lease must keep renewing meanwhile. */
|
|
24
|
+
isStarting = false;
|
|
25
|
+
/**
|
|
26
|
+
* Prepares the owner; nothing runs until start().
|
|
27
|
+
* @param dependencies What it needs from the running server.
|
|
28
|
+
*/
|
|
29
|
+
constructor(dependencies) {
|
|
30
|
+
this.dependencies = dependencies;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Runs the first attempt and keeps trying every LEADER_RENEW_INTERVAL_MS. The timer starts first, so a slow scene
|
|
34
|
+
* load cannot let the lease lapse.
|
|
35
|
+
* @throws Error when this process gets the lease and a boot session cannot start; the owner is stopped first.
|
|
36
|
+
*/
|
|
37
|
+
async start() {
|
|
38
|
+
this.renewTimer = this.dependencies.clock.setInterval(() => void this.tick(), LEADER_RENEW_INTERVAL_MS);
|
|
39
|
+
try {
|
|
40
|
+
await this.attempt();
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
this.stop();
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Stops the timer. The lease runs out by itself after LEADER_LEASE_TTL_MS. */
|
|
48
|
+
stop() {
|
|
49
|
+
if (this.renewTimer != null)
|
|
50
|
+
this.dependencies.clock.clearInterval(this.renewTimer);
|
|
51
|
+
this.renewTimer = null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* One timer tick: renews the lease and starts the sessions when due, unless this process drains. A failure never
|
|
55
|
+
* reaches the running process: it is logged, and the next tick tries again.
|
|
56
|
+
*/
|
|
57
|
+
async tick() {
|
|
58
|
+
if (this.dependencies.isDraining())
|
|
59
|
+
return;
|
|
60
|
+
try {
|
|
61
|
+
await this.attempt();
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
getLogger().error("the boot dedicated sessions could not start; trying again", { error: String(error) });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Renews the lease, then starts the sessions when this process holds it and has not started them.
|
|
69
|
+
* @throws Error when a session cannot start.
|
|
70
|
+
*/
|
|
71
|
+
async attempt() {
|
|
72
|
+
const holdsLease = await this.renewLease();
|
|
73
|
+
if (!holdsLease || this.areSessionsStarted || this.isStarting)
|
|
74
|
+
return;
|
|
75
|
+
this.isStarting = true;
|
|
76
|
+
try {
|
|
77
|
+
this.areSessionsStarted = await this.dependencies.startSessions();
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
this.isStarting = false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Takes or renews the lease; a store failure counts as not holding it.
|
|
85
|
+
* @returns True while this process holds the lease.
|
|
86
|
+
*/
|
|
87
|
+
async renewLease() {
|
|
88
|
+
const holderId = this.dependencies.holderId();
|
|
89
|
+
try {
|
|
90
|
+
const holdsLease = await this.dependencies.tryAcquireLease(holderId, LEADER_LEASE_TTL_MS);
|
|
91
|
+
this.reportLeaseState(holdsLease ? "held" : "waiting", holderId);
|
|
92
|
+
return holdsLease;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
this.reportLeaseState("unavailable", holderId, String(error));
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Logs a change of the lease state, so a process that waits for hours writes one line.
|
|
101
|
+
* @param leaseState What this look found.
|
|
102
|
+
* @param holderId This process's id.
|
|
103
|
+
* @param failure Why the store could not answer, when it could not.
|
|
104
|
+
*/
|
|
105
|
+
reportLeaseState(leaseState, holderId, failure) {
|
|
106
|
+
if (leaseState === this.leaseState)
|
|
107
|
+
return;
|
|
108
|
+
this.leaseState = leaseState;
|
|
109
|
+
if (leaseState === "held")
|
|
110
|
+
getLogger().info("this process holds the boot session lease", { process: holderId });
|
|
111
|
+
else if (leaseState === "waiting")
|
|
112
|
+
getLogger().info("another process holds the boot session lease; this one waits", { process: holderId });
|
|
113
|
+
else
|
|
114
|
+
getLogger().warn("the boot session lease could not be renewed", { process: holderId, error: failure });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The logger handed to Colyseus (`defineServer({ logger })`, game-server-admin-console D21): every Colyseus message goes
|
|
3
|
+
* through the game server's own logger with `source: "colyseus"`, so the console's log buffer captures it and its level
|
|
4
|
+
* and format apply. Colyseus's `trace` goes to `debug`.
|
|
5
|
+
*/
|
|
6
|
+
export declare const colyseusLoggerAdapter: {
|
|
7
|
+
/**
|
|
8
|
+
* Logs Colyseus detail.
|
|
9
|
+
* @param logArguments What Colyseus logged.
|
|
10
|
+
*/
|
|
11
|
+
debug(...logArguments: unknown[]): void;
|
|
12
|
+
/**
|
|
13
|
+
* Logs a Colyseus trace, at debug.
|
|
14
|
+
* @param logArguments What Colyseus logged.
|
|
15
|
+
*/
|
|
16
|
+
trace(...logArguments: unknown[]): void;
|
|
17
|
+
/**
|
|
18
|
+
* Logs a Colyseus lifecycle message.
|
|
19
|
+
* @param logArguments What Colyseus logged.
|
|
20
|
+
*/
|
|
21
|
+
info(...logArguments: unknown[]): void;
|
|
22
|
+
/**
|
|
23
|
+
* Logs a Colyseus warning.
|
|
24
|
+
* @param logArguments What Colyseus logged.
|
|
25
|
+
*/
|
|
26
|
+
warn(...logArguments: unknown[]): void;
|
|
27
|
+
/**
|
|
28
|
+
* Logs a Colyseus error.
|
|
29
|
+
* @param logArguments What Colyseus logged.
|
|
30
|
+
*/
|
|
31
|
+
error(...logArguments: unknown[]): void;
|
|
32
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { getLogger } from "./GameServerLogger.js";
|
|
2
|
+
/** The source field of every entry that comes from Colyseus. */
|
|
3
|
+
const COLYSEUS_SOURCE = "colyseus";
|
|
4
|
+
/**
|
|
5
|
+
* Turns Colyseus's positional log arguments into one message: joined with spaces, an Error as its message, any other
|
|
6
|
+
* object as JSON.
|
|
7
|
+
* @param logArguments The arguments Colyseus passed.
|
|
8
|
+
* @returns The message.
|
|
9
|
+
*/
|
|
10
|
+
function joinLogArguments(logArguments) {
|
|
11
|
+
return logArguments.map((argument) => {
|
|
12
|
+
if (argument instanceof Error)
|
|
13
|
+
return argument.message;
|
|
14
|
+
if (argument !== null && typeof argument === "object")
|
|
15
|
+
return safeJson(argument);
|
|
16
|
+
return String(argument);
|
|
17
|
+
}).join(" ");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* JSON text of an object, or its string form when it cannot be serialized (cycles).
|
|
21
|
+
* @param value The object.
|
|
22
|
+
* @returns The text.
|
|
23
|
+
*/
|
|
24
|
+
function safeJson(value) {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.stringify(value);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return String(value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The logger handed to Colyseus (`defineServer({ logger })`, game-server-admin-console D21): every Colyseus message goes
|
|
34
|
+
* through the game server's own logger with `source: "colyseus"`, so the console's log buffer captures it and its level
|
|
35
|
+
* and format apply. Colyseus's `trace` goes to `debug`.
|
|
36
|
+
*/
|
|
37
|
+
export const colyseusLoggerAdapter = {
|
|
38
|
+
/**
|
|
39
|
+
* Logs Colyseus detail.
|
|
40
|
+
* @param logArguments What Colyseus logged.
|
|
41
|
+
*/
|
|
42
|
+
debug(...logArguments) {
|
|
43
|
+
getLogger().debug(joinLogArguments(logArguments), { source: COLYSEUS_SOURCE });
|
|
44
|
+
},
|
|
45
|
+
/**
|
|
46
|
+
* Logs a Colyseus trace, at debug.
|
|
47
|
+
* @param logArguments What Colyseus logged.
|
|
48
|
+
*/
|
|
49
|
+
trace(...logArguments) {
|
|
50
|
+
getLogger().debug(joinLogArguments(logArguments), { source: COLYSEUS_SOURCE });
|
|
51
|
+
},
|
|
52
|
+
/**
|
|
53
|
+
* Logs a Colyseus lifecycle message.
|
|
54
|
+
* @param logArguments What Colyseus logged.
|
|
55
|
+
*/
|
|
56
|
+
info(...logArguments) {
|
|
57
|
+
getLogger().info(joinLogArguments(logArguments), { source: COLYSEUS_SOURCE });
|
|
58
|
+
},
|
|
59
|
+
/**
|
|
60
|
+
* Logs a Colyseus warning.
|
|
61
|
+
* @param logArguments What Colyseus logged.
|
|
62
|
+
*/
|
|
63
|
+
warn(...logArguments) {
|
|
64
|
+
getLogger().warn(joinLogArguments(logArguments), { source: COLYSEUS_SOURCE });
|
|
65
|
+
},
|
|
66
|
+
/**
|
|
67
|
+
* Logs a Colyseus error.
|
|
68
|
+
* @param logArguments What Colyseus logged.
|
|
69
|
+
*/
|
|
70
|
+
error(...logArguments) {
|
|
71
|
+
getLogger().error(joinLogArguments(logArguments), { source: COLYSEUS_SOURCE });
|
|
72
|
+
}
|
|
73
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { DedicatedSceneConfig } from "./GameServerConfig.js";
|
|
2
|
+
/** A configured dedicated scene with its locations as URLs (D19). */
|
|
3
|
+
export interface ResolvedDedicatedScene extends DedicatedSceneConfig {
|
|
4
|
+
/** sceneRoot as an http(s):// or file:// URL ending in "/". */
|
|
5
|
+
sceneRootUrl: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The dedicated scenes a server may run (D19): the config's `dedicated.scenes`, with each scene root turned into a URL
|
|
9
|
+
* the headless runtime loads from. An http(s):// root is used as it is (with a trailing "/"); any other root is a folder
|
|
10
|
+
* resolved against the working directory as a file:// URL. Project script paths resolve the same way. The registry is
|
|
11
|
+
* also the source of the headless runtime's URL allow-list, so a client can only ever name a scene, never a URL.
|
|
12
|
+
*/
|
|
13
|
+
export declare class DedicatedSceneRegistry {
|
|
14
|
+
/** The scenes by name. */
|
|
15
|
+
private readonly scenes;
|
|
16
|
+
/**
|
|
17
|
+
* Resolves the configured scenes.
|
|
18
|
+
* @param scenes The config's dedicated scenes.
|
|
19
|
+
* @param workingDirectory The folder relative scene roots and script paths resolve against.
|
|
20
|
+
*/
|
|
21
|
+
constructor(scenes: DedicatedSceneConfig[], workingDirectory: string);
|
|
22
|
+
/**
|
|
23
|
+
* The scene of a name.
|
|
24
|
+
* @param sceneName The configured scene name.
|
|
25
|
+
* @returns The scene, or null when none has that name.
|
|
26
|
+
*/
|
|
27
|
+
find(sceneName: string): ResolvedDedicatedScene | null;
|
|
28
|
+
/**
|
|
29
|
+
* Every configured scene, in config order (the console's dedicated page, game-server-admin-console T9).
|
|
30
|
+
* @returns The scenes.
|
|
31
|
+
*/
|
|
32
|
+
listScenes(): ResolvedDedicatedScene[];
|
|
33
|
+
/**
|
|
34
|
+
* The URL prefixes the headless runtime may fetch (D19): every scene root URL and every project script URL.
|
|
35
|
+
* @returns The prefixes (possibly none).
|
|
36
|
+
*/
|
|
37
|
+
allowedUrlPrefixes(): string[];
|
|
38
|
+
/**
|
|
39
|
+
* A scene root as a folder URL ending in "/".
|
|
40
|
+
* @param sceneRoot An http(s):// or file:// URL, or a folder path.
|
|
41
|
+
* @param workingDirectory The folder a relative path resolves against.
|
|
42
|
+
* @returns The URL.
|
|
43
|
+
*/
|
|
44
|
+
private static toFolderUrl;
|
|
45
|
+
/**
|
|
46
|
+
* A location as a URL: a URL as it is, a path as a file:// URL.
|
|
47
|
+
* @param location An http(s):// or file:// URL, or a path.
|
|
48
|
+
* @param workingDirectory The folder a relative path resolves against.
|
|
49
|
+
* @returns The URL.
|
|
50
|
+
*/
|
|
51
|
+
private static toFileUrl;
|
|
52
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
/** URL schemes a scene root or project script may already use; anything else is a folder or file path. */
|
|
4
|
+
const URL_SCHEME_PATTERN = /^(https?|file):\/\//i;
|
|
5
|
+
/**
|
|
6
|
+
* The dedicated scenes a server may run (D19): the config's `dedicated.scenes`, with each scene root turned into a URL
|
|
7
|
+
* the headless runtime loads from. An http(s):// root is used as it is (with a trailing "/"); any other root is a folder
|
|
8
|
+
* resolved against the working directory as a file:// URL. Project script paths resolve the same way. The registry is
|
|
9
|
+
* also the source of the headless runtime's URL allow-list, so a client can only ever name a scene, never a URL.
|
|
10
|
+
*/
|
|
11
|
+
export class DedicatedSceneRegistry {
|
|
12
|
+
/** The scenes by name. */
|
|
13
|
+
scenes = new Map();
|
|
14
|
+
/**
|
|
15
|
+
* Resolves the configured scenes.
|
|
16
|
+
* @param scenes The config's dedicated scenes.
|
|
17
|
+
* @param workingDirectory The folder relative scene roots and script paths resolve against.
|
|
18
|
+
*/
|
|
19
|
+
constructor(scenes, workingDirectory) {
|
|
20
|
+
for (const scene of scenes) {
|
|
21
|
+
const resolved = Object.assign({}, scene, {
|
|
22
|
+
sceneRootUrl: DedicatedSceneRegistry.toFolderUrl(scene.sceneRoot, workingDirectory),
|
|
23
|
+
projectScripts: scene.projectScripts.map((script) => DedicatedSceneRegistry.toFileUrl(script, workingDirectory))
|
|
24
|
+
});
|
|
25
|
+
this.scenes.set(scene.name, resolved);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The scene of a name.
|
|
30
|
+
* @param sceneName The configured scene name.
|
|
31
|
+
* @returns The scene, or null when none has that name.
|
|
32
|
+
*/
|
|
33
|
+
find(sceneName) {
|
|
34
|
+
return this.scenes.get(sceneName) ?? null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Every configured scene, in config order (the console's dedicated page, game-server-admin-console T9).
|
|
38
|
+
* @returns The scenes.
|
|
39
|
+
*/
|
|
40
|
+
listScenes() {
|
|
41
|
+
return Array.from(this.scenes.values());
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The URL prefixes the headless runtime may fetch (D19): every scene root URL and every project script URL.
|
|
45
|
+
* @returns The prefixes (possibly none).
|
|
46
|
+
*/
|
|
47
|
+
allowedUrlPrefixes() {
|
|
48
|
+
const prefixes = [];
|
|
49
|
+
for (const scene of this.scenes.values())
|
|
50
|
+
prefixes.push(scene.sceneRootUrl, ...scene.projectScripts);
|
|
51
|
+
return prefixes;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A scene root as a folder URL ending in "/".
|
|
55
|
+
* @param sceneRoot An http(s):// or file:// URL, or a folder path.
|
|
56
|
+
* @param workingDirectory The folder a relative path resolves against.
|
|
57
|
+
* @returns The URL.
|
|
58
|
+
*/
|
|
59
|
+
static toFolderUrl(sceneRoot, workingDirectory) {
|
|
60
|
+
const url = DedicatedSceneRegistry.toFileUrl(sceneRoot, workingDirectory);
|
|
61
|
+
return url.endsWith("/") ? url : url + "/";
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A location as a URL: a URL as it is, a path as a file:// URL.
|
|
65
|
+
* @param location An http(s):// or file:// URL, or a path.
|
|
66
|
+
* @param workingDirectory The folder a relative path resolves against.
|
|
67
|
+
* @returns The URL.
|
|
68
|
+
*/
|
|
69
|
+
static toFileUrl(location, workingDirectory) {
|
|
70
|
+
if (URL_SCHEME_PATTERN.test(location))
|
|
71
|
+
return location;
|
|
72
|
+
return pathToFileURL(path.resolve(workingDirectory, location)).href;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { type FusionSessionLogicApi } from "./HeadlessRuntime.js";
|
|
2
|
+
import type { DedicatedFrameBucketView, DedicatedStartedBy } from "./admin/AdminApiTypes.js";
|
|
3
|
+
/** What a dedicated session runs. */
|
|
4
|
+
export interface DedicatedSessionOptions {
|
|
5
|
+
/** The session name clients join by. */
|
|
6
|
+
sessionName: string;
|
|
7
|
+
/** The folder URL of the exported scene: http(s):// (for example "http://localhost:8877/scenes/") or file://. */
|
|
8
|
+
sceneRootUrl: string;
|
|
9
|
+
/** The exported scene file, the same one the clients load (for example "networktestbed.gltf"). */
|
|
10
|
+
sceneFile: string;
|
|
11
|
+
/** Most players (default 8). */
|
|
12
|
+
playerCount?: number;
|
|
13
|
+
/** Script bundles to load before the scene (the scene's own project bundle auto-loads without this). */
|
|
14
|
+
projectScripts?: string[];
|
|
15
|
+
/** The configured scene's name (game-server-admin-console T9); code that passes locations only leaves it out. */
|
|
16
|
+
sceneName?: string;
|
|
17
|
+
/** Who started the session, as the console shows it (default "code"). */
|
|
18
|
+
startedBy?: DedicatedStartedBy;
|
|
19
|
+
/**
|
|
20
|
+
* True creates the room on this process. By default the matchmaker picks the process with the fewest rooms, which
|
|
21
|
+
* with Redis can be another one, even one that is draining (it stays eligible until its final shutdown).
|
|
22
|
+
*/
|
|
23
|
+
createOnThisProcess?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** The room creation options of a dedicated session (A17). */
|
|
26
|
+
export interface DedicatedRoomOptions extends DedicatedSessionOptions {
|
|
27
|
+
/** TOOLKIT.GameMode.Server. */
|
|
28
|
+
gameMode: number;
|
|
29
|
+
/** Always true: the room runs a headless Server-mode runner. */
|
|
30
|
+
dedicated: true;
|
|
31
|
+
}
|
|
32
|
+
/** The result of TOOLKIT.NetworkRunner.startGame. */
|
|
33
|
+
export interface DedicatedStartResult {
|
|
34
|
+
/** True when the runner started. */
|
|
35
|
+
ok: boolean;
|
|
36
|
+
/** A readable failure message, or null. */
|
|
37
|
+
errorMessage: string | null;
|
|
38
|
+
}
|
|
39
|
+
/** The members of TOOLKIT.NetworkObject the session inspector reads (game-server-admin-console DR §6.6). */
|
|
40
|
+
export interface DedicatedNetworkObject {
|
|
41
|
+
/** The network id. */
|
|
42
|
+
readonly id: number;
|
|
43
|
+
/** The object's name. */
|
|
44
|
+
readonly name: string;
|
|
45
|
+
/** The prefab's guid; empty for a scene object. */
|
|
46
|
+
readonly prefabGuid: string;
|
|
47
|
+
/** The player with state authority. */
|
|
48
|
+
readonly stateAuthority: {
|
|
49
|
+
readonly playerId: number;
|
|
50
|
+
};
|
|
51
|
+
/** The object's state words, or null before it has any. */
|
|
52
|
+
readonly stateBuffer: {
|
|
53
|
+
readonly wordCount: number;
|
|
54
|
+
} | null;
|
|
55
|
+
}
|
|
56
|
+
/** The members of TOOLKIT.NetworkRunner a dedicated session uses. */
|
|
57
|
+
export interface DedicatedRunner {
|
|
58
|
+
/** False on a dedicated server: it has no local player. */
|
|
59
|
+
provideInput: boolean;
|
|
60
|
+
/** Simulation ticks per second. */
|
|
61
|
+
readonly tickRate: number;
|
|
62
|
+
/** Starts the runner. */
|
|
63
|
+
startGame(args: object): Promise<DedicatedStartResult>;
|
|
64
|
+
/** Stops the runner. */
|
|
65
|
+
shutdown(): Promise<void>;
|
|
66
|
+
/** Every network object the runner simulates (read by the session inspector). */
|
|
67
|
+
getAllNetworkObjects?(): DedicatedNetworkObject[];
|
|
68
|
+
}
|
|
69
|
+
/** The Colyseus room type of sessions (D10). */
|
|
70
|
+
export declare const SESSION_ROOM_NAME: string;
|
|
71
|
+
/**
|
|
72
|
+
* The frame-time history of one dedicated runner (game-server-admin-console D23): DEDICATED_FRAME_BUCKET_SECONDS
|
|
73
|
+
* buckets on the wall-clock grid, the last DEDICATED_FRAME_BUCKET_COUNT of them (one hour). It is kept per session,
|
|
74
|
+
* apart from the Prometheus histogram, so the console can chart each session on its own.
|
|
75
|
+
*/
|
|
76
|
+
export declare class DedicatedFrameStatistics {
|
|
77
|
+
/** The buckets, oldest first; the last one may still be filling. */
|
|
78
|
+
private readonly frameBuckets;
|
|
79
|
+
/** Reads the current time, in milliseconds since the epoch. */
|
|
80
|
+
private readonly now;
|
|
81
|
+
/**
|
|
82
|
+
* Creates an empty history.
|
|
83
|
+
* @param now Reads the current time (default Date.now).
|
|
84
|
+
*/
|
|
85
|
+
constructor(now?: () => number);
|
|
86
|
+
/**
|
|
87
|
+
* Adds one rendered frame.
|
|
88
|
+
* @param seconds The frame's duration, in seconds.
|
|
89
|
+
* @param tickSeconds The simulation tick length the frame must fit in, in seconds.
|
|
90
|
+
*/
|
|
91
|
+
observe(seconds: number, tickSeconds: number): void;
|
|
92
|
+
/**
|
|
93
|
+
* The history for the console, oldest first, including the bucket still filling.
|
|
94
|
+
* @returns The buckets.
|
|
95
|
+
*/
|
|
96
|
+
buckets(): DedicatedFrameBucketView[];
|
|
97
|
+
/**
|
|
98
|
+
* The bucket of the current time, started (and the oldest dropped) when a new grid step began.
|
|
99
|
+
* @returns The bucket.
|
|
100
|
+
*/
|
|
101
|
+
private currentBucket;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* A dedicated Server-mode session (T13): a headless NetworkRunner in Server mode inside a session room, on the same exported
|
|
105
|
+
* scene the clients load. The server spawns every joining player's objects (the scene's scripts do it, as on a host), and
|
|
106
|
+
* clients join it as Client.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* await createGameServer({ config: { port: 2567 } });
|
|
110
|
+
* await DedicatedSession.start({ sessionName: "srv1", sceneRootUrl: "http://localhost:8877/scenes/", sceneFile: "networktestbed.gltf" });
|
|
111
|
+
*/
|
|
112
|
+
export declare class DedicatedSession {
|
|
113
|
+
/** The headless scene and its engine. */
|
|
114
|
+
private readonly headless;
|
|
115
|
+
/** The Server-mode runner. */
|
|
116
|
+
private readonly serverRunner;
|
|
117
|
+
/** The runner's frame-time history (D23). */
|
|
118
|
+
private readonly frameStatistics;
|
|
119
|
+
/**
|
|
120
|
+
* Wraps a running dedicated session. Use DedicatedSession.start (or attach, from the room).
|
|
121
|
+
* @param headless The headless scene.
|
|
122
|
+
* @param serverRunner The running Server-mode runner.
|
|
123
|
+
* @param frameStatistics The frame-time history the render loop feeds.
|
|
124
|
+
*/
|
|
125
|
+
private constructor();
|
|
126
|
+
/** The Server-mode runner (tests read its prefab table and objects). */
|
|
127
|
+
get runner(): DedicatedRunner;
|
|
128
|
+
/** The runner's frame-time and overrun history for the last hour (D23). */
|
|
129
|
+
get statistics(): DedicatedFrameStatistics;
|
|
130
|
+
/**
|
|
131
|
+
* Creates a dedicated session room (A17): the room loads the scene headless and starts its Server-mode runner before
|
|
132
|
+
* it accepts players. The server must be listening (createGameServer). The scene root and project scripts are added to
|
|
133
|
+
* the headless runtime's URL allow-list: this is trusted server code, unlike a client's join.
|
|
134
|
+
* @param options The session name, scene and player count.
|
|
135
|
+
* @returns The room id.
|
|
136
|
+
* @throws Error when the scene cannot be loaded or the runner does not start.
|
|
137
|
+
*/
|
|
138
|
+
static start(options: DedicatedSessionOptions): Promise<string>;
|
|
139
|
+
/**
|
|
140
|
+
* Runs a dedicated session inside a session room (the room's onCreate calls it): loads the extra script bundles and
|
|
141
|
+
* the scene headless (rendering once ready), then starts a Server-mode runner on a RoomServerTransport to the room's
|
|
142
|
+
* logic. The scene's scripts find the runner on the scene (NetworkRunner.instances) and become its callbacks. Each
|
|
143
|
+
* rendered frame's duration feeds the dedicated frame metrics (D16) and the session's own frame statistics (D23).
|
|
144
|
+
* @param logic The room's session logic.
|
|
145
|
+
* @param options The room creation options.
|
|
146
|
+
* @returns The running session.
|
|
147
|
+
* @throws Error when the runner does not start.
|
|
148
|
+
*/
|
|
149
|
+
static attach(logic: FusionSessionLogicApi, options: DedicatedRoomOptions): Promise<DedicatedSession>;
|
|
150
|
+
/** Stops the runner, the render loop and the engine. */
|
|
151
|
+
stop(): Promise<void>;
|
|
152
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { matchMaker } from "colyseus";
|
|
2
|
+
import { getLogger } from "./GameServerLogger.js";
|
|
3
|
+
import { findGameServerState } from "./GameServerState.js";
|
|
4
|
+
import { allowScriptUrlPrefixes, getToolkitRuntime, loadHeadlessScene, loadNodeScriptAsync } from "./HeadlessRuntime.js";
|
|
5
|
+
import { RoomServerTransport } from "./RoomServerTransport.js";
|
|
6
|
+
import { DEDICATED_FRAME_BUCKET_COUNT, DEDICATED_FRAME_BUCKET_SECONDS } from "./admin/AdminConstants.js";
|
|
7
|
+
/** The Colyseus room type of sessions (D10). */
|
|
8
|
+
export const SESSION_ROOM_NAME = "fusion_session";
|
|
9
|
+
/** Players a dedicated session accepts when the options do not say. */
|
|
10
|
+
const DEFAULT_PLAYER_COUNT = 8;
|
|
11
|
+
/** Milliseconds per second. */
|
|
12
|
+
const MILLISECONDS_PER_SECOND = 1000;
|
|
13
|
+
/** Length of one frame statistics bucket, in milliseconds. */
|
|
14
|
+
const FRAME_BUCKET_MILLISECONDS = DEDICATED_FRAME_BUCKET_SECONDS * MILLISECONDS_PER_SECOND;
|
|
15
|
+
/**
|
|
16
|
+
* The frame-time history of one dedicated runner (game-server-admin-console D23): DEDICATED_FRAME_BUCKET_SECONDS
|
|
17
|
+
* buckets on the wall-clock grid, the last DEDICATED_FRAME_BUCKET_COUNT of them (one hour). It is kept per session,
|
|
18
|
+
* apart from the Prometheus histogram, so the console can chart each session on its own.
|
|
19
|
+
*/
|
|
20
|
+
export class DedicatedFrameStatistics {
|
|
21
|
+
/** The buckets, oldest first; the last one may still be filling. */
|
|
22
|
+
frameBuckets = [];
|
|
23
|
+
/** Reads the current time, in milliseconds since the epoch. */
|
|
24
|
+
now;
|
|
25
|
+
/**
|
|
26
|
+
* Creates an empty history.
|
|
27
|
+
* @param now Reads the current time (default Date.now).
|
|
28
|
+
*/
|
|
29
|
+
constructor(now = Date.now) {
|
|
30
|
+
this.now = now;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Adds one rendered frame.
|
|
34
|
+
* @param seconds The frame's duration, in seconds.
|
|
35
|
+
* @param tickSeconds The simulation tick length the frame must fit in, in seconds.
|
|
36
|
+
*/
|
|
37
|
+
observe(seconds, tickSeconds) {
|
|
38
|
+
const bucket = this.currentBucket();
|
|
39
|
+
const milliseconds = seconds * MILLISECONDS_PER_SECOND;
|
|
40
|
+
bucket.frameCount += 1;
|
|
41
|
+
bucket.totalMilliseconds += milliseconds;
|
|
42
|
+
bucket.maximumMilliseconds = Math.max(bucket.maximumMilliseconds, milliseconds);
|
|
43
|
+
if (seconds > tickSeconds)
|
|
44
|
+
bucket.tickOverruns += 1;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The history for the console, oldest first, including the bucket still filling.
|
|
48
|
+
* @returns The buckets.
|
|
49
|
+
*/
|
|
50
|
+
buckets() {
|
|
51
|
+
return this.frameBuckets.map((bucket) => ({
|
|
52
|
+
at: new Date(bucket.startedAt).toISOString(),
|
|
53
|
+
averageFrameMilliseconds: (bucket.frameCount > 0) ? bucket.totalMilliseconds / bucket.frameCount : 0,
|
|
54
|
+
maximumFrameMilliseconds: bucket.maximumMilliseconds,
|
|
55
|
+
tickOverruns: bucket.tickOverruns
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The bucket of the current time, started (and the oldest dropped) when a new grid step began.
|
|
60
|
+
* @returns The bucket.
|
|
61
|
+
*/
|
|
62
|
+
currentBucket() {
|
|
63
|
+
const startedAt = Math.floor(this.now() / FRAME_BUCKET_MILLISECONDS) * FRAME_BUCKET_MILLISECONDS;
|
|
64
|
+
const newest = this.frameBuckets[this.frameBuckets.length - 1];
|
|
65
|
+
if (newest != null && newest.startedAt === startedAt)
|
|
66
|
+
return newest;
|
|
67
|
+
const bucket = { startedAt: startedAt, frameCount: 0, totalMilliseconds: 0, maximumMilliseconds: 0, tickOverruns: 0 };
|
|
68
|
+
this.frameBuckets.push(bucket);
|
|
69
|
+
if (this.frameBuckets.length > DEDICATED_FRAME_BUCKET_COUNT)
|
|
70
|
+
this.frameBuckets.shift();
|
|
71
|
+
return bucket;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A dedicated Server-mode session (T13): a headless NetworkRunner in Server mode inside a session room, on the same exported
|
|
76
|
+
* scene the clients load. The server spawns every joining player's objects (the scene's scripts do it, as on a host), and
|
|
77
|
+
* clients join it as Client.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* await createGameServer({ config: { port: 2567 } });
|
|
81
|
+
* await DedicatedSession.start({ sessionName: "srv1", sceneRootUrl: "http://localhost:8877/scenes/", sceneFile: "networktestbed.gltf" });
|
|
82
|
+
*/
|
|
83
|
+
export class DedicatedSession {
|
|
84
|
+
/** The headless scene and its engine. */
|
|
85
|
+
headless;
|
|
86
|
+
/** The Server-mode runner. */
|
|
87
|
+
serverRunner;
|
|
88
|
+
/** The runner's frame-time history (D23). */
|
|
89
|
+
frameStatistics;
|
|
90
|
+
/**
|
|
91
|
+
* Wraps a running dedicated session. Use DedicatedSession.start (or attach, from the room).
|
|
92
|
+
* @param headless The headless scene.
|
|
93
|
+
* @param serverRunner The running Server-mode runner.
|
|
94
|
+
* @param frameStatistics The frame-time history the render loop feeds.
|
|
95
|
+
*/
|
|
96
|
+
constructor(headless, serverRunner, frameStatistics) {
|
|
97
|
+
this.headless = headless;
|
|
98
|
+
this.serverRunner = serverRunner;
|
|
99
|
+
this.frameStatistics = frameStatistics;
|
|
100
|
+
}
|
|
101
|
+
/** The Server-mode runner (tests read its prefab table and objects). */
|
|
102
|
+
get runner() {
|
|
103
|
+
return this.serverRunner;
|
|
104
|
+
}
|
|
105
|
+
/** The runner's frame-time and overrun history for the last hour (D23). */
|
|
106
|
+
get statistics() {
|
|
107
|
+
return this.frameStatistics;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Creates a dedicated session room (A17): the room loads the scene headless and starts its Server-mode runner before
|
|
111
|
+
* it accepts players. The server must be listening (createGameServer). The scene root and project scripts are added to
|
|
112
|
+
* the headless runtime's URL allow-list: this is trusted server code, unlike a client's join.
|
|
113
|
+
* @param options The session name, scene and player count.
|
|
114
|
+
* @returns The room id.
|
|
115
|
+
* @throws Error when the scene cannot be loaded or the runner does not start.
|
|
116
|
+
*/
|
|
117
|
+
static async start(options) {
|
|
118
|
+
// Server code chose these locations, so they join the headless runtime's allow-list (D19).
|
|
119
|
+
allowScriptUrlPrefixes([options.sceneRootUrl, ...(options.projectScripts ?? [])]);
|
|
120
|
+
const toolkit = getToolkitRuntime().TOOLKIT;
|
|
121
|
+
const { createOnThisProcess, ...sessionOptions } = options;
|
|
122
|
+
const roomOptions = Object.assign({}, sessionOptions, { gameMode: toolkit.GameMode.Server, dedicated: true, playerCount: options.playerCount ?? DEFAULT_PLAYER_COUNT });
|
|
123
|
+
const room = (createOnThisProcess === true) ? await matchMaker.handleCreateRoom(SESSION_ROOM_NAME, roomOptions) : await matchMaker.createRoom(SESSION_ROOM_NAME, roomOptions);
|
|
124
|
+
return room.roomId;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Runs a dedicated session inside a session room (the room's onCreate calls it): loads the extra script bundles and
|
|
128
|
+
* the scene headless (rendering once ready), then starts a Server-mode runner on a RoomServerTransport to the room's
|
|
129
|
+
* logic. The scene's scripts find the runner on the scene (NetworkRunner.instances) and become its callbacks. Each
|
|
130
|
+
* rendered frame's duration feeds the dedicated frame metrics (D16) and the session's own frame statistics (D23).
|
|
131
|
+
* @param logic The room's session logic.
|
|
132
|
+
* @param options The room creation options.
|
|
133
|
+
* @returns The running session.
|
|
134
|
+
* @throws Error when the runner does not start.
|
|
135
|
+
*/
|
|
136
|
+
static async attach(logic, options) {
|
|
137
|
+
const runtime = getToolkitRuntime();
|
|
138
|
+
for (const scriptUrl of options.projectScripts ?? [])
|
|
139
|
+
await loadNodeScriptAsync(scriptUrl);
|
|
140
|
+
let runner = null;
|
|
141
|
+
const frameStatistics = new DedicatedFrameStatistics();
|
|
142
|
+
const observeFrame = (seconds) => {
|
|
143
|
+
if (runner == null)
|
|
144
|
+
return;
|
|
145
|
+
const tickSeconds = 1 / runner.tickRate;
|
|
146
|
+
findGameServerState()?.metrics?.observeDedicatedFrame(seconds, tickSeconds);
|
|
147
|
+
frameStatistics.observe(seconds, tickSeconds);
|
|
148
|
+
};
|
|
149
|
+
const headless = await loadHeadlessScene(options.sceneRootUrl, options.sceneFile, observeFrame);
|
|
150
|
+
const toolkit = runtime.TOOLKIT;
|
|
151
|
+
runner = new toolkit.NetworkRunner(headless.scene);
|
|
152
|
+
runner.provideInput = false;
|
|
153
|
+
const result = await runner.startGame({
|
|
154
|
+
gameMode: toolkit.GameMode.Server,
|
|
155
|
+
sessionName: options.sessionName,
|
|
156
|
+
playerCount: options.playerCount ?? DEFAULT_PLAYER_COUNT,
|
|
157
|
+
transport: new RoomServerTransport(logic)
|
|
158
|
+
});
|
|
159
|
+
if (!result.ok) {
|
|
160
|
+
headless.engine.dispose();
|
|
161
|
+
throw new Error("[GameServer] the dedicated runner did not start: " + result.errorMessage);
|
|
162
|
+
}
|
|
163
|
+
getLogger().info("dedicated session \"" + options.sessionName + "\" running " + options.sceneRootUrl + options.sceneFile, { sessionName: options.sessionName });
|
|
164
|
+
return new DedicatedSession(headless, runner, frameStatistics);
|
|
165
|
+
}
|
|
166
|
+
/** Stops the runner, the render loop and the engine. */
|
|
167
|
+
async stop() {
|
|
168
|
+
await this.serverRunner.shutdown();
|
|
169
|
+
this.headless.engine.stopRenderLoop();
|
|
170
|
+
this.headless.scene.dispose();
|
|
171
|
+
this.headless.engine.dispose();
|
|
172
|
+
}
|
|
173
|
+
}
|