diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1deaa2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.vscode +node_modules/ +**/node_modules/ +.pnpm-store/ +.pnp.* +.yarn/ +yarn.lock +*.tsbuildinfo +*.db-shm +*.db-wal +.tmp \ No newline at end of file diff --git a/UndauntedDeployServer/.env.example b/UndauntedDeployServer/.env.example index f3fd483..4b69dff 100644 --- a/UndauntedDeployServer/.env.example +++ b/UndauntedDeployServer/.env.example @@ -4,4 +4,50 @@ PORT_RANGE_BEGIN= PORT_RANGE_END= GAMESERVER_BINARY_PATH= METAGAME_API_KEY= -SECONDS_TO_WAIT_BETWEEN_GAMESERVER_STARTUP= \ No newline at end of file +SECONDS_TO_WAIT_BETWEEN_GAMESERVER_STARTUP=0 +GAMESERVER_STARTUP_POLL_INTERVAL_MS=100 +GAMESERVER_STARTUP_TIMEOUT_SECONDS=60 +GAMESERVER_PORT_RELEASE_TIMEOUT_MS=5000 +# Show a console window for each gameserver process. +GAMESERVER_CONSOLE_LOG=false +# off, report (scan only), safe (force textures/materials collectible), or aggressive +# (also force sounds and inactive-map objects collectible). Cleanup runs once before readiness, +# only after initial load clears and while both tracked and raw connection counts are zero. +GAMESERVER_ASSET_STRIPPING_MODE=aggressive +# Include objects whose outer package belongs to a loaded but non-current map in aggressive mode. +GAMESERVER_STRIP_INACTIVE_MAP_PACKAGES=true +# Write a timestamped asset-strip-*.jsonl candidate manifest beside the game executable. +# Summary cleanup metrics are logged regardless; leave this false to avoid large manifests. +GAMESERVER_ASSET_STRIPPING_LOG_DETAILS=false +# Maximum pre-readiness wait for initial load to clear and both connection counts to remain zero. +# A timeout skips cleanup and publishes readiness; it never retries GC during gameplay. +GAMESERVER_ASSET_GC_WAIT_SECONDS=15 +# Explicit local-development opt-in for native gameserver profiling files. +# Missing or false means no profiling file or writer thread. +GAMESERVER_PROFILING=false +# Native profiling interval in seconds. Invalid values fall back to 30 seconds. +GAMESERVER_PROFILE_INTERVAL_SECONDS=30 +# Required output directory when native profiling is enabled. An empty value keeps the writer disabled. +GAMESERVER_DIAGNOSTICS_DIRECTORY= +# Hard upper bound for each native JSONL profile file (1 MiB to 1 GiB). +GAMESERVER_PROFILE_MAX_BYTES=67108864 +# Maximum age of the reusable actor consideration list; world/level actor changes still rebuild immediately. +GAMESERVER_CONSIDER_CACHE_MAX_AGE_MS=250 +# Explicit opt-in for DeployServer process and event-loop sampling. +DEPLOYSERVER_PROFILING=false +# DeployServer process CPU/memory sampling interval in seconds. +DEPLOYSERVER_PROFILE_INTERVAL_SECONDS=60 +# Explicit opt-in for a bounded DeployServer diagnostics file. Console logging is always available. +DEPLOYSERVER_FILE_DIAGNOSTICS=false +# Required output directory when file diagnostics are enabled. +DEPLOYSERVER_DIAGNOSTICS_DIRECTORY= +# Hard upper bound for deployserver.log (1 MiB to 1 GiB). +DEPLOYSERVER_DIAGNOSTICS_MAX_BYTES=67108864 +ADOPT_GAMESERVER=true +# Shut down idle empty ramsgate after this many seconds. +# When both tracked and raw NetDriver connection counts at zero. Set 0 to keep it running. +RAMSGATE_IDLE_SHUTDOWN_SECONDS=10 +PREWARM_TRAINING_DOJO=false +TRAINING_DOJO_IDLE_SHUTDOWN_SECONDS=300 +HUNT_IDLE_SHUTDOWN_SECONDS=180 +ESCALATION_IDLE_SHUTDOWN_SECONDS=30 diff --git a/UndauntedDeployServer/.gitignore b/UndauntedDeployServer/.gitignore index 49c5f7b..299748f 100644 --- a/UndauntedDeployServer/.gitignore +++ b/UndauntedDeployServer/.gitignore @@ -9,4 +9,5 @@ tmp/ temp/ .cache/ *.db -file \ No newline at end of file +*.log +file diff --git a/UndauntedDeployServer/build.cjs b/UndauntedDeployServer/build.cjs new file mode 100644 index 0000000..d223f2f --- /dev/null +++ b/UndauntedDeployServer/build.cjs @@ -0,0 +1,18 @@ +const path = require("path"); +const { build } = require("esbuild"); + +build({ + absWorkingDir: __dirname, + entryPoints: ["./src/server.ts"], + bundle: true, + platform: "node", + target: "es2022", + format: "cjs", + packages: "external", + minify: true, + legalComments: "none", + outfile: "./dist/server.js", +}).catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/UndauntedDeployServer/package-lock.json b/UndauntedDeployServer/package-lock.json deleted file mode 100644 index da53983..0000000 --- a/UndauntedDeployServer/package-lock.json +++ /dev/null @@ -1,3420 +0,0 @@ -{ - "name": "undauntedmetagame", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "undauntedmetagame", - "version": "0.0.1", - "license": "AGPL-3.0-only", - "dependencies": { - "better-sqlite3": "^12.11.1", - "dotenv": "^17.4.2", - "drizzle-orm": "0.45.2", - "express": "5.2.1", - "jsonwebtoken": "^9.0.3", - "pino": "^10.3.1", - "pino-pretty": "^13.1.3" - }, - "devDependencies": { - "@types/express": "5.0.6", - "@types/jsonwebtoken": "^9.0.10", - "@types/node": "26.0.0", - "drizzle-kit": "0.31.10", - "tsx": "4.22.4", - "typescript": "6.0.3" - } - }, - "node_modules/@drizzle-team/brocli": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", - "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@esbuild-kit/core-utils": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", - "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", - "deprecated": "Merged into tsx: https://tsx.is", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.18.20", - "source-map-support": "^0.5.21" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "node_modules/@esbuild-kit/esm-loader": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", - "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", - "deprecated": "Merged into tsx: https://tsx.is", - "dev": true, - "license": "MIT", - "dependencies": { - "@esbuild-kit/core-utils": "^3.3.2", - "get-tsconfig": "^4.7.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@pinojs/redact": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", - "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", - "license": "MIT" - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", - "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "^2" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", - "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/jsonwebtoken": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", - "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/ms": "*", - "@types/node": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", - "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~8.3.0" - } - }, - "node_modules/@types/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/better-sqlite3": { - "version": "12.11.1", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", - "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", - "hasInstallScript": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - }, - "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^2.0.0", - "debug": "^4.4.3", - "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", - "on-finished": "^2.4.1", - "qs": "^6.15.2", - "raw-body": "^3.0.2", - "type-is": "^2.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "license": "MIT" - }, - "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/dateformat": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", - "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/dotenv": { - "version": "17.4.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", - "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/drizzle-kit": { - "version": "0.31.10", - "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz", - "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@drizzle-team/brocli": "^0.10.2", - "@esbuild-kit/esm-loader": "^2.5.5", - "esbuild": "^0.25.4", - "tsx": "^4.21.0" - }, - "bin": { - "drizzle-kit": "bin.cjs" - } - }, - "node_modules/drizzle-orm": { - "version": "0.45.2", - "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", - "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", - "license": "Apache-2.0", - "peerDependencies": { - "@aws-sdk/client-rds-data": ">=3", - "@cloudflare/workers-types": ">=4", - "@electric-sql/pglite": ">=0.2.0", - "@libsql/client": ">=0.10.0", - "@libsql/client-wasm": ">=0.10.0", - "@neondatabase/serverless": ">=0.10.0", - "@op-engineering/op-sqlite": ">=2", - "@opentelemetry/api": "^1.4.1", - "@planetscale/database": ">=1.13", - "@prisma/client": "*", - "@tidbcloud/serverless": "*", - "@types/better-sqlite3": "*", - "@types/pg": "*", - "@types/sql.js": "*", - "@upstash/redis": ">=1.34.7", - "@vercel/postgres": ">=0.8.0", - "@xata.io/client": "*", - "better-sqlite3": ">=7", - "bun-types": "*", - "expo-sqlite": ">=14.0.0", - "gel": ">=2", - "knex": "*", - "kysely": "*", - "mysql2": ">=2", - "pg": ">=8", - "postgres": ">=3", - "sql.js": ">=1", - "sqlite3": ">=5" - }, - "peerDependenciesMeta": { - "@aws-sdk/client-rds-data": { - "optional": true - }, - "@cloudflare/workers-types": { - "optional": true - }, - "@electric-sql/pglite": { - "optional": true - }, - "@libsql/client": { - "optional": true - }, - "@libsql/client-wasm": { - "optional": true - }, - "@neondatabase/serverless": { - "optional": true - }, - "@op-engineering/op-sqlite": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@prisma/client": { - "optional": true - }, - "@tidbcloud/serverless": { - "optional": true - }, - "@types/better-sqlite3": { - "optional": true - }, - "@types/pg": { - "optional": true - }, - "@types/sql.js": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@vercel/postgres": { - "optional": true - }, - "@xata.io/client": { - "optional": true - }, - "better-sqlite3": { - "optional": true - }, - "bun-types": { - "optional": true - }, - "expo-sqlite": { - "optional": true - }, - "gel": { - "optional": true - }, - "knex": { - "optional": true - }, - "kysely": { - "optional": true - }, - "mysql2": { - "optional": true - }, - "pg": { - "optional": true - }, - "postgres": { - "optional": true - }, - "prisma": { - "optional": true - }, - "sql.js": { - "optional": true - }, - "sqlite3": { - "optional": true - } - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/fast-copy": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.3.tgz", - "integrity": "sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw==", - "license": "MIT" - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "license": "MIT" - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-tsconfig": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", - "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/help-me": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", - "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/jsonwebtoken": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", - "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", - "license": "MIT", - "dependencies": { - "jws": "^4.0.1", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", - "license": "MIT" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "license": "MIT" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", - "license": "MIT" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "license": "MIT" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "license": "MIT" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "license": "MIT" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-abi": { - "version": "3.92.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", - "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pino": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", - "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", - "license": "MIT", - "dependencies": { - "@pinojs/redact": "^0.4.0", - "atomic-sleep": "^1.0.0", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^3.0.0", - "pino-std-serializers": "^7.0.0", - "process-warning": "^5.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^4.0.1", - "thread-stream": "^4.0.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", - "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", - "license": "MIT", - "dependencies": { - "split2": "^4.0.0" - } - }, - "node_modules/pino-pretty": { - "version": "13.1.3", - "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", - "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", - "license": "MIT", - "dependencies": { - "colorette": "^2.0.7", - "dateformat": "^4.6.3", - "fast-copy": "^4.0.0", - "fast-safe-stringify": "^2.1.1", - "help-me": "^5.0.0", - "joycon": "^3.1.1", - "minimist": "^1.2.6", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^3.0.0", - "pump": "^3.0.0", - "secure-json-parse": "^4.0.0", - "sonic-boom": "^4.0.1", - "strip-json-comments": "^5.0.2" - }, - "bin": { - "pino-pretty": "bin.js" - } - }, - "node_modules/pino-std-serializers": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", - "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", - "license": "MIT" - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/process-warning": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", - "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "license": "MIT" - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/secure-json-parse": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", - "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/sonic-boom": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", - "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-json-comments": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", - "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/thread-stream": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", - "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", - "license": "MIT", - "dependencies": { - "real-require": "^1.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/thread-stream/node_modules/real-require": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", - "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", - "license": "MIT" - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tsx": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", - "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.28.0" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.1", - "@esbuild/android-arm": "0.28.1", - "@esbuild/android-arm64": "0.28.1", - "@esbuild/android-x64": "0.28.1", - "@esbuild/darwin-arm64": "0.28.1", - "@esbuild/darwin-x64": "0.28.1", - "@esbuild/freebsd-arm64": "0.28.1", - "@esbuild/freebsd-x64": "0.28.1", - "@esbuild/linux-arm": "0.28.1", - "@esbuild/linux-arm64": "0.28.1", - "@esbuild/linux-ia32": "0.28.1", - "@esbuild/linux-loong64": "0.28.1", - "@esbuild/linux-mips64el": "0.28.1", - "@esbuild/linux-ppc64": "0.28.1", - "@esbuild/linux-riscv64": "0.28.1", - "@esbuild/linux-s390x": "0.28.1", - "@esbuild/linux-x64": "0.28.1", - "@esbuild/netbsd-arm64": "0.28.1", - "@esbuild/netbsd-x64": "0.28.1", - "@esbuild/openbsd-arm64": "0.28.1", - "@esbuild/openbsd-x64": "0.28.1", - "@esbuild/openharmony-arm64": "0.28.1", - "@esbuild/sunos-x64": "0.28.1", - "@esbuild/win32-arm64": "0.28.1", - "@esbuild/win32-ia32": "0.28.1", - "@esbuild/win32-x64": "0.28.1" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", - "license": "MIT", - "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - } - } -} diff --git a/UndauntedDeployServer/package.json b/UndauntedDeployServer/package.json index a27d834..f6f942e 100644 --- a/UndauntedDeployServer/package.json +++ b/UndauntedDeployServer/package.json @@ -6,8 +6,9 @@ "author": "gwog :3", "scripts": { "dev": "tsx --env-file=.env src/server.ts", - "build": "tsc", - "start": "node --env-file=.env dist/server.js" + "build": "node build.cjs", + "start": "node --env-file=.env dist/server.js", + "test:diagnostics": "tsx src/selftests/diagnostics.ts" }, "dependencies": { "better-sqlite3": "^12.11.1", @@ -23,6 +24,7 @@ "@types/jsonwebtoken": "^9.0.10", "@types/node": "26.0.0", "drizzle-kit": "0.31.10", + "esbuild": "0.28.1", "tsx": "4.22.4", "typescript": "6.0.3" } diff --git a/UndauntedDeployServer/src/app.ts b/UndauntedDeployServer/src/app.ts index 58667a3..7347930 100644 --- a/UndauntedDeployServer/src/app.ts +++ b/UndauntedDeployServer/src/app.ts @@ -1,4 +1,5 @@ import express from "express"; +import { gameserverRouter } from "./routes/gameservers.js"; import { matchmakingRouter } from "./routes/matchmaker.js"; import { logger } from "./logger.js"; @@ -8,4 +9,5 @@ app.use(express.json()); app.use(express.urlencoded({ extended: true })); -app.use("/api/matchmaker", matchmakingRouter); \ No newline at end of file +app.use("/api/gameservers", gameserverRouter); +app.use("/api/matchmaker", matchmakingRouter); diff --git a/UndauntedDeployServer/src/controllers/gameservers.ts b/UndauntedDeployServer/src/controllers/gameservers.ts index f1e28d3..2eb73b7 100644 --- a/UndauntedDeployServer/src/controllers/gameservers.ts +++ b/UndauntedDeployServer/src/controllers/gameservers.ts @@ -1,4 +1,7 @@ -import { spawn } from "node:child_process" +import { execFileSync, spawn, type ChildProcess } from "node:child_process" +import dgram from "node:dgram"; +import { createReadStream } from "node:fs"; +import { dirname, join } from "node:path"; import { setTimeout } from "node:timers/promises"; import crypto from "node:crypto"; @@ -7,12 +10,20 @@ import PlayerHuntTable from "../vendor/player_hunts_table.json"; import MatchmakerHuntTable from "../vendor/matchmaker_hunts_table.json"; import TrialsHardHuntTable from "../vendor/trials_hard_table.json"; import TrialsEliteHuntTable from "../vendor/trials_elite_table.json"; -import { kill } from "node:process"; import { logger } from "../logger"; +import { kill } from "node:process"; const RAMSGATE_MAP_PATH = "/Game/Maps/ramsgate/ramsgate_01_persistent"; const TRAINING_DOJO_MAP_PATH = "/Game/Maps/islands/dojo/training_dojo_persistent"; const TRIALS_MAP_PATH = "/Game/Maps/islands/arenas/arena_ramsgate_00"; +const GAMESERVER_PORT_RELEASE_TIMEOUT_MS = Number(process.env.GAMESERVER_PORT_RELEASE_TIMEOUT_MS || "5000"); + +class NoFreeHuntPortsError extends Error { + constructor(){ + super("No free ports left!"); + this.name = "NoFreeHuntPortsError"; + } +} export type Gameserver = { id: string, @@ -23,8 +34,23 @@ export type Gameserver = { expectedPlayers: ExpectedPlayer[] | undefined, isRamsgate: boolean, isTrainingDojo: boolean, + origin: GameserverOrigin, + trigger: string, processId: number, - startTime: Date + startTime: Date, + lastTouchedTime: Date, + callbackToken: string | undefined, + lastLifecycleHeartbeatTime: Date | undefined, + reportedLiveConnections: number | undefined, + reportedRawConnections: number | undefined, + hadLiveConnection: boolean, + lifecycleRetirement: LifecycleRetirementState, + shutdownAfterSeconds: number | undefined, + expectedShutdownReason: string | undefined, + optimization: GameserverOptimizationMetrics | undefined, + dllSha256: string | undefined, + profiling: GameserverProfilingStatus | undefined, + pacing: GameserverPacingStatus | undefined }; type ExpectedPlayer = { @@ -32,140 +58,1588 @@ type ExpectedPlayer = { playerHuntId: string }; +type GameserverOptimizationMetrics = { + mode: "off" | "report" | "safe" | "aggressive", + safetyGate: "not_required" | "passed" | "initial_load" | "connections_present" | "timeout" | "signature_mismatch" | "failed", + signatureValid: boolean, + candidates: number, + collected: number, + retained: number, + textures: number, + materials: number, + sounds: number, + mapPackageCandidates: number, + activeMapPackages: number, + inactiveMapPackages: number, + durationMs: number, + workingSetBefore: number, + workingSetAfter: number, + privateBytesBefore: number, + privateBytesAfter: number, + configuredMaxFps: number, + observedMaxFps: number, + capSignatureValid: boolean, + capResolved: boolean, + capApplied: boolean, + capVerified: boolean, + preReadyNetworkingPasses: number, + netServerMaxTickRate: number, + maxNetTickRate: number, + bootstrapMinimumMilliseconds: number, + bootstrapMaximumMilliseconds: number, + considerCacheMaxAgeMilliseconds: number, + profilingEnabled: boolean, + failed: boolean, + error?: string +}; + +type GameserverReadyPayload = { + id: string, + port: number, + pid: number, + ready?: boolean, + error?: string, + optimization?: GameserverOptimizationMetrics, + profiling?: GameserverProfilingStatus, + pacing?: GameserverPacingStatus +}; + +type LifecycleRetirementState = { + protocolVersion: number | undefined, + heartbeatSequence: number | undefined, + observationSequence: number | undefined, + worldEpoch: number | undefined, + zeroHeartbeatStreak: number, + zeroSinceTime: Date | undefined, + armedHeartbeatSequence: number | undefined +}; + +type GameserverHeartbeatPayload = { + id: string, + port: number, + pid: number, + liveConnections: number, + rawConnections: number, + protocolVersion?: number, + heartbeatSequence?: number, + observationSequence?: number, + worldEpoch?: number +}; + +type GameserverPacingStatus = { + state: "disabled" | "observing" | "active" | "fallback" | "unknown", + installFailure: string, + fallbackReason: string, + executableBuildValid: boolean, + signaturesValid: boolean, + hookEnabled: boolean, + highResolutionApiAvailable: boolean, + timerCreated: boolean, + correctionEverActivated: boolean, + observedFrames: number, + cvarMaxFps: number, + cachedMaxFps: number, + virtualMaxFps: number, + rollingMedianCadenceHz: number, + rollingMedianCoarseOvershootMs: number +}; + +type GameserverProfilingStatus = { + enabled: boolean, + started: boolean, + startupWriteSucceeded: boolean, + writerStarted: boolean, + path: string, + startupBytes: number, + startupError: number, + writerError: number, + intervalBytes: number, + intervalWriteFailures: number, + lastIntervalError: number, + dllSha256: string +}; + +type PendingGameserverReady = { + port: number, + token: string, + expectedDllSha256: string, + resolve: (Payload: GameserverReadyPayload) => void, + reject: (Reason: Error) => void +}; + +type StartedGameserverProcess = { + processId: number, + child: ChildProcess | undefined +}; + +export type GameserverOrigin = "RAMSGATE_LAZY" | "TRAINING_DOJO_PREWARM" | "TRAINING_DOJO_LAZY" | "HUNT_ARGS" | "HUNT_MATCHMAKER"; + +type ProcessExitInfo = { + code: number | null, + signal: NodeJS.Signals | null +}; + export let Gameservers: Gameserver[] = []; let FreePorts: number[] = []; +const PendingGameserverReadyById = new Map(); +const PlayerServerAssignments = new Map(); +const PlayerToServerId = new Map(); +const ServerToPlayers = new Map>(); + +export type ShutdownReasonCode = "none" | "idle" | "neverConnected" | "escalation"; +export interface GameserverIdleDecision { + code: ShutdownReasonCode, + serverId: string, + reason: string, + idleSeconds: number, + zeroHeartbeatStreak: number +} + +function RemovePlayerAssignment(PlayerId: string){ + const ServerId = PlayerToServerId.get(PlayerId); + PlayerToServerId.delete(PlayerId); + const Server = PlayerServerAssignments.get(PlayerId); + PlayerServerAssignments.delete(PlayerId); + if(ServerId != undefined){ + const Players = ServerToPlayers.get(ServerId); + Players?.delete(PlayerId); + if(Players != undefined && Players.size === 0) + ServerToPlayers.delete(ServerId); + } + return Server; +} + +function AddPlayerAssignment(PlayerId: string, Server: Gameserver){ + const Previous = PlayerServerAssignments.get(PlayerId); + if(Previous != undefined && Previous !== Server) + ServerToPlayers.get(Previous.id)?.delete(PlayerId); + PlayerServerAssignments.set(PlayerId, Server); + PlayerToServerId.set(PlayerId, Server.id); + let Players = ServerToPlayers.get(Server.id); + if(Players == undefined){ + Players = new Set(); + ServerToPlayers.set(Server.id, Players); + } + Players.add(PlayerId); +} + +let RamsgateServer : Gameserver | undefined; +let TrainingDojoServer : Gameserver | undefined; +let TrainingDojoStartup: Promise | undefined; +let RamsgateStartup: Promise | undefined; +let RamsgateShutdown: Promise | undefined; + +function CreateLifecycleRetirementState(): LifecycleRetirementState { + return { + protocolVersion: undefined, + heartbeatSequence: undefined, + observationSequence: undefined, + worldEpoch: undefined, + zeroHeartbeatStreak: 0, + zeroSinceTime: undefined, + armedHeartbeatSequence: undefined + }; +} + +function CancelLifecycleRetirement(Server: Gameserver, Reason: string){ + const State = Server.lifecycleRetirement; + if(State.zeroHeartbeatStreak > 0 || State.armedHeartbeatSequence != undefined){ + logger.debug({ + serverId: Server.id, + port: Server.port, + reason: Reason, + zeroHeartbeatStreak: State.zeroHeartbeatStreak, + armedHeartbeatSequence: State.armedHeartbeatSequence + }, "Cancelled gameserver idle-retirement evidence"); + } + State.zeroHeartbeatStreak = 0; + State.zeroSinceTime = undefined; + State.armedHeartbeatSequence = undefined; +} + +function TouchGameserver(Server: Gameserver, Now = new Date()){ + Server.lastTouchedTime = Now; + CancelLifecycleRetirement(Server, "activity_touch"); +} + +function AssignPlayersToServer(Server: Gameserver, Players: ExpectedPlayer[]){ + if(Players.length === 0) + return; + + const PlayerIds = new Set(Players.map((Player) => Player.playerUid)); + const PreviousServers = new Set(); + + for(const Player of Players){ + const Previous = PlayerServerAssignments.get(Player.playerUid); + if(Previous != undefined && Previous !== Server){ + PreviousServers.add(Previous); + Previous.expectedPlayers = Previous.expectedPlayers?.filter( + (Existing) => Existing.playerUid !== Player.playerUid); + } + AddPlayerAssignment(Player.playerUid, Server); + } + + const Existing = new Set( + (Server.expectedPlayers ?? []).map((Player) => Player.playerUid)); + Server.expectedPlayers ??= []; + for(const Player of Players){ + if(!Existing.has(Player.playerUid)){ + Server.expectedPlayers.push(Player); + Existing.add(Player.playerUid); + } + } -let RamsgateServer : Gameserver; -let TrainingDojoServer : Gameserver; + logger.info({ + serverId: Server.id, + port: Server.port, + origin: Server.origin, + playerIds: [...PlayerIds], + previousServers: [...PreviousServers].map((Previous) => ({ + id: Previous.id, + port: Previous.port, + processId: Previous.processId + })) + }, "Updated authoritative player gameserver assignments"); +} const PORT_RANGE_BEGIN = Number(process.env.PORT_RANGE_BEGIN!); const PORT_RANGE_END = Number(process.env.PORT_RANGE_END!); const RAMSGATE_PORT = PORT_RANGE_END; const TRAINING_DOJO_PORT = PORT_RANGE_END - 1; -const GAMESERVER_BINARY_PATH = process.env.GAMESERVER_BINARY_PATH!; +const GAMESERVER_BINARY_PATH = process.env.GAMESERVER_BINARY_PATH!.replace(/^"|"$/g, ""); +const GAMESERVER_DLL_PATH = join(dirname(GAMESERVER_BINARY_PATH), "UndauntedInternalServer.dll"); const STANDARD_GAMESERVER_ARGS = ["-EpicPortal", "-server", "-nullrhi"]; const METAGAME_API_KEY = process.env.METAGAME_API_KEY!; const MY_IP = process.env.MY_IP!; +const DEPLOYSERVER_PORT = process.env.PORT!; +const GAMESERVER_READY_CALLBACK_URL = process.env.GAMESERVER_READY_CALLBACK_URL || `http://127.0.0.1:${DEPLOYSERVER_PORT}/api/gameservers/ready`; +const GAMESERVER_LIFECYCLE_CALLBACK_URL = `http://127.0.0.1:${DEPLOYSERVER_PORT}/api/gameservers/heartbeat`; const SECONDS_TO_WAIT_BETWEEN_GAMESERVER_STARTUP = Number(process.env.SECONDS_TO_WAIT_BETWEEN_GAMESERVER_STARTUP!); +const GAMESERVER_STARTUP_TIMEOUT_SECONDS = Number(process.env.GAMESERVER_STARTUP_TIMEOUT_SECONDS || "60"); +const ADOPT_GAMESERVER = process.env.ADOPT_GAMESERVER !== "false"; +const RAMSGATE_IDLE_SHUTDOWN_SECONDS = ParseBoundedIntegerEnvironment( + "RAMSGATE_IDLE_SHUTDOWN_SECONDS", 10, 0, 86400); +const LIFECYCLE_PROTOCOL_VERSION = 2; +const LIFECYCLE_HEARTBEAT_STALE_SECONDS = 7.5; +const LIFECYCLE_MINIMUM_ZERO_HEARTBEATS = 3; +const NEVER_CONNECTED_IDLE_GRACE_SECONDS = 90; +const PREWARM_TRAINING_DOJO = process.env.PREWARM_TRAINING_DOJO === "true"; +const TRAINING_DOJO_IDLE_SHUTDOWN_SECONDS = Number(process.env.TRAINING_DOJO_IDLE_SHUTDOWN_SECONDS || "300"); +const HUNT_IDLE_SHUTDOWN_SECONDS = Number(process.env.HUNT_IDLE_SHUTDOWN_SECONDS || "90"); +const ESCALATION_IDLE_SHUTDOWN_SECONDS = ParseBoundedIntegerEnvironment( + "ESCALATION_IDLE_SHUTDOWN_SECONDS", 30, 0, 86400); +const GAMESERVER_CONSOLE_LOG = process.env.GAMESERVER_CONSOLE_LOG === "true"; +const GAMESERVER_ASSET_STRIPPING_MODE = ParseAssetStrippingMode(process.env.GAMESERVER_ASSET_STRIPPING_MODE); +const GAMESERVER_STRIP_INACTIVE_MAP_PACKAGES = ParseBooleanEnvironment("GAMESERVER_STRIP_INACTIVE_MAP_PACKAGES", true); +const GAMESERVER_ASSET_STRIPPING_LOG_DETAILS = ParseBooleanEnvironment("GAMESERVER_ASSET_STRIPPING_LOG_DETAILS", false); +const GAMESERVER_ASSET_GC_WAIT_SECONDS = ParseBoundedIntegerEnvironment("GAMESERVER_ASSET_GC_WAIT_SECONDS", 15, 1, 15); +const GAMESERVER_PROFILING = ParseBooleanEnvironment("GAMESERVER_PROFILING", false); +const GAMESERVER_PROFILE_INTERVAL_SECONDS = ParseBoundedIntegerEnvironment("GAMESERVER_PROFILE_INTERVAL_SECONDS", 30, 10, 3600); +const GAMESERVER_DIAGNOSTICS_DIRECTORY = process.env.GAMESERVER_DIAGNOSTICS_DIRECTORY?.trim() || ""; +const GAMESERVER_PROFILE_MAX_BYTES = ParseBoundedIntegerEnvironment( + "GAMESERVER_PROFILE_MAX_BYTES", 64 * 1024 * 1024, 1024 * 1024, 1024 * 1024 * 1024); +const GAMESERVER_CONSIDER_CACHE_MAX_AGE_MS = ParseBoundedIntegerEnvironment( + "GAMESERVER_CONSIDER_CACHE_MAX_AGE_MS", 250, 50, 5000); + +function ParseBooleanEnvironment(Name: string, Fallback: boolean){ + const RawValue = process.env[Name]; + + if(RawValue == undefined || RawValue.trim().length === 0){ + return Fallback; + } + + if(RawValue === "true" || RawValue === "1"){ + return true; + } + + if(RawValue === "false" || RawValue === "0"){ + return false; + } + + logger.warn({name: Name, value: RawValue, fallback: Fallback}, "Invalid boolean gameserver configuration; using fallback"); + return Fallback; +} + +function ParseBoundedIntegerEnvironment(Name: string, Fallback: number, Minimum: number, Maximum: number){ + const RawValue = process.env[Name]; + const ParsedValue = Number(RawValue); + + if(RawValue == undefined || RawValue.trim().length === 0 || !Number.isInteger(ParsedValue) || ParsedValue < Minimum || ParsedValue > Maximum){ + if(RawValue != undefined && RawValue.trim().length > 0){ + logger.warn({name: Name, value: RawValue, minimum: Minimum, maximum: Maximum, fallback: Fallback}, "Invalid numeric gameserver configuration; using fallback"); + } + return Fallback; + } + + return ParsedValue; +} + +function ParseAssetStrippingMode(Value: string | undefined): GameserverOptimizationMetrics["mode"]{ + if(Value === "off" || Value === "report" || Value === "safe" || Value === "aggressive"){ + return Value; + } + + if(Value != undefined && Value.trim().length > 0){ + logger.warn({value: Value, fallback: "aggressive"}, "Invalid asset stripping mode; using fallback"); + } + + return "aggressive"; +} + +function IsEscalationServer(HuntId: string | undefined, MatchmakerHuntId: string | undefined, MapPath: string | undefined){ + return [HuntId, MatchmakerHuntId, MapPath] + .some((Value) => Value?.toLowerCase().includes("escalation")); +} + +function GetHuntIdleShutdownSeconds(HuntId: string | undefined, MatchmakerHuntId: string | undefined, MapPath: string | undefined){ + if(IsEscalationServer(HuntId, MatchmakerHuntId, MapPath)){ + return ESCALATION_IDLE_SHUTDOWN_SECONDS; + } + + return HUNT_IDLE_SHUTDOWN_SECONDS; +} + +function FormatHex32(Value: number){ + return `0x${(Value >>> 0).toString(16).toUpperCase().padStart(8, "0")}`; +} + +function ToSigned32(Value: number){ + return Value > 0x7FFFFFFF ? Value - 0x100000000 : Value; +} + +function DescribeWindowsExitCode(Code: number | null){ + if(Code == undefined){ + return "code=null"; + } + + const UnsignedCode = Code >>> 0; + const SignedCode = ToSigned32(UnsignedCode); + const KnownStatus = KnownWindowsExitStatuses.get(UnsignedCode); + const Parts = [ + `code=${Code}`, + `hex=${FormatHex32(UnsignedCode)}` + ]; + + if(SignedCode !== Code){ + Parts.push(`signed=${SignedCode}`); + } + + if(KnownStatus != undefined){ + Parts.push(`status=${KnownStatus}`); + } + + return Parts.join(" "); +} + +function DescribeProcessExit(Exit: ProcessExitInfo){ + return `${DescribeWindowsExitCode(Exit.code)} signal=${Exit.signal ?? "null"}`; +} + +const KnownWindowsExitStatuses = new Map([ + [0xC0000005, "STATUS_ACCESS_VIOLATION"], + [0xC000001D, "STATUS_ILLEGAL_INSTRUCTION"], + [0xC00000FD, "STATUS_STACK_OVERFLOW"], + [0xC0000135, "STATUS_DLL_NOT_FOUND"], + [0xC0000139, "STATUS_ENTRYPOINT_NOT_FOUND"], + [0xC0000142, "STATUS_DLL_INIT_FAILED"], + [0xC0000409, "STATUS_STACK_BUFFER_OVERRUN"], + [0xC0000374, "STATUS_HEAP_CORRUPTION"], + [0xE0434352, "CLR_EXCEPTION"] +]); + +async function IsUdpPortInUse(Port: number){ + return await new Promise((resolve) => { + const Socket = dgram.createSocket("udp4"); + let Settled = false; + + const Finish = (Result: boolean) => { + if(Settled){ + return; + } + + Settled = true; + Socket.removeAllListeners(); + try{ + Socket.close(); + } + catch{ + } + resolve(Result); + }; + + Socket.once("error", (Error: NodeJS.ErrnoException) => { + Finish(Error.code === "EADDRINUSE"); + }); + + Socket.once("listening", () => Finish(false)); + Socket.bind(Port, "0.0.0.0"); + }); +} + +async function WaitForUdpPortRelease(Port: number){ + const Deadline = Date.now() + GAMESERVER_PORT_RELEASE_TIMEOUT_MS; + + while(await IsUdpPortInUse(Port)){ + if(Date.now() >= Deadline){ + throw new Error(`Timed out waiting for gameserver UDP port ${Port} to be released`); + } + + await setTimeout(50); + } +} + +function IsProcessAlive(ProcessId: number){ + try{ + kill(ProcessId, 0); + return true; + } + catch{ + return false; + } +} + +function IsGameserverProcessAlive(GameserverToCheck: Gameserver){ + return IsProcessAlive(GameserverToCheck.processId); +} + +function GetUdpPortOwnerPid(Port: number){ + try{ + const NetstatOutput = execFileSync("netstat", ["-ano"], {encoding: "utf8"}); + const PortPattern = new RegExp(`^\\s*UDP\\s+\\S+:${Port}\\s+\\S+\\s+(\\d+)\\s*$`, "mi"); + const Match = NetstatOutput.match(PortPattern); + + if(Match == undefined){ + return undefined; + } + + return Number(Match[1]); + } + catch(Error){ + logger.warn(Error, `Failed to query owner for UDP port ${Port}`); + return undefined; + } +} + +function CreateAdoptedFixedPortServer(Options: StartServerOptions, Port: number, ProcessId: number): Gameserver{ + const AdoptedServer: Gameserver = { + id: crypto.randomUUID(), + port: Port, + map: Options.map, + behemoth: Options.behemoth, + matchmakerHuntId: Options.matchmakerHuntId, + expectedPlayers: Options.expectedPlayers, + isRamsgate: Options.isRamsgate, + isTrainingDojo: Options.isTrainingDojo, + origin: Options.origin, + trigger: `adopted_existing:${Options.trigger}`, + processId: ProcessId, + startTime: new Date(), + lastTouchedTime: new Date(), + callbackToken: undefined, + lastLifecycleHeartbeatTime: undefined, + reportedLiveConnections: undefined, + reportedRawConnections: undefined, + hadLiveConnection: false, + lifecycleRetirement: CreateLifecycleRetirementState(), + shutdownAfterSeconds: Options.shutdownAfterSeconds, + expectedShutdownReason: undefined, + optimization: undefined, + dllSha256: undefined, + profiling: undefined, + pacing: undefined + }; + + Gameservers.push(AdoptedServer); + for(const Player of AdoptedServer.expectedPlayers ?? []) + AddPlayerAssignment(Player.playerUid, AdoptedServer); + logger.warn(`Adopted existing gameserver process ${ProcessId} on ${MY_IP}:${Port} origin=${Options.origin} map=${Options.map}`); + + return AdoptedServer; +} + +function RegisterPendingGameserverReady(Id: string, Port: number, Token: string, ExpectedDllSha256: string){ + return new Promise((resolve, reject) => { + PendingGameserverReadyById.set(Id, { + port: Port, + token: Token, + expectedDllSha256: ExpectedDllSha256, + resolve: resolve, + reject: reject + }); + }); +} + +function ParseProfilingStatus(Value: unknown): GameserverProfilingStatus | undefined{ + if(typeof Value !== "object" || Value == undefined){ + return undefined; + } + + const Candidate = Value as Record; + const BooleanFields = ["enabled", "started", "startupWriteSucceeded", "writerStarted"] as const; + const NumericFields = [ + "startupBytes", "startupError", "writerError", "intervalBytes", "intervalWriteFailures", "lastIntervalError" + ] as const; + + if(!BooleanFields.every((Field) => typeof Candidate[Field] === "boolean") || + !NumericFields.every((Field) => typeof Candidate[Field] === "number" && + Number.isFinite(Candidate[Field]) && Number(Candidate[Field]) >= 0) || + typeof Candidate.path !== "string" || typeof Candidate.dllSha256 !== "string" || + (Candidate.dllSha256.length !== 0 && !/^[a-fA-F0-9]{64}$/.test(Candidate.dllSha256))){ + return undefined; + } + + return Candidate as GameserverProfilingStatus; +} + +function ParseRequiredProfilingDllSha256(Value: unknown){ + if(typeof Value !== "object" || Value == undefined){ + return undefined; + } + + const DllSha256 = (Value as Record).dllSha256; + if(typeof DllSha256 !== "string" || !/^[a-fA-F0-9]{64}$/.test(DllSha256)){ + return undefined; + } + + return DllSha256.toLowerCase(); +} + +function ParsePacingStatus(Value: unknown): GameserverPacingStatus | undefined{ + if(typeof Value !== "object" || Value == undefined){ + return undefined; + } + + const Candidate = Value as Record; + const States = new Set(["disabled", "observing", "active", "fallback", "unknown"]); + const BooleanFields = [ + "executableBuildValid", "signaturesValid", "hookEnabled", "highResolutionApiAvailable", + "timerCreated", "correctionEverActivated" + ] as const; + const NumericFields = [ + "observedFrames", "cvarMaxFps", "cachedMaxFps", "virtualMaxFps", + "rollingMedianCadenceHz", "rollingMedianCoarseOvershootMs" + ] as const; + + if(!States.has(String(Candidate.state)) || typeof Candidate.installFailure !== "string" || + typeof Candidate.fallbackReason !== "string" || + !BooleanFields.every((Field) => typeof Candidate[Field] === "boolean") || + !NumericFields.every((Field) => typeof Candidate[Field] === "number" && + Number.isFinite(Candidate[Field]) && Number(Candidate[Field]) >= 0)){ + return undefined; + } + + return Candidate as GameserverPacingStatus; +} + +function ParseOptimizationMetrics(Value: unknown): GameserverOptimizationMetrics | undefined{ + if(typeof Value !== "object" || Value == undefined){ + return undefined; + } + + const Candidate = Value as Record; + const Modes = new Set(["off", "report", "safe", "aggressive"]); + const SafetyGates = new Set(["not_required", "passed", "initial_load", "connections_present", "timeout", "signature_mismatch", "failed"]); + const NumericFields = [ + "candidates", "collected", "retained", "textures", "materials", "sounds", + "mapPackageCandidates", "activeMapPackages", "inactiveMapPackages", "durationMs", + "workingSetBefore", "workingSetAfter", "privateBytesBefore", "privateBytesAfter", + "configuredMaxFps", "observedMaxFps", "preReadyNetworkingPasses", + "netServerMaxTickRate", "maxNetTickRate", "bootstrapMinimumMilliseconds", + "bootstrapMaximumMilliseconds", "considerCacheMaxAgeMilliseconds" + ] as const; + + if(!Modes.has(String(Candidate.mode)) || !SafetyGates.has(String(Candidate.safetyGate)) || + typeof Candidate.signatureValid !== "boolean" || typeof Candidate.profilingEnabled !== "boolean" || + typeof Candidate.capSignatureValid !== "boolean" || typeof Candidate.capResolved !== "boolean" || + typeof Candidate.capApplied !== "boolean" || typeof Candidate.capVerified !== "boolean" || + typeof Candidate.failed !== "boolean" || + !NumericFields.every((Field) => typeof Candidate[Field] === "number" && Number.isFinite(Candidate[Field]) && Number(Candidate[Field]) >= 0) || + (Candidate.error != undefined && typeof Candidate.error !== "string")){ + return undefined; + } + + return Candidate as GameserverOptimizationMetrics; +} + +export function HandleGameserverReadyCallback(Token: string | undefined, Body: unknown){ + if(typeof Body !== "object" || Body == undefined){ + return { + status: 400, + body: {ready: false, error: "invalid_body"} + }; + } + + const Payload = Body as Partial; + + if(typeof Payload.id !== "string" || typeof Payload.port !== "number" || typeof Payload.pid !== "number" || + (Payload.ready != undefined && typeof Payload.ready !== "boolean") || + (Payload.error != undefined && typeof Payload.error !== "string")){ + return { + status: 400, + body: {ready: false, error: "invalid_payload"} + }; + } + + const PendingReady = PendingGameserverReadyById.get(Payload.id); + + if(PendingReady == undefined){ + return { + status: 404, + body: {ready: false, error: "unknown_gameserver"} + }; + } + + if(Token !== PendingReady.token){ + return { + status: 403, + body: {ready: false, error: "invalid_token"} + }; + } + + if(Payload.port !== PendingReady.port){ + return { + status: 409, + body: {ready: false, error: "port_mismatch"} + }; + } + + if(Payload.ready === false){ + const FailureReason = Payload.error?.trim() || "unspecified native startup failure"; + PendingReady.reject(new Error(`Gameserver on port ${Payload.port} refused readiness: ${FailureReason}`)); + + return { + status: 200, + body: {ready: false, error: "startup_failed"} + }; + } + + const Optimization = ParseOptimizationMetrics(Payload.optimization); + const Profiling = ParseProfilingStatus(Payload.profiling); + const Pacing = ParsePacingStatus(Payload.pacing); + const ReportedDllSha256 = ParseRequiredProfilingDllSha256(Payload.profiling); + + // Adopted fixed-port processes never register a pending readiness callback, + // so this strict check applies only to a process spawned (and hashed) by this + // DeployServer. Keep the rest of the profiling payload optional: a future + // producer may add fields without making a valid hash unusable. + if(GAMESERVER_PROFILING && ReportedDllSha256 == undefined){ + PendingReady.reject(new Error( + `Gameserver on port ${Payload.port} did not report a valid profiling DLL SHA-256`)); + return { + status: 409, + body: {ready: false, error: "profiling_dll_hash_missing_or_invalid"} + }; + } + + if(GAMESERVER_PROFILING && ReportedDllSha256 !== PendingReady.expectedDllSha256){ + PendingReady.reject(new Error( + `Gameserver on port ${Payload.port} reported DLL SHA-256 ${ReportedDllSha256}; expected ${PendingReady.expectedDllSha256}`)); + return { + status: 409, + body: {ready: false, error: "profiling_dll_hash_mismatch"} + }; + } + + if(Payload.optimization != undefined && Optimization == undefined){ + logger.warn({serverId: Payload.id, port: Payload.port}, "Ignoring invalid optional gameserver optimization metrics"); + } + + if(Payload.profiling != undefined && Profiling == undefined){ + logger.warn({serverId: Payload.id, port: Payload.port}, "Ignoring invalid optional gameserver profiling status"); + } + + if(Payload.pacing != undefined && Pacing == undefined){ + logger.warn({serverId: Payload.id, port: Payload.port}, "Ignoring invalid optional gameserver pacing status"); + } + + PendingReady.resolve({ + id: Payload.id, + port: Payload.port, + pid: Payload.pid, + ready: true, + optimization: Optimization, + profiling: Profiling, + pacing: Pacing + }); + + return { + status: 200, + body: {ready: true} + }; +} + +export function HandleGameserverHeartbeat(Token: string | undefined, Body: unknown){ + if(typeof Body !== "object" || Body == undefined){ + return {status: 400, body: {accepted: false, error: "invalid_body"}}; + } + + const Payload = Body as Partial; + if(typeof Payload.id !== "string" || typeof Payload.port !== "number" + || typeof Payload.pid !== "number" + || typeof Payload.liveConnections !== "number" + || typeof Payload.rawConnections !== "number" + || !Number.isSafeInteger(Payload.port) || !Number.isSafeInteger(Payload.pid) + || !Number.isSafeInteger(Payload.liveConnections) + || Payload.liveConnections < 0 || Payload.liveConnections > 128 + || !Number.isSafeInteger(Payload.rawConnections) + || Payload.rawConnections < 0 || Payload.rawConnections > 128){ + return {status: 400, body: {accepted: false, error: "invalid_payload"}}; + } + + const Server = Gameservers.find((Candidate) => Candidate.id === Payload.id); + if(Server == undefined){ + return {status: 404, body: {accepted: false, error: "unknown_gameserver"}}; + } + if(Token == undefined || Token !== Server.callbackToken){ + return {status: 403, body: {accepted: false, error: "invalid_token"}}; + } + if(Server.port !== Payload.port || Server.processId !== Payload.pid){ + return {status: 409, body: {accepted: false, error: "identity_mismatch"}}; + } + + const LifecycleFields = [Payload.protocolVersion, Payload.heartbeatSequence, + Payload.observationSequence, Payload.worldEpoch]; + const HasAnyLifecycleField = LifecycleFields.some((Value) => Value != undefined); + const HasValidLifecycleFields = + Payload.protocolVersion === LIFECYCLE_PROTOCOL_VERSION + && typeof Payload.heartbeatSequence === "number" + && Number.isSafeInteger(Payload.heartbeatSequence) + && Payload.heartbeatSequence > 0 + && typeof Payload.observationSequence === "number" + && Number.isSafeInteger(Payload.observationSequence) + && Payload.observationSequence > 0 + && typeof Payload.worldEpoch === "number" + && Number.isSafeInteger(Payload.worldEpoch) + && Payload.worldEpoch >= 0 + && Payload.worldEpoch <= 0xffff; + if(HasAnyLifecycleField && !HasValidLifecycleFields){ + CancelLifecycleRetirement(Server, "invalid_lifecycle_fields"); + return {status: 400, body: {accepted: false, error: "invalid_lifecycle_fields"}}; + } + + const Now = new Date(); + const PreviouslyConnected = + (Server.reportedLiveConnections ?? 0) > 0 || + (Server.reportedRawConnections ?? 0) > 0; + Server.lastLifecycleHeartbeatTime = Now; + Server.reportedLiveConnections = Payload.liveConnections; + Server.reportedRawConnections = Payload.rawConnections; + const IsConnected = + Payload.liveConnections > 0 || Payload.rawConnections > 0; + if(IsConnected !== PreviouslyConnected){ + logger.info({ + serverId: Server.id, + port: Server.port, + liveConnections: Payload.liveConnections, + rawConnections: Payload.rawConnections, + lastPositiveConnection: IsConnected + ? Now.toISOString() + : Server.lastTouchedTime.toISOString() + }, IsConnected + ? "Gameserver observed a positive connection" + : "Gameserver observed connection close; zero-evidence window begins"); + } + + const State = Server.lifecycleRetirement; + if(!HasValidLifecycleFields){ + State.protocolVersion = undefined; + State.heartbeatSequence = undefined; + State.observationSequence = undefined; + State.worldEpoch = undefined; + CancelLifecycleRetirement(Server, "legacy_heartbeat"); + if(Payload.liveConnections > 0 || Payload.rawConnections > 0){ + Server.hadLiveConnection = true; + TouchGameserver(Server, Now); + } + return {status: 200, body: {accepted: true, retirementEligible: false}}; + } + + const HeartbeatSequence = Payload.heartbeatSequence!; + const ObservationSequence = Payload.observationSequence!; + const WorldEpoch = Payload.worldEpoch!; + if(State.heartbeatSequence != undefined && + HeartbeatSequence <= State.heartbeatSequence){ + CancelLifecycleRetirement(Server, "heartbeat_sequence_regression"); + return {status: 409, body: {accepted: false, error: "stale_heartbeat"}}; + } + + const ObservationAdvanced = State.observationSequence == undefined + || ObservationSequence > State.observationSequence; + const WorldChanged = State.worldEpoch != undefined && State.worldEpoch !== WorldEpoch; + State.protocolVersion = LIFECYCLE_PROTOCOL_VERSION; + State.heartbeatSequence = HeartbeatSequence; + if(!ObservationAdvanced){ + CancelLifecycleRetirement(Server, "game_thread_observation_not_advanced"); + if(Payload.liveConnections > 0 || Payload.rawConnections > 0){ + Server.hadLiveConnection = true; + TouchGameserver(Server, Now); + } + return {status: 200, body: {accepted: true, retirementEligible: false}}; + } + State.observationSequence = ObservationSequence; + State.worldEpoch = WorldEpoch; + + if(WorldChanged){ + TouchGameserver(Server, Now); + logger.info({serverId: Server.id, port: Server.port, worldEpoch: WorldEpoch}, + "Gameserver lifecycle world changed; reset idle-retirement evidence"); + } + + if(Payload.liveConnections > 0 || Payload.rawConnections > 0){ + Server.hadLiveConnection = true; + TouchGameserver(Server, Now); + } + else { + if(State.zeroHeartbeatStreak === 0){ + State.zeroSinceTime = Now; + logger.debug({serverId: Server.id, port: Server.port, + heartbeatSequence: HeartbeatSequence, observationSequence: ObservationSequence, + worldEpoch: WorldEpoch}, "Gameserver entered zero-connection lifecycle state"); + } + ++State.zeroHeartbeatStreak; + } + + return {status: 200, body: {accepted: true, retirementEligible: true}}; +} + +async function WaitForProcessExit(ProcessId: number, Port: number, ShouldStop: () => boolean): Promise{ + while(!ShouldStop()){ + if(!IsProcessAlive(ProcessId)){ + throw new Error(`Gameserver on port ${Port} stopped during startup: process ${ProcessId} exited`); + } + + await setTimeout(250); + } + + return await new Promise(() => {}); +} + +async function WaitForGameserverReady(StartedProcess: StartedGameserverProcess, Id: string, Port: number, ReadyPromise: Promise){ + let StopPollingProcess = false; + const StartupFailurePromise = StartedProcess.child != undefined + ? new Promise((_, reject) => { + StartedProcess.child!.once("exit", (Code, Signal) => { + reject(new Error(`Gameserver on port ${Port} stopped during startup: ${DescribeProcessExit({code: Code, signal: Signal})}`)); + }); + + StartedProcess.child!.once("error", (SpawnError) => { + reject(new Error(`Gameserver on port ${Port} stopped during startup: spawn error: ${SpawnError.message}`)); + }); + }) + : WaitForProcessExit(StartedProcess.processId, Port, () => StopPollingProcess); + + try{ + return await Promise.race([ + ReadyPromise, + StartupFailurePromise, + setTimeout(GAMESERVER_STARTUP_TIMEOUT_SECONDS * 1000).then(() => { + throw new Error(`Gameserver on port ${Port} did not report ready within ${GAMESERVER_STARTUP_TIMEOUT_SECONDS}s`); + }) + ]); + } + finally{ + StopPollingProcess = true; + PendingGameserverReadyById.delete(Id); + } +} + +function TransformExpectedPlayerArgs(ExpectedPlayers: ExpectedPlayer[]){ + let ToReturn = ""; + + for(const Player of ExpectedPlayers){ + ToReturn = ToReturn + Player.playerUid + ":" + Player.playerHuntId + ","; + } + + if(ToReturn.length > 0){ + ToReturn = ToReturn.slice(0, -1); // Remove trailing ',' + } + + return ToReturn; +} + +async function ComputeGameserverDllSha256(){ + return await new Promise((resolve, reject) => { + const Hash = crypto.createHash("sha256"); + const Stream = createReadStream(GAMESERVER_DLL_PATH); + + Stream.on("data", (Chunk) => Hash.update(Chunk)); + Stream.once("error", reject); + Stream.once("end", () => resolve(Hash.digest("hex"))); + }); +} + +async function StartGameserverProcess(Args: string[]): Promise{ + const Child = spawn(GAMESERVER_BINARY_PATH, Args, { + detached: false, + stdio: "ignore", + windowsHide: !GAMESERVER_CONSOLE_LOG + }); + + return { + processId: Child.pid!, + child: Child + }; +} + +export async function CleanupServer(ServerToShutdown: Gameserver){ + const WasTracked = Gameservers.includes(ServerToShutdown); + Gameservers = Gameservers.filter(Server => Server !== ServerToShutdown); + for(const PlayerId of [...(ServerToPlayers.get(ServerToShutdown.id) ?? [])]) + RemovePlayerAssignment(PlayerId); + ServerToPlayers.delete(ServerToShutdown.id); + + if(!WasTracked && RamsgateServer !== ServerToShutdown && TrainingDojoServer !== ServerToShutdown){ + logger.warn(`Skipping cleanup for already removed gameserver ${ServerToShutdown.processId} on port ${ServerToShutdown.port}`); + return; + } + + if(ServerToShutdown.isRamsgate){ + if(RamsgateServer === ServerToShutdown){ + RamsgateServer = undefined; + } + + if(ServerToShutdown.expectedShutdownReason == undefined){ + logger.warn("Ramsgate stopped unexpectedly; it will lazy-start on the next CITY request"); + } + else{ + logger.info("Ramsgate stopped cleanly and remains dormant until the next CITY request"); + } + } + else if(ServerToShutdown.isTrainingDojo){ + logger.warn("Training Dojo cleaned up; it will lazy-start on the next request"); + + if(TrainingDojoServer === ServerToShutdown){ + TrainingDojoServer = undefined; + } + + TrainingDojoStartup = undefined; + } + else{ + FreePorts.push(ServerToShutdown.port); + } +} -function TransformExpectedPlayerArgs(ExpectedPlayers: ExpectedPlayer[]){ - let ToReturn = ""; +async function CleanupFailedStartup(NewGameserver: Gameserver, Options: StartServerOptions){ + NewGameserver.expectedShutdownReason = "startup_failed"; - for(const Player of ExpectedPlayers){ - ToReturn = ToReturn + Player.playerUid + ":" + Player.playerHuntId + ","; + try{ + kill(NewGameserver.processId); + } + catch(Error){ + logger.warn(Error, `Failed to signal failed gameserver startup ${NewGameserver.processId} on port ${NewGameserver.port}`); } - if(ToReturn.length > 0){ - ToReturn = ToReturn.slice(0, -1); // Remove trailing ',' + Gameservers = Gameservers.filter(Server => Server !== NewGameserver); + + if(Options.isRamsgate){ + if(RamsgateServer === NewGameserver){ + RamsgateServer = undefined; + } + + return; } - return ToReturn; -} + if(Options.isTrainingDojo){ + if(TrainingDojoServer === NewGameserver){ + TrainingDojoServer = undefined; + } -export async function CleanupServer(ServerToShutdown: Gameserver){ - Gameservers = Gameservers.filter(Server => Server !== ServerToShutdown); + TrainingDojoStartup = undefined; + return; + } - if(ServerToShutdown.isRamsgate){ - logger.warn("RAMSGATE HAS FALLEN! Restarting!"); + FreePorts.push(NewGameserver.port); +} - await StartServer(RAMSGATE_MAP_PATH, undefined, undefined, undefined, true, false); +export async function ShutdownServer(ServerToShutdown: Gameserver, Reason: string){ + if(ServerToShutdown.expectedShutdownReason != undefined){ + logger.debug({ + serverId: ServerToShutdown.id, + port: ServerToShutdown.port, + existingReason: ServerToShutdown.expectedShutdownReason, + ignoredReason: Reason + }, "Gameserver shutdown already in progress"); + return; } - else if(ServerToShutdown.isTrainingDojo){ - logger.warn("Training Dojo Crashed! Restarting!"); - await StartServer(TRAINING_DOJO_MAP_PATH, undefined, undefined, undefined, false, true); + logger.info(`Shutting down gameserver ${ServerToShutdown.processId} on port ${ServerToShutdown.port} (${ServerToShutdown.origin}) due to ${Reason}`); + ServerToShutdown.expectedShutdownReason = Reason; + + try{ + kill(ServerToShutdown.processId); } - else{ - FreePorts.push(ServerToShutdown.port); + catch(Error){ + logger.warn(Error, `Failed to signal gameserver ${ServerToShutdown.processId} on port ${ServerToShutdown.port}`); } + + await WaitForUdpPortRelease(ServerToShutdown.port); + + await CleanupServer(ServerToShutdown); } let ServerLaunchQueue: Promise = Promise.resolve(); -async function StartServer(Map: string, Behemoth: string | undefined, MatchmakerHuntId: string | undefined, ExpectedPlayers: ExpectedPlayer[] | undefined, IsRamsgate: boolean, IsTrainingDojo: boolean){ +type StartServerOptions = { + map: string, + behemoth: string | undefined, + matchmakerHuntId: string | undefined, + expectedPlayers: ExpectedPlayer[] | undefined, + isRamsgate: boolean, + isTrainingDojo: boolean, + origin: GameserverOrigin, + trigger: string, + shutdownAfterSeconds: number | undefined +}; + +async function StartServer(Options: StartServerOptions){ const LaunchProc = ServerLaunchQueue; ServerLaunchQueue = ServerLaunchQueue.catch(() => {}).then(async () => await setTimeout(SECONDS_TO_WAIT_BETWEEN_GAMESERVER_STARTUP * 1000)); await LaunchProc; - let Port; + let Port: number | undefined; - if(IsRamsgate){ + if(Options.isRamsgate){ Port = RAMSGATE_PORT; } - else if(IsTrainingDojo){ + else if(Options.isTrainingDojo){ Port = TRAINING_DOJO_PORT; } else{ - Port = FreePorts.pop(); + while(Port == undefined && FreePorts.length > 0){ + const CandidatePort = FreePorts.pop()!; + + if(await IsUdpPortInUse(CandidatePort)){ + logger.warn(`Skipping port ${CandidatePort} because it is already in use before gameserver startup`); + continue; + } + + Port = CandidatePort; + } } const Id = crypto.randomUUID(); if(Port == undefined){ - throw new Error("No free ports left!"); + throw new NoFreeHuntPortsError(); + } + + if((Options.isRamsgate || Options.isTrainingDojo) && await IsUdpPortInUse(Port)){ + const OwnerPid = GetUdpPortOwnerPid(Port); + + if(OwnerPid != undefined){ + if(ADOPT_GAMESERVER){ + return CreateAdoptedFixedPortServer(Options, Port, OwnerPid); + } + + throw new Error(`Fixed gameserver port ${Port} is already owned by pid ${OwnerPid}; refusing to adopt it with ADOPT_GAMESERVER=false`); + } + + throw new Error(`Fixed gameserver port ${Port} is already in use before startup and its owner could not be identified`); + } + + let DllSha256: string; + + try{ + DllSha256 = await ComputeGameserverDllSha256(); + } + catch(CaughtError){ + if(!Options.isRamsgate && !Options.isTrainingDojo){ + FreePorts.push(Port); + } + throw new Error(`Failed to hash gameserver DLL at ${GAMESERVER_DLL_PATH}: ${CaughtError instanceof Error ? CaughtError.message : String(CaughtError)}`); } - const Child = spawn(GAMESERVER_BINARY_PATH, [ + const ReadyCallbackToken = crypto.randomBytes(32).toString("hex"); + const ReadyPromise = RegisterPendingGameserverReady(Id, Port, ReadyCallbackToken, DllSha256); + + const GameserverArgs = [ METAGAME_API_KEY, Port.toString(), - Map, - Behemoth != undefined ? Behemoth : "NO_BEHEMOTH", - MatchmakerHuntId != undefined ? MatchmakerHuntId : "NO_MM_HUNTID", - ExpectedPlayers != undefined ? TransformExpectedPlayerArgs(ExpectedPlayers) : "NO_EXPECTED_PLAYERS", + Options.map, + Options.behemoth != undefined ? Options.behemoth : "NO_BEHEMOTH", + Options.matchmakerHuntId != undefined ? Options.matchmakerHuntId : "NO_MM_HUNTID", + Options.expectedPlayers != undefined ? TransformExpectedPlayerArgs(Options.expectedPlayers) : "NO_EXPECTED_PLAYERS", MY_IP + ":" + Port.toString(), + Id, + GAMESERVER_READY_CALLBACK_URL, + ReadyCallbackToken, + `-undauntedConsoleLog=${GAMESERVER_CONSOLE_LOG}`, + `-undauntedAssetStrippingMode=${GAMESERVER_ASSET_STRIPPING_MODE}`, + `-undauntedStripInactiveMapPackages=${GAMESERVER_STRIP_INACTIVE_MAP_PACKAGES}`, + `-undauntedAssetStrippingLogDetails=${GAMESERVER_ASSET_STRIPPING_LOG_DETAILS}`, + `-undauntedAssetGcWaitSeconds=${GAMESERVER_ASSET_GC_WAIT_SECONDS}`, + `-undauntedProfiling=${GAMESERVER_PROFILING}`, + `-undauntedProfileIntervalSeconds=${GAMESERVER_PROFILE_INTERVAL_SECONDS}`, + `-undauntedProfileOutputDirectory=${GAMESERVER_DIAGNOSTICS_DIRECTORY}`, + `-undauntedProfileMaxBytes=${GAMESERVER_PROFILE_MAX_BYTES}`, + `-undauntedConsiderCacheMaxAgeMs=${GAMESERVER_CONSIDER_CACHE_MAX_AGE_MS}`, + `-undauntedLifecycleCallbackUrl=${GAMESERVER_LIFECYCLE_CALLBACK_URL}`, + `-undauntedDllSha256=${DllSha256}`, ...STANDARD_GAMESERVER_ARGS - ]); + ]; + + logger.info({ + id: Id, + port: Port, + origin: Options.origin, + trigger: Options.trigger, + map: Options.map, + dllPath: GAMESERVER_DLL_PATH, + dllSha256: DllSha256, + behemoth: Options.behemoth, + matchmakerHuntId: Options.matchmakerHuntId, + expectedPlayers: Options.expectedPlayers + }, "Starting gameserver process"); - Child.unref(); + const StartedProcess = await StartGameserverProcess(GameserverArgs); + StartedProcess.child?.on("error", (Error) => { + logger.error(Error, `Gameserver process failed to start for port ${Port}`); + }); + let StartupCompleted = false; const NewGameserver: Gameserver = { id: Id, port: Port, - map: Map, - behemoth: Behemoth, - matchmakerHuntId: MatchmakerHuntId, - expectedPlayers: ExpectedPlayers, - isRamsgate: IsRamsgate, - isTrainingDojo: IsTrainingDojo, - processId: Child.pid!, - startTime: new Date() + map: Options.map, + behemoth: Options.behemoth, + matchmakerHuntId: Options.matchmakerHuntId, + expectedPlayers: Options.expectedPlayers, + isRamsgate: Options.isRamsgate, + isTrainingDojo: Options.isTrainingDojo, + origin: Options.origin, + trigger: Options.trigger, + processId: StartedProcess.processId, + startTime: new Date(), + lastTouchedTime: new Date(), + callbackToken: ReadyCallbackToken, + lastLifecycleHeartbeatTime: undefined, + reportedLiveConnections: undefined, + reportedRawConnections: undefined, + hadLiveConnection: false, + lifecycleRetirement: CreateLifecycleRetirementState(), + shutdownAfterSeconds: Options.shutdownAfterSeconds, + expectedShutdownReason: undefined, + optimization: undefined, + dllSha256: DllSha256, + profiling: undefined, + pacing: undefined }; + StartedProcess.child?.on("exit", (Code, Signal) => { + const ExitMessage = `Gameserver process ${StartedProcess.processId} on port ${Port} exited: ${DescribeProcessExit({code: Code, signal: Signal})}`; + + if(NewGameserver.expectedShutdownReason != undefined){ + logger.info(`${ExitMessage} after expected shutdown: ${NewGameserver.expectedShutdownReason}`); + return; + } + + logger.warn(ExitMessage); + + if(!StartupCompleted){ + return; + } + + void CleanupServer(NewGameserver).catch((Error) => { + logger.error(Error, `Failed to cleanup gameserver ${StartedProcess.processId} on port ${Port} after unexpected exit`); + }); + }); + Gameservers.push(NewGameserver); + for(const Player of NewGameserver.expectedPlayers ?? []) + AddPlayerAssignment(Player.playerUid, NewGameserver); + + try{ + const ReadyPayload = await WaitForGameserverReady(StartedProcess, Id, Port, ReadyPromise); + NewGameserver.processId = ReadyPayload.pid; + NewGameserver.optimization = ReadyPayload.optimization; + NewGameserver.profiling = ReadyPayload.profiling; + NewGameserver.pacing = ReadyPayload.pacing; + } + catch(Error){ + await CleanupFailedStartup(NewGameserver, Options); + throw Error; + } + + StartupCompleted = true; + + if(NewGameserver.optimization != undefined){ + logger.info({ + serverId: NewGameserver.id, + port: Port, + optimization: NewGameserver.optimization + }, "Gameserver optimization completed before readiness"); + + } + + logger.info({ + serverId: NewGameserver.id, + port: Port, + expectedDllSha256: DllSha256, + profiling: NewGameserver.profiling, + pacing: NewGameserver.pacing + }, "Gameserver profiling startup status"); + + logger.info(`Gameserver ${NewGameserver.processId} is ready on ${MY_IP}:${Port} origin=${Options.origin} trigger=${Options.trigger} map=${Options.map}`); return NewGameserver; } -export function GetRamsgateConnectionDetails(){ +function StartRamsgateServer(Trigger: string){ + return StartServer({ + map: RAMSGATE_MAP_PATH, + behemoth: undefined, + matchmakerHuntId: undefined, + expectedPlayers: undefined, + isRamsgate: true, + isTrainingDojo: false, + origin: "RAMSGATE_LAZY", + trigger: Trigger, + shutdownAfterSeconds: RAMSGATE_IDLE_SHUTDOWN_SECONDS > 0 + ? RAMSGATE_IDLE_SHUTDOWN_SECONDS : undefined + }); +} + +export async function GetRamsgateConnectionDetails(ExpectedPlayers: string[] = []){ + if(RamsgateShutdown != undefined){ + await RamsgateShutdown; + } + if(RamsgateServer != undefined && !IsGameserverProcessAlive(RamsgateServer)){ + await CleanupServer(RamsgateServer); + } + if(RamsgateServer?.expectedShutdownReason != undefined){ + throw new Error("Ramsgate shutdown is still in progress"); + } + + if(RamsgateServer == undefined){ + if(RamsgateStartup == undefined){ + RamsgateStartup = StartRamsgateServer("city_matchmaking").then((Server) => { + RamsgateServer = Server; + return Server; + }).finally(() => { + RamsgateStartup = undefined; + }); + } + RamsgateServer = await RamsgateStartup; + } + + TouchGameserver(RamsgateServer); + AssignPlayersToServer(RamsgateServer, + TransformExpectedPlayers("ShatteredIsles_ReturnToRamsgate", ExpectedPlayers) ?? []); return { + id: RamsgateServer.id, host: MY_IP, port: RamsgateServer.port }; } +export function GetGameserverIdleShutdownReason(Server: Gameserver, Now = Date.now()): GameserverIdleDecision | undefined{ + if(Server.shutdownAfterSeconds == undefined || Server.shutdownAfterSeconds <= 0 + || Server.expectedShutdownReason != undefined || Server.callbackToken == undefined + || Server.lastLifecycleHeartbeatTime == undefined + || Server.reportedLiveConnections !== 0 || Server.reportedRawConnections !== 0){ + return undefined; + } + + const State = Server.lifecycleRetirement; + if(State.protocolVersion !== LIFECYCLE_PROTOCOL_VERSION + || State.heartbeatSequence == undefined + || State.observationSequence == undefined + || State.worldEpoch == undefined + || State.zeroHeartbeatStreak < LIFECYCLE_MINIMUM_ZERO_HEARTBEATS + || State.zeroSinceTime == undefined){ + return undefined; + } + + const HeartbeatAgeSeconds = + (Now - Server.lastLifecycleHeartbeatTime.getTime()) / 1000; + if(HeartbeatAgeSeconds > LIFECYCLE_HEARTBEAT_STALE_SECONDS){ + CancelLifecycleRetirement(Server, "heartbeat_stale"); + return undefined; + } + + const IdleSeconds = (Now - Server.lastTouchedTime.getTime()) / 1000; + const RequiredIdleSeconds = Server.hadLiveConnection + ? Server.shutdownAfterSeconds + : Math.max(NEVER_CONNECTED_IDLE_GRACE_SECONDS, Server.shutdownAfterSeconds); + if(IdleSeconds < RequiredIdleSeconds){ + return undefined; + } + + if(State.armedHeartbeatSequence == undefined){ + State.armedHeartbeatSequence = State.heartbeatSequence; + logger.info({ + serverId: Server.id, + port: Server.port, + heartbeatSequence: State.heartbeatSequence, + observationSequence: State.observationSequence, + worldEpoch: State.worldEpoch, + zeroHeartbeatStreak: State.zeroHeartbeatStreak, + idleSeconds: Math.floor(IdleSeconds) + }, "Armed gameserver idle retirement; waiting for a confirming heartbeat"); + return undefined; + } + + if(State.heartbeatSequence <= State.armedHeartbeatSequence){ + return undefined; + } + + const Code: ShutdownReasonCode = Server.hadLiveConnection ? "idle" : "neverConnected"; + return { + code: Code, + serverId: Server.id, + idleSeconds: Math.floor(IdleSeconds), + zeroHeartbeatStreak: State.zeroHeartbeatStreak, + reason: `${Code} for ${Math.floor(IdleSeconds)}s with ${State.zeroHeartbeatStreak} fresh zero-connection heartbeats` + }; +} + +export function GetRamsgateIdleShutdownReason(Server: Gameserver, Now = Date.now()){ + return Server.isRamsgate ? GetGameserverIdleShutdownReason(Server, Now) : undefined; +} + +export async function ShutdownIdleGameserver(Server: Gameserver, ExpectedDecision: GameserverIdleDecision){ + if(!Gameservers.includes(Server) || Server.expectedShutdownReason != undefined + || !IsProcessAlive(Server.processId)){ + return; + } + + const RevalidatedReason = GetGameserverIdleShutdownReason(Server); + if(RevalidatedReason == undefined){ + logger.info({serverId: Server.id, port: Server.port, expectedReason: ExpectedDecision.reason}, + "Cancelled gameserver idle retirement during final revalidation"); + return; + } + + await ShutdownServer(Server, RevalidatedReason.reason); +} + +export async function ShutdownIdleRamsgate(Server: Gameserver, Decision: GameserverIdleDecision){ + if(RamsgateServer !== Server || Server.expectedShutdownReason != undefined){ + return; + } + const Operation = RamsgateShutdown ??= ShutdownIdleGameserver(Server, Decision); + try{ + await Operation; + } + finally{ + if(RamsgateShutdown === Operation){ + RamsgateShutdown = undefined; + } + } +} + export function GetTrainingDojoConnectionDetails(){ + if(TrainingDojoServer == undefined){ + throw new Error("Training Dojo server has not started"); + } + + TouchGameserver(TrainingDojoServer); + return { + id: TrainingDojoServer.id, host: MY_IP, port: TrainingDojoServer.port }; } -export async function StartupGameserverWithArgs(GameArgs: string){ +function FindGameserverForPlayer(PlayerId: string){ + const Assigned = PlayerServerAssignments.get(PlayerId); + if(Assigned == undefined) + return undefined; + if(Gameservers.includes(Assigned) && Assigned.expectedShutdownReason == undefined) + return Assigned; + RemovePlayerAssignment(PlayerId); + return undefined; +} + +export function CleanupPlayerAssignment(PlayerId: string){ + RemovePlayerAssignment(PlayerId); +} + +export function CleanupStaleAssignments(Budget = 64){ + let Checked = 0; + for(const [PlayerId, Server] of PlayerServerAssignments){ + if(Checked++ >= Budget) + break; + if(!Gameservers.includes(Server) || Server.expectedShutdownReason != undefined) + RemovePlayerAssignment(PlayerId); + } +} + +// mm timing logging, no gameplay effect +export function GetRamsgatePrewarmStatus(){ + return { + ready: RamsgateServer != undefined, + restarting: RamsgateStartup != undefined, + port: RamsgateServer?.port + }; +} + +function FindHuntGameserversForPlayers(PlayerIds: string[]){ + const Players = new Set(PlayerIds); + return Gameservers.filter((Candidate) => + !Candidate.isRamsgate + && !Candidate.isTrainingDojo + && Candidate.expectedShutdownReason == undefined + && Candidate.expectedPlayers?.some((Player) => Players.has(Player.playerUid))); +} + +function DetachPlayersFromGameservers(Servers: Gameserver[], PlayerIds: string[]){ + const Players = new Set(PlayerIds); + + for(const Server of Servers){ + Server.expectedPlayers = Server.expectedPlayers?.filter((Player) => !Players.has(Player.playerUid)); + } +} + +function RetireReplacedGameservers(Servers: Gameserver[], PlayerIds: string[], Replacement: Gameserver){ + if(Servers.length === 0){ + return; + } + + DetachPlayersFromGameservers(Servers, PlayerIds); + logger.info({ + replacementServerId: Replacement.id, + replacementPort: Replacement.port, + playerIds: PlayerIds, + retiredServers: Servers.map((Server) => ({id: Server.id, port: Server.port, processId: Server.processId})) + }, "Retiring gameservers replaced by a fresh instance"); + + for(const Server of Servers){ + void ShutdownServer(Server, `replaced_by_fresh_instance:${Replacement.id}`).catch((Error) => { + logger.error(Error, `Failed to retire replaced gameserver ${Server.id} on port ${Server.port}`); + }); + } +} + +export async function GetGameserverStatusForPlayer(PlayerId: string){ + const Server = FindGameserverForPlayer(PlayerId); + + if(Server == undefined){ + return { + found: false, + joinable: false + }; + } + + const ProcessAlive = IsGameserverProcessAlive(Server); + const PortBound = await IsUdpPortInUse(Server.port); + const Joinable = ProcessAlive && PortBound && Server.expectedShutdownReason == undefined; + + return { + found: true, + joinable: Joinable, + server: { + host: MY_IP, + port: Server.port, + id: Server.id, + origin: Server.origin, + map: Server.map, + matchmakerHuntId: Server.matchmakerHuntId, + expectedPlayers: Server.expectedPlayers, + processAlive: ProcessAlive, + portBound: PortBound, + lastTouchedTime: Server.lastTouchedTime.toISOString() + } + }; +} + +export function TouchGameserverForPlayer(PlayerId: string){ + const Server = FindGameserverForPlayer(PlayerId); + + if(Server == undefined){ + return undefined; + } + + TouchGameserver(Server); + + return { + host: MY_IP, + port: Server.port, + id: Server.id, + origin: Server.origin, + lastTouchedTime: Server.lastTouchedTime.toISOString() + }; +} + +export async function GetOrStartTrainingDojoConnectionDetails( + Origin: GameserverOrigin, + ExpectedPlayers: string[] = [] +){ + if(TrainingDojoServer != undefined){ + AssignPlayersToServer(TrainingDojoServer, + TransformExpectedPlayers(undefined, ExpectedPlayers) ?? []); + return GetTrainingDojoConnectionDetails(); + } + + if(TrainingDojoStartup == undefined){ + TrainingDojoStartup = StartServer({ + map: TRAINING_DOJO_MAP_PATH, + behemoth: undefined, + matchmakerHuntId: undefined, + expectedPlayers: undefined, + isRamsgate: false, + isTrainingDojo: true, + origin: Origin, + trigger: "training_dojo_matchmaking", + shutdownAfterSeconds: TRAINING_DOJO_IDLE_SHUTDOWN_SECONDS + }).then((Server) => { + TrainingDojoServer = Server; + return Server; + }).finally(() => { + TrainingDojoStartup = undefined; + }); + } + + const Server = await TrainingDojoStartup; + TouchGameserver(Server); + AssignPlayersToServer(Server, + TransformExpectedPlayers(undefined, ExpectedPlayers) ?? []); + + return { + id: Server.id, + host: MY_IP, + port: Server.port + }; +} + +function TransformExpectedPlayers(HuntId: string | undefined, ExpectedPlayers: string[] | undefined){ + return ExpectedPlayers?.map((PlayerId) => ({ + playerUid: PlayerId, + playerHuntId: HuntId ?? "UNKNOWN_HUNT" + })); +} + +export async function StartupGameserverWithArgs(GameArgs: string, ExpectedPlayers: string[] | undefined){ const Map = GameArgs.split("?")[0]; const Behemoth = GameArgs.split("?")[2].split("=")[1]; + const ShutdownAfterSeconds = GetHuntIdleShutdownSeconds(undefined, undefined, GameArgs); - const GameServerToReturn = await StartServer(Map, Behemoth, undefined, undefined, false, false); + const GameServerToReturn = await StartServer({ + map: Map, + behemoth: Behemoth, + matchmakerHuntId: undefined, + expectedPlayers: TransformExpectedPlayers(undefined, ExpectedPlayers), + isRamsgate: false, + isTrainingDojo: false, + origin: "HUNT_ARGS", + trigger: GameArgs, + shutdownAfterSeconds: ShutdownAfterSeconds + }); + AssignPlayersToServer(GameServerToReturn, + TransformExpectedPlayers(undefined, ExpectedPlayers) ?? []); return { + id: GameServerToReturn.id, host: MY_IP, port: GameServerToReturn.port }; } function GetMatchmakerHuntIdFromPlayerHuntId(PlayerHuntId: string){ - const MatchmakerHuntIDs = (PlayerHuntTable[0].Rows as any)[PlayerHuntId].MatchmakerHuntIDs; + const MatchmakerHuntIDs = PLAYER_HUNT_ROWS[PlayerHuntId].MatchmakerHuntIDs; let MatchmakerHuntObject; @@ -177,13 +1651,13 @@ function GetMatchmakerHuntIdFromPlayerHuntId(PlayerHuntId: string){ } function GetBehemothPathFromMatchmakerHuntId(MatchmakerHuntId: string): string{ - const MatchmakerHuntObject = (MatchmakerHuntTable[0].Rows as any)[MatchmakerHuntId]; + const MatchmakerHuntObject = MATCHMAKER_HUNT_ROWS[MatchmakerHuntId]; return MatchmakerHuntObject.SpecificBehemoth.BehemothAsset.AssetPathName; } function GetMapPathFromMatchmakerHuntId(MatchmakerHuntId: string): string{ - const MatchmakerHuntObject = (MatchmakerHuntTable[0].Rows as any)[MatchmakerHuntId]; + const MatchmakerHuntObject = MATCHMAKER_HUNT_ROWS[MatchmakerHuntId]; const MapList = MatchmakerHuntObject.MapList; @@ -191,38 +1665,165 @@ function GetMapPathFromMatchmakerHuntId(MatchmakerHuntId: string): string{ } function GetGameModeOverrideFromMatchmakerHuntId(MatchmakerHuntId: string): string{ - const MatchmakerHuntObject = (MatchmakerHuntTable[0].Rows as any)[MatchmakerHuntId]; + const MatchmakerHuntObject = MATCHMAKER_HUNT_ROWS[MatchmakerHuntId]; return MatchmakerHuntObject.GameModeOverride.replaceAll("Archon/Content", "/Game"); } -type TrialsData = { +type TrialData = { Behemoth: string; TrialsHuntId: string; } -function RandomlyGenTrialsData(IsElite: boolean): TrialsData{ +const TRIAL_HUNT_ID_REGEX = /^Arena_MatchmakerHunt_(Hard|Elite)_(\d{3})$/; +const TRIAL_PLAYER_HUNT_ID_REGEX = /^(Trials_PlayerHunt_(Hard|Elite)_(\d{3}))(?:_(?:Day|Night))?$/; +const TRIALS_HARD_ROWS = TrialsHardHuntTable[0].Rows as any; +const TRIALS_ELITE_ROWS = TrialsEliteHuntTable[0].Rows as any; +const PLAYER_HUNT_ROWS = PlayerHuntTable[0].Rows as any; +const MATCHMAKER_HUNT_ROWS = MatchmakerHuntTable[0].Rows as any; +const TRIAL_ROWS = { + Hard: TRIALS_HARD_ROWS, + Elite: TRIALS_ELITE_ROWS +} as const; +let TrialByHuntId: Map | undefined; +let DisplayHuntByBehemoth: Map | undefined; + +function MakeTrial(TrialsHuntId: string, Row: any): TrialData{ + return { + Behemoth: Row.SpecificBehemoth.BehemothAsset.AssetPathName, + TrialsHuntId: TrialsHuntId + }; +} + +function GetTrialIndex(){ + if(TrialByHuntId != undefined){ + return TrialByHuntId; + } + + TrialByHuntId = new Map(); + + for(const [Difficulty, Rows] of Object.entries(TRIAL_ROWS)){ + for(const [TrialsHuntId, Row] of Object.entries(Rows)){ + const Match = TRIAL_HUNT_ID_REGEX.exec(TrialsHuntId); + + if(Match == undefined || Match[1] !== Difficulty){ + continue; + } + + const Data = MakeTrial(TrialsHuntId, Row); + TrialByHuntId.set(TrialsHuntId, Data); + TrialByHuntId.set(`Trials_PlayerHunt_${Difficulty}_${Match[2]}`, Data); + } + } + + TrialByHuntId.set("CR19_PlayerHunt_Arena_Hard", TrialByHuntId.get("Arena_MatchmakerHunt_Hard_001")!); + TrialByHuntId.set("CR19_PlayerHunt_Arena_Elite", TrialByHuntId.get("Arena_MatchmakerHunt_Elite_001")!); + + logger.info({ + huntIdCount: TrialByHuntId.size + }, "Indexed trials hunt ids"); + + return TrialByHuntId; +} + +function ResolveTrial(PlayerHuntId: string): TrialData | undefined{ + const AliasMatch = TRIAL_PLAYER_HUNT_ID_REGEX.exec(PlayerHuntId); + const BasePlayerHuntId = AliasMatch?.[1] ?? PlayerHuntId; + const Trial = GetTrialIndex().get(BasePlayerHuntId); + + if(Trial != undefined) + return Trial; + + return PlayerHuntId.includes("Arena") ? RandomlyGenTrialsData(PlayerHuntId.includes("Elite")) : undefined; +} + +function GetMatchmakerBehemothAssetPath(MatchmakerHuntId: string): string | undefined{ + const MatchmakerHuntObject = MATCHMAKER_HUNT_ROWS[MatchmakerHuntId]; + const BehemothAssetPath = MatchmakerHuntObject?.SpecificBehemoth?.BehemothAsset?.AssetPathName; + + return typeof BehemothAssetPath === "string" ? BehemothAssetPath : undefined; +} + +function GetPlayerHuntMatchmakerIds(PlayerHunt: any): string[]{ + const MatchmakerHuntIDs = PlayerHunt?.MatchmakerHuntIDs; + + if(!Array.isArray(MatchmakerHuntIDs)){ + return []; + } + + return MatchmakerHuntIDs + .map((MatchmakerHuntID: any) => MatchmakerHuntID?.RowName) + .filter((MatchmakerHuntId: any): MatchmakerHuntId is string => typeof MatchmakerHuntId === "string"); +} + +function GetDisplayHuntIndex(){ + if(DisplayHuntByBehemoth != undefined){ + return DisplayHuntByBehemoth; + } + + DisplayHuntByBehemoth = new Map(); + + for(const [PlayerHuntId, PlayerHunt] of Object.entries(PLAYER_HUNT_ROWS)){ + for(const MatchmakerHuntId of GetPlayerHuntMatchmakerIds(PlayerHunt)){ + const BehemothAssetPath = GetMatchmakerBehemothAssetPath(MatchmakerHuntId); + + if(BehemothAssetPath == undefined || BehemothAssetPath.trim().length === 0){ + continue; + } + + const ExistingPlayerHuntId = DisplayHuntByBehemoth.get(BehemothAssetPath); + const IsPreferredPursuit = PlayerHuntId.startsWith("CR19_PlayerHunt_Pursuit_"); + const HasPreferredPursuit = ExistingPlayerHuntId?.startsWith("CR19_PlayerHunt_Pursuit_") === true; + + if(ExistingPlayerHuntId == undefined || (IsPreferredPursuit && !HasPreferredPursuit)){ + DisplayHuntByBehemoth.set(BehemothAssetPath, PlayerHuntId); + } + } + } + + logger.info({ + behemothCount: DisplayHuntByBehemoth.size + }, "Indexed trial display player hunts"); + + return DisplayHuntByBehemoth; +} + +function GetDisplayHuntId(Trial: TrialData | undefined): string | undefined{ + if(Trial == undefined || Trial.Behemoth.trim().length === 0){ + return undefined; + } + + return GetDisplayHuntIndex().get(Trial.Behemoth); +} + +function GetExpectedHuntId(Trial: TrialData | undefined, FallbackPlayerHuntId: string){ + const DisplayPlayerHuntId = GetDisplayHuntId(Trial); + + if(Trial != undefined && DisplayPlayerHuntId == undefined){ + logger.warn(`No canonical display player hunt found for trial ${Trial.TrialsHuntId}; falling back to ${FallbackPlayerHuntId}`); + } + + return DisplayPlayerHuntId ?? FallbackPlayerHuntId; +} + +function RandomlyGenTrialsData(IsElite: boolean): TrialData{ const RandomTrialNum = String(crypto.randomInt(1, 89)).padStart(3, "0"); const Difficulty = IsElite ? "Elite" : "Hard"; const TrialsHuntId = `Arena_MatchmakerHunt_${Difficulty}_${RandomTrialNum}`; - const Row = IsElite ? (TrialsEliteHuntTable[0].Rows as any)[TrialsHuntId] : (TrialsHardHuntTable[0].Rows as any)[TrialsHuntId]; + const Row = IsElite ? TRIALS_ELITE_ROWS[TrialsHuntId] : TRIALS_HARD_ROWS[TrialsHuntId]; - const Behemoth = Row.SpecificBehemoth.BehemothAsset.AssetPathName; - - return { - Behemoth: Behemoth, - TrialsHuntId: TrialsHuntId - }; + return MakeTrial(TrialsHuntId, Row); } -export async function StartupGameserverWithHuntIdAndPlayers(HuntId: string, ExpectedPlayers: string[]){ - const TrialsData = HuntId.includes("Arena") ? RandomlyGenTrialsData(HuntId.includes("Elite")) : undefined; - const MatchmakerHuntId = TrialsData == undefined ? GetMatchmakerHuntIdFromPlayerHuntId(HuntId) : TrialsData.TrialsHuntId; - let BehemothPath = TrialsData == undefined ? GetBehemothPathFromMatchmakerHuntId(MatchmakerHuntId!) : TrialsData.Behemoth; - let MapPath = TrialsData == undefined ? GetMapPathFromMatchmakerHuntId(MatchmakerHuntId!) : TRIALS_MAP_PATH; +export async function StartupGameserverWithHuntIdAndPlayers(HuntId: string, ExpectedPlayers: string[], FreshInstance = false){ + const Trial = ResolveTrial(HuntId); + const MatchmakerHuntId = Trial == undefined ? GetMatchmakerHuntIdFromPlayerHuntId(HuntId) : Trial.TrialsHuntId; + let BehemothPath = Trial == undefined ? GetBehemothPathFromMatchmakerHuntId(MatchmakerHuntId!) : Trial.Behemoth; + let MapPath = Trial == undefined ? GetMapPathFromMatchmakerHuntId(MatchmakerHuntId!) : TRIALS_MAP_PATH; + const ExpectedPlayerHuntId = GetExpectedHuntId(Trial, HuntId); if(MatchmakerHuntId != undefined && !MatchmakerHuntId.includes("Arena")){ const OverrideGameMode = GetGameModeOverrideFromMatchmakerHuntId(MatchmakerHuntId); @@ -233,14 +1834,82 @@ export async function StartupGameserverWithHuntIdAndPlayers(HuntId: string, Expe } } - const GameServerToReturn = await StartServer(MapPath, BehemothPath, MatchmakerHuntId, ExpectedPlayers.map((PlayerId) => { - return { - playerUid: PlayerId, - playerHuntId: HuntId - }; - }), false, false); + logger.info({ + huntId: HuntId, + matchmakerHuntId: MatchmakerHuntId, + expectedPlayerHuntId: ExpectedPlayerHuntId, + behemoth: BehemothPath, + map: MapPath, + expectedPlayers: ExpectedPlayers, + freshInstance: FreshInstance + }, "Resolved hunt gameserver launch"); + + const StartOptions: StartServerOptions = { + map: MapPath, + behemoth: BehemothPath, + matchmakerHuntId: MatchmakerHuntId, + expectedPlayers: TransformExpectedPlayers(ExpectedPlayerHuntId, ExpectedPlayers), + isRamsgate: false, + isTrainingDojo: false, + origin: "HUNT_MATCHMAKER", + trigger: HuntId, + shutdownAfterSeconds: GetHuntIdleShutdownSeconds(HuntId, MatchmakerHuntId, MapPath) + }; + + const PreviousServers = FreshInstance ? FindHuntGameserversForPlayers(ExpectedPlayers) : []; + let RetiredBeforeStartup = false; + + const RetireBeforeStartup = async () => { + if(RetiredBeforeStartup || PreviousServers.length === 0){ + return; + } + + RetiredBeforeStartup = true; + logger.info({ + huntId: HuntId, + playerIds: ExpectedPlayers, + servers: PreviousServers.map((Server) => ({id: Server.id, port: Server.port, processId: Server.processId})) + }, "No free hunt port; retiring previous gameservers before fresh startup"); + await Promise.all(PreviousServers.map((Server) => ShutdownServer(Server, "fresh_instance_port_required"))); + }; + + if(FreshInstance && PreviousServers.length > 0 && FreePorts.length === 0){ + await RetireBeforeStartup(); + } + + let GameServerToReturn: Gameserver; + try{ + GameServerToReturn = await StartServer(StartOptions); + } + catch(Error){ + if(FreshInstance && PreviousServers.length > 0 && !RetiredBeforeStartup && Error instanceof NoFreeHuntPortsError){ + await RetireBeforeStartup(); + GameServerToReturn = await StartServer(StartOptions); + } + else{ + throw Error; + } + } + + if(FreshInstance && !RetiredBeforeStartup){ + RetireReplacedGameservers(PreviousServers, ExpectedPlayers, GameServerToReturn); + } + + if(FreshInstance){ + logger.info({ + huntId: HuntId, + playerIds: ExpectedPlayers, + serverId: GameServerToReturn.id, + port: GameServerToReturn.port, + replacementMode: RetiredBeforeStartup ? "old_first_no_free_port" : "new_first" + }, "Fresh gameserver instance ready"); + } + + AssignPlayersToServer(GameServerToReturn, + TransformExpectedPlayers(ExpectedPlayerHuntId, ExpectedPlayers) ?? []); return { + id: GameServerToReturn.id, host: MY_IP, port: GameServerToReturn.port } @@ -251,7 +1920,9 @@ export async function Startup(){ FreePorts.push(i); } - RamsgateServer = await StartServer(RAMSGATE_MAP_PATH, undefined, undefined, undefined, true, false); + logger.info("Ramsgate will start on the first CITY request"); - TrainingDojoServer = await StartServer(TRAINING_DOJO_MAP_PATH, undefined, undefined, undefined, false, true); -} \ No newline at end of file + if(PREWARM_TRAINING_DOJO){ + await GetOrStartTrainingDojoConnectionDetails("TRAINING_DOJO_PREWARM"); + } +} diff --git a/UndauntedDeployServer/src/controllers/matchmaker.ts b/UndauntedDeployServer/src/controllers/matchmaker.ts index 3d9244c..86c361b 100644 --- a/UndauntedDeployServer/src/controllers/matchmaker.ts +++ b/UndauntedDeployServer/src/controllers/matchmaker.ts @@ -1,30 +1,38 @@ import { logger } from "../logger"; -import { GetRamsgateConnectionDetails, GetTrainingDojoConnectionDetails, StartupGameserverWithArgs, StartupGameserverWithHuntIdAndPlayers } from "./gameservers"; +import { GetOrStartTrainingDojoConnectionDetails, GetRamsgateConnectionDetails, StartupGameserverWithArgs, StartupGameserverWithHuntIdAndPlayers } from "./gameservers"; -export async function HandleMatchmakingRequest(GameMode: string, GameArgs: string, HuntId: string, ExpectedPlayers: string[] | undefined){ - logger.info(`Handling matchmaking with GameMode: ${GameMode} HuntId: ${HuntId} and GameArgs: ${GameArgs} and ExpectedPlayers ${ExpectedPlayers}`); +export async function HandleMatchmakingRequest(GameMode: string, GameArgs: string, HuntId: string, ExpectedPlayers: string[] | undefined, FreshInstance = false){ + const NormalizedHuntId = HuntId ?? ""; + const NormalizedExpectedPlayers = ExpectedPlayers ?? []; + + logger.info({ + gameMode: GameMode, + huntId: NormalizedHuntId, + gameArgs: GameArgs, + expectedPlayers: NormalizedExpectedPlayers, + freshInstance: FreshInstance + }, "Handling matchmaking"); if(GameMode === "CITY"){ - return GetRamsgateConnectionDetails(); + return await GetRamsgateConnectionDetails(NormalizedExpectedPlayers); } else if(GameMode === "SHARED"){ - if (HuntId != undefined && HuntId.trim().length > 0){ - if(HuntId == "ShatteredIsles_TrainingDojo"){ - return GetTrainingDojoConnectionDetails(); - } + if(NormalizedHuntId === "ShatteredIsles_TrainingDojo"){ + return await GetOrStartTrainingDojoConnectionDetails( + "TRAINING_DOJO_LAZY", NormalizedExpectedPlayers); } } else if(GameMode === "ISLAND"){ if(GameArgs != undefined && GameArgs.trim().length > 0){ - return await StartupGameserverWithArgs(GameArgs); + return await StartupGameserverWithArgs(GameArgs, ExpectedPlayers); } - if(HuntId != undefined && HuntId.trim().length > 0 && ExpectedPlayers != undefined){ - return await StartupGameserverWithHuntIdAndPlayers(HuntId, ExpectedPlayers!); + if(NormalizedHuntId.length > 0 && ExpectedPlayers != undefined){ + return await StartupGameserverWithHuntIdAndPlayers(NormalizedHuntId, ExpectedPlayers, FreshInstance); } } logger.error("Matchmaking failed, sending you to Ramsgate!"); - return GetRamsgateConnectionDetails(); -} \ No newline at end of file + return await GetRamsgateConnectionDetails(NormalizedExpectedPlayers); +} diff --git a/UndauntedDeployServer/src/controllers/watchdog.ts b/UndauntedDeployServer/src/controllers/watchdog.ts index bded83a..92f54e3 100644 --- a/UndauntedDeployServer/src/controllers/watchdog.ts +++ b/UndauntedDeployServer/src/controllers/watchdog.ts @@ -1,6 +1,6 @@ import { kill } from "node:process"; import { logger } from "../logger"; -import { Gameserver, Gameservers, CleanupServer } from "./gameservers"; +import { Gameserver, Gameservers, CleanupServer, CleanupStaleAssignments, GetGameserverIdleShutdownReason, ShutdownIdleGameserver, ShutdownIdleRamsgate } from "./gameservers"; /** * TODO: @@ -18,13 +18,29 @@ function IsGameserverStillAlive(GameserverToCheck: Gameserver){ } export async function RunWatchdog(){ - logger.info(`Running Gameserver Watchdog!`); + // logger.info(`Running Gameserver Watchdog!`); - for(const Gameserver of Gameservers){ + CleanupStaleAssignments(); + for(const Gameserver of [...Gameservers]){ if(!IsGameserverStillAlive(Gameserver)){ - console.log(`Cleaning up Gameserver on port ${Gameserver.port}`); + logger.info(`Cleaning up dead gameserver on port ${Gameserver.port}`); - CleanupServer(Gameserver); + await CleanupServer(Gameserver); + + continue; + } + + if(Gameserver.isRamsgate){ + const IdleDecision = GetGameserverIdleShutdownReason(Gameserver); + if(IdleDecision != undefined){ + await ShutdownIdleRamsgate(Gameserver, IdleDecision); + } + continue; + } + + const IdleDecision = GetGameserverIdleShutdownReason(Gameserver); + if(IdleDecision != undefined){ + await ShutdownIdleGameserver(Gameserver, IdleDecision); } } -} \ No newline at end of file +} diff --git a/UndauntedDeployServer/src/logger.ts b/UndauntedDeployServer/src/logger.ts index 435824d..9b160ca 100644 --- a/UndauntedDeployServer/src/logger.ts +++ b/UndauntedDeployServer/src/logger.ts @@ -1,17 +1,120 @@ -import pino from "pino"; - -export const logger = pino({ - level: process.env.LOG_LEVEL || "info", - - transport: - process.env.NODE_ENV !== "production" - ? { - target: "pino-pretty", - options: { - colorize: true, - translateTime: "HH:MM:ss", - ignore: "pid,hostname" - }, - } - : undefined, -}); \ No newline at end of file +import { + closeSync, + mkdirSync, + openSync, + writeSync +} from "node:fs"; +import { resolve } from "node:path"; +import { Writable } from "node:stream"; + +import pino, { type DestinationStream, type Logger, type StreamEntry } from "pino"; + +const DEFAULT_MAX_BYTES = 64 * 1024 * 1024; +const MINIMUM_MAX_BYTES = 1024 * 1024; +const MAXIMUM_MAX_BYTES = 1024 * 1024 * 1024; + +class BoundedFileDestination extends Writable { + readonly path: string; + private readonly descriptor: number; + private bytesWritten = 0; + private destinationClosed = false; + + constructor(directory: string, maximumBytes: number){ + super(); + mkdirSync(directory, {recursive: true}); + this.path = resolve(directory, "deployserver.log"); + this.descriptor = openSync(this.path, "w"); + this.maximumBytes = maximumBytes; + } + + private readonly maximumBytes: number; + + override _write(chunk: Buffer | string, encoding: BufferEncoding, callback: (error?: Error | null) => void){ + try{ + const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding); + if(this.bytesWritten + buffer.byteLength <= this.maximumBytes){ + this.bytesWritten += writeSync(this.descriptor, buffer); + } + callback(); + } + catch(error){ + callback(error as Error); + } + } + + close(){ + if(this.destinationClosed){ + return; + } + + this.destinationClosed = true; + closeSync(this.descriptor); + } +} + +function ParseMaximumBytes(value: string | undefined){ + const parsed = Number(value); + if(!Number.isSafeInteger(parsed) || parsed < MINIMUM_MAX_BYTES || parsed > MAXIMUM_MAX_BYTES){ + return DEFAULT_MAX_BYTES; + } + + return parsed; +} + +export type LoggingInstance = { + logger: Logger, + fileDiagnosticsEnabled: boolean, + diagnosticsPath: string | undefined, + close: () => void +}; + +export function CreateLogging( + environment: NodeJS.ProcessEnv = process.env, + consoleStream: DestinationStream = pino.destination(1) +): LoggingInstance { + const logLevel = environment.LOG_LEVEL || "info"; + const fileDiagnosticsRequested = environment.DEPLOYSERVER_FILE_DIAGNOSTICS === "true"; + const diagnosticsDirectory = environment.DEPLOYSERVER_DIAGNOSTICS_DIRECTORY?.trim(); + let fileDestination: BoundedFileDestination | undefined; + + if(fileDiagnosticsRequested && diagnosticsDirectory){ + try{ + fileDestination = new BoundedFileDestination( + resolve(diagnosticsDirectory), + ParseMaximumBytes(environment.DEPLOYSERVER_DIAGNOSTICS_MAX_BYTES) + ); + } + catch{ + // Diagnostics must fail closed: console logging remains available, but a + // bad optional output path must never prevent the server from starting. + fileDestination = undefined; + } + } + + const streams: StreamEntry[] = [{stream: consoleStream}]; + if(fileDestination){ + streams.push({stream: fileDestination}); + } + + const instance = pino({level: logLevel}, pino.multistream(streams)); + let closed = false; + + return { + logger: instance, + fileDiagnosticsEnabled: fileDestination != undefined, + diagnosticsPath: fileDestination?.path, + close: () => { + if(closed){ + return; + } + closed = true; + instance.flush(); + fileDestination?.close(); + } + }; +} + +export const logging = CreateLogging(); +export const logger = logging.logger; + +process.once("beforeExit", logging.close); diff --git a/UndauntedDeployServer/src/performance.ts b/UndauntedDeployServer/src/performance.ts new file mode 100644 index 0000000..dc6cc07 --- /dev/null +++ b/UndauntedDeployServer/src/performance.ts @@ -0,0 +1,225 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { monitorEventLoopDelay } from "node:perf_hooks"; + +import { Gameservers } from "./controllers/gameservers"; +import { logger } from "./logger"; + +const ExecFile = promisify(execFile); +const MINIMUM_INTERVAL_SECONDS = 10; +const POWERSHELL_TIMEOUT_MS = 10_000; + +type ProcessSnapshot = { + Id: number, + CPU: number | null, + WorkingSet64: number, + PrivateMemorySize64: number, + HandleCount: number, + ThreadCount: number +}; + +type PreviousCpuSnapshot = { + cpuSeconds: number, + sampledAtMs: number +}; + +const PreviousCpuByPid = new Map(); + +function ParseEnabled(Value: string | undefined, Default: boolean){ + if(Value == undefined || Value.trim().length === 0){ + return Default; + } + + return Value.toLowerCase() === "true"; +} + +function ParseIntervalSeconds(Name: string, Default: number){ + const Value = Number(process.env[Name]); + + if(!Number.isFinite(Value) || Value < MINIMUM_INTERVAL_SECONDS){ + return Default; + } + + return Value; +} + +function NormalizeSnapshots(Value: unknown): ProcessSnapshot[]{ + if(Value == undefined){ + return []; + } + + const Candidates = Array.isArray(Value) ? Value : [Value]; + + return Candidates.filter((Candidate): Candidate is ProcessSnapshot => { + if(typeof Candidate !== "object" || Candidate == undefined){ + return false; + } + + const Snapshot = Candidate as Partial; + return typeof Snapshot.Id === "number" + && (typeof Snapshot.CPU === "number" || Snapshot.CPU === null) + && typeof Snapshot.WorkingSet64 === "number" + && typeof Snapshot.PrivateMemorySize64 === "number" + && typeof Snapshot.HandleCount === "number" + && typeof Snapshot.ThreadCount === "number"; + }); +} + +async function ReadProcessSnapshots(ProcessIds: number[]){ + if(ProcessIds.length === 0){ + return []; + } + + const IdList = ProcessIds.join(","); + const Command = `$processes = Get-Process -Id ${IdList} -ErrorAction SilentlyContinue; ` + + `$processes | Select-Object Id,CPU,WorkingSet64,PrivateMemorySize64,HandleCount,@{Name='ThreadCount';Expression={$_.Threads.Count}} | ConvertTo-Json -Compress`; + const {stdout} = await ExecFile("powershell.exe", [ + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-Command", + Command + ], { + windowsHide: true, + timeout: POWERSHELL_TIMEOUT_MS, + maxBuffer: 1024 * 1024 + }); + + const Output = stdout.trim(); + return Output.length === 0 ? [] : NormalizeSnapshots(JSON.parse(Output)); +} + +function CalculateCpuPercent(Snapshot: ProcessSnapshot, SampledAtMs: number){ + if(Snapshot.CPU == undefined){ + return undefined; + } + + const Previous = PreviousCpuByPid.get(Snapshot.Id); + PreviousCpuByPid.set(Snapshot.Id, { + cpuSeconds: Snapshot.CPU, + sampledAtMs: SampledAtMs + }); + + if(Previous == undefined){ + return undefined; + } + + const ElapsedSeconds = (SampledAtMs - Previous.sampledAtMs) / 1000; + const CpuSeconds = Snapshot.CPU - Previous.cpuSeconds; + + if(ElapsedSeconds <= 0 || CpuSeconds < 0){ + return undefined; + } + + return Math.round((CpuSeconds / ElapsedSeconds) * 10_000) / 100; +} + +let GameserverSampleInProgress = false; + +async function SampleGameservers(){ + if(GameserverSampleInProgress){ + logger.warn("Skipping overlapping gameserver performance sample"); + return; + } + + GameserverSampleInProgress = true; + + try{ + const TrackedServers = [...Gameservers]; + const TrackedPids = new Set(TrackedServers.map((Server) => Server.processId)); + + for(const ProcessId of PreviousCpuByPid.keys()){ + if(!TrackedPids.has(ProcessId)){ + PreviousCpuByPid.delete(ProcessId); + } + } + + const SampledAtMs = Date.now(); + const Snapshots = await ReadProcessSnapshots([...TrackedPids]); + const SnapshotByPid = new Map(Snapshots.map((Snapshot) => [Snapshot.Id, Snapshot])); + + for(const Server of TrackedServers){ + const Snapshot = SnapshotByPid.get(Server.processId); + + if(Snapshot == undefined){ + logger.debug({ + event: "gameserver-performance-unavailable", + serverId: Server.id, + processId: Server.processId, + port: Server.port + }, "Tracked gameserver process was unavailable during performance sampling"); + continue; + } + + logger.info({ + event: "gameserver-performance", + serverId: Server.id, + processId: Server.processId, + port: Server.port, + map: Server.map, + origin: Server.origin, + runtimeSeconds: Math.max(0, Math.round((SampledAtMs - Server.startTime.getTime()) / 1000)), + expectedPlayerCount: Server.expectedPlayers?.length ?? 0, + cpuPercentOfOneCore: CalculateCpuPercent(Snapshot, SampledAtMs), + cumulativeCpuSeconds: Snapshot.CPU, + workingSetBytes: Snapshot.WorkingSet64, + privateMemoryBytes: Snapshot.PrivateMemorySize64, + handleCount: Snapshot.HandleCount, + threadCount: Snapshot.ThreadCount + }, "Gameserver performance sample"); + } + } + catch(Error){ + logger.warn({err: Error, event: "gameserver-performance-sample-failed"}, "Failed to sample gameserver performance"); + } + finally{ + GameserverSampleInProgress = false; + } +} + +export function StartPerformanceMonitoring(){ + const GameserverProfilingEnabled = ParseEnabled(process.env.GAMESERVER_PROFILING, false); + const DeployServerProfilingEnabled = ParseEnabled(process.env.DEPLOYSERVER_PROFILING, false); + const GameserverIntervalSeconds = ParseIntervalSeconds("GAMESERVER_PROFILE_INTERVAL_SECONDS", 30); + const DeployServerIntervalSeconds = ParseIntervalSeconds("DEPLOYSERVER_PROFILE_INTERVAL_SECONDS", 60); + + if(DeployServerProfilingEnabled){ + const EventLoopDelay = monitorEventLoopDelay({resolution: 20}); + EventLoopDelay.enable(); + + const DeployServerTimer = setInterval(() => { + const Memory = process.memoryUsage(); + + logger.info({ + event: "deployserver-performance", + processId: process.pid, + uptimeSeconds: Math.round(process.uptime()), + residentSetBytes: Memory.rss, + heapUsedBytes: Memory.heapUsed, + heapTotalBytes: Memory.heapTotal, + externalBytes: Memory.external, + eventLoopDelayMeanMs: Math.round(EventLoopDelay.mean / 1e4) / 100, + eventLoopDelayP99Ms: Math.round(EventLoopDelay.percentile(99) / 1e4) / 100, + eventLoopDelayMaxMs: Math.round(EventLoopDelay.max / 1e4) / 100 + }, "DeployServer performance sample"); + + EventLoopDelay.reset(); + }, DeployServerIntervalSeconds * 1000); + DeployServerTimer.unref(); + } + + if(GameserverProfilingEnabled){ + void SampleGameservers(); + const GameserverTimer = setInterval(() => void SampleGameservers(), GameserverIntervalSeconds * 1000); + GameserverTimer.unref(); + } + + logger.info({ + event: "performance-monitoring-started", + deployServerProfilingEnabled: DeployServerProfilingEnabled, + gameserverProfilingEnabled: GameserverProfilingEnabled, + gameserverIntervalSeconds: GameserverIntervalSeconds, + deployServerIntervalSeconds: DeployServerIntervalSeconds, + gameserverMeasurement: "external-process-only" + }, "Performance monitoring configured"); +} diff --git a/UndauntedDeployServer/src/routes/gameservers.ts b/UndauntedDeployServer/src/routes/gameservers.ts new file mode 100644 index 0000000..3445d47 --- /dev/null +++ b/UndauntedDeployServer/src/routes/gameservers.ts @@ -0,0 +1,19 @@ +import { Router } from "express"; +import { HandleGameserverHeartbeat, HandleGameserverReadyCallback } from "../controllers/gameservers"; + +export const gameserverRouter = Router(); + +gameserverRouter.post("/ready", (req, res) => { + const Result = HandleGameserverReadyCallback(req.header("x-undaunted-ready-token"), req.body); + + res.status(Result.status); + res.json(Result.body); +}); + +gameserverRouter.post("/heartbeat", (req, res) => { + const Result = HandleGameserverHeartbeat( + req.header("x-undaunted-lifecycle-token"), req.body); + + res.status(Result.status); + res.json(Result.body); +}); diff --git a/UndauntedDeployServer/src/routes/matchmaker.ts b/UndauntedDeployServer/src/routes/matchmaker.ts index 1927cc6..1ef5439 100644 --- a/UndauntedDeployServer/src/routes/matchmaker.ts +++ b/UndauntedDeployServer/src/routes/matchmaker.ts @@ -1,18 +1,81 @@ import { Router } from "express"; import { logger } from "../logger"; import { HandleMatchmakingRequest } from "../controllers/matchmaker"; +import { GetGameserverStatusForPlayer, GetRamsgatePrewarmStatus, TouchGameserverForPlayer } from "../controllers/gameservers"; import express from "express"; export const matchmakingRouter = Router(); matchmakingRouter.post("/handle-matchmaking-for-player", express.json(), async (req, res) => { + const StartedAt = performance.now(); const GameMode = req.body.GameMode; const GameArgs = req.body.GameArgs; const HuntId = req.body.HuntId; const ExpectedPlayers = req.body.ExpectedPlayers; + const FreshInstance = req.body.FreshInstance === true; - const MatchmakingResult = await HandleMatchmakingRequest(GameMode, GameArgs, HuntId, ExpectedPlayers); + try{ + const MatchmakingResult = await HandleMatchmakingRequest(GameMode, GameArgs, HuntId, ExpectedPlayers, FreshInstance); + const DurationMs = Math.round(performance.now() - StartedAt); + + logger.info({ + gameMode: GameMode, + huntId: HuntId ?? "", + durationMs: DurationMs, + ramsgatePrewarm: GameMode === "CITY" ? GetRamsgatePrewarmStatus() : undefined + }, "Matchmaking response ready"); + + res.status(200); + res.json(MatchmakingResult); + } + catch(Error){ + const DurationMs = Math.round(performance.now() - StartedAt); + logger.error(Error, `Failed to handle matchmaking for GameMode ${GameMode} HuntId ${HuntId}`); + logger.info({ gameMode: GameMode, huntId: HuntId ?? "", durationMs: DurationMs }, "Matchmaking response failed"); + + res.status(503); + res.json({ + error: "gameserver_startup_failed" + }); + } +}); + +matchmakingRouter.post("/touch-player", express.json(), (req, res) => { + const PlayerId = req.body.PlayerId; + + if(typeof PlayerId !== "string" || PlayerId.length === 0){ + res.status(400); + res.json({touched: false}); + return; + } + + const TouchedServer = TouchGameserverForPlayer(PlayerId); + + if(TouchedServer == undefined){ + res.status(404); + res.json({touched: false}); + return; + } + + res.status(200); + res.json({ + touched: true, + server: TouchedServer + }); +}); + +matchmakingRouter.post("/player-server-status", express.json(), async (req, res) => { + const PlayerId = req.body.PlayerId; + + if(typeof PlayerId !== "string" || PlayerId.length === 0){ + res.status(400); + res.json({ + found: false, + joinable: false + }); + return; + } res.status(200); - res.json(MatchmakingResult); -}); \ No newline at end of file + res.json(await GetGameserverStatusForPlayer(PlayerId)); +}); diff --git a/UndauntedDeployServer/src/selftests/diagnostics.ts b/UndauntedDeployServer/src/selftests/diagnostics.ts new file mode 100644 index 0000000..e5ae6ed --- /dev/null +++ b/UndauntedDeployServer/src/selftests/diagnostics.ts @@ -0,0 +1,48 @@ +import assert from "node:assert/strict"; +import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs"; +import { resolve } from "node:path"; +import { PassThrough } from "node:stream"; + +import { CreateLogging } from "../logger"; + +const root = resolve(process.cwd(), `.diagnostics-selftest-${process.pid}`); +const disabledDirectory = resolve(root, "disabled"); +const enabledDirectory = resolve(root, "enabled"); + +rmSync(root, {recursive: true, force: true}); +mkdirSync(root, {recursive: true}); + +try{ + const disabledConsole = new PassThrough(); + let disabledConsoleOutput = ""; + disabledConsole.on("data", (chunk: Buffer) => disabledConsoleOutput += chunk.toString()); + const disabled = CreateLogging({ + NODE_ENV: "production", + DEPLOYSERVER_DIAGNOSTICS_DIRECTORY: disabledDirectory + }, disabledConsole); + + disabled.logger.info({event: "disabled-probe"}, "disabled probe"); + disabled.close(); + assert.equal(disabled.fileDiagnosticsEnabled, false); + assert.equal(existsSync(disabledDirectory), false); + assert.match(disabledConsoleOutput, /disabled-probe/); + + const enabledConsole = new PassThrough(); + const enabled = CreateLogging({ + NODE_ENV: "production", + DEPLOYSERVER_FILE_DIAGNOSTICS: "true", + DEPLOYSERVER_DIAGNOSTICS_DIRECTORY: enabledDirectory, + DEPLOYSERVER_DIAGNOSTICS_MAX_BYTES: "1048576" + }, enabledConsole); + + enabled.logger.info({event: "shutdown-flush-probe"}, "shutdown flush probe"); + enabled.close(); + assert.equal(enabled.fileDiagnosticsEnabled, true); + assert.ok(enabled.diagnosticsPath); + assert.match(readFileSync(enabled.diagnosticsPath!, "utf8"), /shutdown-flush-probe/); + + console.log("Diagnostics policy self-test passed"); +} +finally{ + rmSync(root, {recursive: true, force: true}); +} diff --git a/UndauntedDeployServer/src/server.ts b/UndauntedDeployServer/src/server.ts index 00357c8..af17c0c 100644 --- a/UndauntedDeployServer/src/server.ts +++ b/UndauntedDeployServer/src/server.ts @@ -2,14 +2,22 @@ import { app } from "./app"; import { Startup } from "./controllers/gameservers"; import { RunWatchdog } from "./controllers/watchdog"; import { logger } from "./logger"; +import { StartPerformanceMonitoring } from "./performance"; const PORT = process.env.PORT; -app.listen(PORT, () => { - Startup(); +app.listen(PORT, async () => { + try{ + StartPerformanceMonitoring(); + await Startup(); - setInterval(RunWatchdog, 60 * 1000); + setInterval(RunWatchdog, 5 * 1000); - logger.info(`Undaunted DeployServer on port ${PORT}`); - logger.info(`Clear Skies, Slayer.`); -}); \ No newline at end of file + logger.info(`Undaunted DeployServer on port ${PORT}`); + logger.info(`Clear Skies, Slayer.`); + } + catch(Error){ + logger.fatal(Error, "Undaunted DeployServer startup failed"); + process.exitCode = 1; + } +}); diff --git a/UndauntedDeployServer/tsconfig.json b/UndauntedDeployServer/tsconfig.json index 814fd9e..6fd64a1 100644 --- a/UndauntedDeployServer/tsconfig.json +++ b/UndauntedDeployServer/tsconfig.json @@ -9,7 +9,9 @@ "strict": true, "esModuleInterop": true, "skipLibCheck": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "incremental": true, + "tsBuildInfoFile": ".tsbuildinfo" }, "include": ["src/**/*.ts"] -} \ No newline at end of file +} diff --git a/UndauntedInternalServer/AssetOptimization.cpp b/UndauntedInternalServer/AssetOptimization.cpp new file mode 100644 index 0000000..3b89b54 --- /dev/null +++ b/UndauntedInternalServer/AssetOptimization.cpp @@ -0,0 +1,527 @@ +#include "AssetOptimization.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "SDK.hpp" + +#pragma comment(lib, "psapi.lib") + +using namespace SDK; + +namespace AssetOptimization { + namespace { + constexpr uint32_t PublicKeepFlags = static_cast(EObjectFlags::Standalone) | + static_cast(EObjectFlags::MarkAsRootSet); + constexpr uint32_t InternalRootSet = 0x40000000u; + constexpr uintptr_t CollectGarbageRva = 0x01ED4750; + constexpr uintptr_t InitialLoadFlagRva = 0x0597166C; + constexpr uint8_t ExpectedCollectGarbagePrologue[] = { + 0x48, 0x89, 0x5C, 0x24, 0x08, 0x57, 0x48, 0x83, 0xEC, 0x20, + 0x0F, 0xB6, 0xDA, 0x8B, 0xF9 + }; + + struct ProcessMemory { + uint64_t workingSet = 0; + uint64_t privateBytes = 0; + }; + + enum class Outcome { + Retained, + Collected, + DestroyPending, + SlotReused, + Aborted + }; + + struct Candidate { + int32_t index = -1; + UObject* object = nullptr; + int32_t serialNumber = 0; + uint32_t publicFlags = 0; + uint32_t internalFlags = 0; + std::string fullName; + std::string className; + std::string packageName; + Outcome outcome = Outcome::Retained; + std::string reason = "report_only"; + }; + + struct PackageSelection { + std::unordered_set protectedPackages; + std::unordered_set inactiveMapPackages; + }; + + ProcessMemory ReadProcessMemory() { + PROCESS_MEMORY_COUNTERS_EX Counters{}; + Counters.cb = sizeof(Counters); + if (!GetProcessMemoryInfo(GetCurrentProcess(), + reinterpret_cast(&Counters), sizeof(Counters))) { + return {}; + } + return { static_cast(Counters.WorkingSetSize), static_cast(Counters.PrivateUsage) }; + } + + std::string JsonEscape(const std::string& Value) { + std::string Result; + Result.reserve(Value.size()); + for (const unsigned char Character : Value) { + switch (Character) { + case '\\': Result += "\\\\"; break; + case '"': Result += "\\\""; break; + case '\n': Result += "\\n"; break; + case '\r': Result += "\\r"; break; + case '\t': Result += "\\t"; break; + default: if (Character >= 0x20) Result += static_cast(Character); break; + } + } + return Result; + } + + std::string BytesToHex(const uint8_t* Bytes, size_t Length) { + std::ostringstream Output; + Output << std::uppercase << std::hex << std::setfill('0'); + for (size_t Index = 0; Index < Length; ++Index) { + if (Index != 0) Output << ' '; + Output << std::setw(2) << static_cast(Bytes[Index]); + } + return Output.str(); + } + + std::string UtcTimestamp(bool FilenameSafe) { + SYSTEMTIME Time{}; + GetSystemTime(&Time); + char Buffer[40]{}; + if (FilenameSafe) { + sprintf_s(Buffer, "%04u%02u%02uT%02u%02u%02u%03uZ", Time.wYear, Time.wMonth, Time.wDay, + Time.wHour, Time.wMinute, Time.wSecond, Time.wMilliseconds); + } + else { + sprintf_s(Buffer, "%04u-%02u-%02uT%02u:%02u:%02u.%03uZ", Time.wYear, Time.wMonth, Time.wDay, + Time.wHour, Time.wMinute, Time.wSecond, Time.wMilliseconds); + } + return Buffer; + } + + std::filesystem::path ExecutableDirectory() { + std::wstring Buffer(32768, L'\0'); + const DWORD Length = GetModuleFileNameW(nullptr, Buffer.data(), static_cast(Buffer.size())); + if (Length == 0 || Length >= Buffer.size()) return std::filesystem::current_path(); + Buffer.resize(Length); + return std::filesystem::path(Buffer).parent_path(); + } + + std::string GetOutermostName(UObject* Object) { + UObject* Outermost = Object; + while (Outermost && Outermost->Outer) Outermost = Outermost->Outer; + return Outermost ? Outermost->GetName() : std::string(); + } + + void ProtectObjectPackage(UObject* Object, PackageSelection& Selection) { + if (!Object) return; + const std::string PackageName = GetOutermostName(Object); + if (!PackageName.empty()) Selection.protectedPackages.insert(PackageName); + } + + bool IsApexDestructionObject(UObject* Object) { + return Object && (Object->IsA(ADestructibleActor::StaticClass()) || + Object->IsA(UDestructibleComponent::StaticClass()) || + Object->IsA(UDestructibleMesh::StaticClass()) || + Object->IsA(UDestructibleFractureSettings::StaticClass())); + } + + PackageSelection FindPackages(const std::string& MapContext) { + PackageSelection Selection; + UWorld* World = UWorld::GetWorld(); + if (World) { + ProtectObjectPackage(World, Selection); + ProtectObjectPackage(World->PersistentLevel, Selection); + ProtectObjectPackage(World->CurrentLevelPendingVisibility, Selection); + ProtectObjectPackage(World->CurrentLevelPendingInvisibility, Selection); + for (ULevel* Level : World->Levels) ProtectObjectPackage(Level, Selection); + for (ULevelStreaming* Streaming : World->StreamingLevels) { + ProtectObjectPackage(Streaming, Selection); + if (Streaming) { + ProtectObjectPackage(Streaming->LoadedLevel, Selection); + ProtectObjectPackage(Streaming->PendingUnloadLevel, Selection); + } + } + } + + // APEX destructible actors retain native asset/actor state outside UE's + // reflected reference graph. Clearing the keep flags from one of their + // packages can let GC release the native asset before a later actor + // teardown, which crashes Apex_Destructible while reading its asset + // table. Keep every package participating in a destructible lifetime. + for (int32_t Index = 0; Index < UObject::GObjects->Num(); ++Index) { + UObject* Object = UObject::GObjects->GetByIndex(Index); + if (IsApexDestructionObject(Object)) ProtectObjectPackage(Object, Selection); + } + + for (int32_t Index = 0; Index < UObject::GObjects->Num(); ++Index) { + UObject* Object = UObject::GObjects->GetByIndex(Index); + if (!Object || Object->IsDefaultObject() || + (!Object->IsA(UWorld::StaticClass()) && !Object->IsA(ULevel::StaticClass()))) { + continue; + } + const std::string PackageName = GetOutermostName(Object); + if (PackageName.empty() || PackageName == "Engine" || PackageName == "CoreUObject" || PackageName == "Transient") { + continue; + } + if (!Selection.protectedPackages.contains(PackageName)) Selection.inactiveMapPackages.insert(PackageName); + } + + // The configured map may be represented by a package object before every level pointer settles. + if (!MapContext.empty()) { + for (auto It = Selection.inactiveMapPackages.begin(); It != Selection.inactiveMapPackages.end();) { + if (MapContext.find(*It) != std::string::npos || It->find(MapContext) != std::string::npos) { + Selection.protectedPackages.insert(*It); + It = Selection.inactiveMapPackages.erase(It); + } + else { + ++It; + } + } + } + return Selection; + } + + uint64_t CountLiveObjects() { + uint64_t Count = 0; + for (int32_t Index = 0; Index < UObject::GObjects->Num(); ++Index) { + if (UObject::GObjects->GetByIndex(Index)) ++Count; + } + return Count; + } + + bool VerifyCollectGarbageSignature(Metrics& Result) { + const auto Address = reinterpret_cast(GetModuleHandleW(nullptr)) + CollectGarbageRva; + Result.expectedGcSignature = BytesToHex(ExpectedCollectGarbagePrologue, std::size(ExpectedCollectGarbagePrologue)); + Result.observedGcSignature = BytesToHex(Address, std::size(ExpectedCollectGarbagePrologue)); + Result.signatureValid = std::equal(std::begin(ExpectedCollectGarbagePrologue), + std::end(ExpectedCollectGarbagePrologue), Address); + return Result.signatureValid; + } + + bool IsDestroying(UObject* Object) { + return Object && ((Object->Flags & EObjectFlags::BeginDestroyed) || + (Object->Flags & EObjectFlags::FinishDestroyed)); + } + + bool IsSameCandidateObject(const FUObjectItem* CurrentItem, const Candidate& Item) { + if (!CurrentItem || CurrentItem->Object != Item.object) + return false; + + // A zero serial means this object had not needed a weak identity yet. + // UE may assign one during a GC callback without replacing the object. + // Pointer plus object-array slot still identify that survivor; once a + // serial existed, require it exactly so slot reuse cannot be mistaken + // for the original object. + return Item.serialNumber == 0 || + CurrentItem->SerialNumber == Item.serialNumber; + } + + void RestoreSurvivingFlags(Candidate& Item) { + FUObjectItem* CurrentItem = UObject::GObjects->GetItemByIndex(Item.index); + if (!IsSameCandidateObject(CurrentItem, Item) || IsDestroying(Item.object)) { + return; + } + + // GC may legitimately change reachability, destruction, cluster, + // and async bookkeeping. Restore only the keep bits deliberately + // cleared by this optimization. + const uint32_t CurrentPublic = static_cast(Item.object->Flags); + Item.object->Flags = static_cast( + CurrentPublic | (Item.publicFlags & PublicKeepFlags)); + const uint32_t CurrentInternal = static_cast(CurrentItem->Flags); + CurrentItem->Flags = static_cast( + CurrentInternal | (Item.internalFlags & InternalRootSet)); + } + + void RestoreAfterFailure(std::vector& Candidates, Metrics& Result, const std::string& Reason) { + Result.collected = 0; + Result.retained = 0; + for (Candidate& Item : Candidates) { + FUObjectItem* CurrentItem = UObject::GObjects->GetItemByIndex(Item.index); + if (!IsSameCandidateObject(CurrentItem, Item) || IsDestroying(Item.object)) { + Item.outcome = Outcome::Collected; + Item.reason = "not_surviving_after_" + Reason; + ++Result.collected; + continue; + } + RestoreSurvivingFlags(Item); + Item.outcome = Outcome::Aborted; + Item.reason = Reason; + ++Result.retained; + } + } + + const char* OutcomeName(Outcome Value) { + switch (Value) { + case Outcome::Collected: return "collected"; + case Outcome::DestroyPending: return "destroy_pending"; + case Outcome::SlotReused: return "slot_reused"; + case Outcome::Aborted: return "aborted"; + default: return "retained"; + } + } + + void WriteManifest(const std::vector& Candidates, const std::string& MapContext, Metrics& Result) { + const std::filesystem::path Directory = ExecutableDirectory(); + const std::string Stem = "asset-strip-" + UtcTimestamp(true) + "-" + std::to_string(GetCurrentProcessId()); + const std::filesystem::path FinalPath = Directory / (Stem + ".jsonl"); + const std::filesystem::path TemporaryPath = Directory / (Stem + ".jsonl.tmp"); + std::ofstream Output(TemporaryPath, std::ios::out | std::ios::trunc); + if (!Output.is_open()) return; + + Output << "{\"type\":\"header\",\"timestamp\":\"" << UtcTimestamp(false) + << "\",\"mode\":\"" << ModeName(Result.mode) << "\",\"map\":\"" << JsonEscape(MapContext) + << "\",\"candidates\":" << Result.candidates << "}\n"; + for (const Candidate& Item : Candidates) { + Output << "{\"type\":\"asset\",\"index\":" << Item.index + << ",\"class\":\"" << JsonEscape(Item.className) + << "\",\"name\":\"" << JsonEscape(Item.fullName) + << "\",\"package\":\"" << JsonEscape(Item.packageName) + << "\",\"outcome\":\"" << OutcomeName(Item.outcome) + << "\",\"reason\":\"" << JsonEscape(Item.reason) << "\"}\n"; + } + Output << "{\"type\":\"summary\",\"metrics\":" << MetricsJson(Result) << "}\n"; + Output.flush(); + if (!Output.good()) { + Output.close(); + DeleteFileW(TemporaryPath.c_str()); + return; + } + Output.close(); + if (MoveFileExW(TemporaryPath.c_str(), FinalPath.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) { + Result.manifestWritten = true; + Result.manifestPath = FinalPath.string(); + } + else { + DeleteFileW(TemporaryPath.c_str()); + } + } + } + + Mode ParseMode(const std::wstring& Value) { + if (Value == L"off") return Mode::Off; + if (Value == L"report") return Mode::Report; + if (Value == L"safe") return Mode::Safe; + if (Value == L"aggressive") return Mode::Aggressive; + return Mode::Aggressive; + } + + const char* ModeName(Mode Value) { + switch (Value) { + case Mode::Report: return "report"; + case Mode::Safe: return "safe"; + case Mode::Aggressive: return "aggressive"; + default: return "off"; + } + } + + const char* SafetyGateName(SafetyGate Value) { + switch (Value) { + case SafetyGate::Passed: return "passed"; + case SafetyGate::InitialLoad: return "initial_load"; + case SafetyGate::ConnectionsPresent: return "connections_present"; + case SafetyGate::Timeout: return "timeout"; + case SafetyGate::SignatureMismatch: return "signature_mismatch"; + case SafetyGate::Failed: return "failed"; + default: return "not_required"; + } + } + + bool IsInitialLoadComplete() { + const auto Flag = reinterpret_cast(GetModuleHandleW(nullptr)) + InitialLoadFlagRva; + return *Flag == 0; + } + + Metrics MakeSkipped(Mode ModeValue, SafetyGate Gate, const std::string& Reason) { + Metrics Result{}; + Result.mode = ModeValue; + Result.safetyGate = Gate; + Result.failed = Gate != SafetyGate::NotRequired; + Result.error = Reason; + return Result; + } + + Metrics Run(Mode ModeValue, bool StripInactiveMapPackages, bool LogDetails, const std::string& MapContext) { + Metrics Result{}; + Result.mode = ModeValue; + Result.safetyGate = ModeValue == Mode::Off || ModeValue == Mode::Report + ? SafetyGate::NotRequired : SafetyGate::Passed; + const auto StartedAt = std::chrono::steady_clock::now(); + const ProcessMemory Before = ReadProcessMemory(); + Result.workingSetBefore = Before.workingSet; + Result.privateBytesBefore = Before.privateBytes; + std::vector Candidates; + + if (ModeValue == Mode::Off) { + Result.durationMs = 0; + Result.workingSetAfter = Before.workingSet; + Result.privateBytesAfter = Before.privateBytes; + return Result; + } + + if (ModeValue != Mode::Report && !VerifyCollectGarbageSignature(Result)) { + Result.safetyGate = SafetyGate::SignatureMismatch; + Result.failed = true; + Result.error = "CollectGarbage(uint32,bool) signature mismatch; cleanup skipped"; + Result.workingSetAfter = Before.workingSet; + Result.privateBytesAfter = Before.privateBytes; + return Result; + } + + Result.objectCountBefore = CountLiveObjects(); + try { + const PackageSelection Packages = FindPackages(MapContext); + Result.activeMapPackages = Packages.protectedPackages.size(); + Result.inactiveMapPackages = Packages.inactiveMapPackages.size(); + Candidates.reserve(4096); + + for (int32_t Index = 0; Index < UObject::GObjects->Num(); ++Index) { + FUObjectItem* ObjectItem = UObject::GObjects->GetItemByIndex(Index); + UObject* Object = ObjectItem ? ObjectItem->Object : nullptr; + if (!Object || Object->IsDefaultObject() || IsDestroying(Object)) continue; + + const std::string PackageName = GetOutermostName(Object); + if (Packages.protectedPackages.contains(PackageName)) continue; + + const bool IsTexture = Object->IsA(UTexture::StaticClass()); + const bool IsMaterial = Object->IsA(UMaterialInterface::StaticClass()); + const bool IsSound = Object->IsA(USoundBase::StaticClass()); + const bool IsInactiveMapObject = StripInactiveMapPackages && + Packages.inactiveMapPackages.contains(PackageName); + const bool IsAggressive = ModeValue == Mode::Aggressive || ModeValue == Mode::Report; + const bool IsCandidate = IsTexture || IsMaterial || (IsAggressive && (IsSound || IsInactiveMapObject)); + if (!IsCandidate || IsApexDestructionObject(Object)) continue; + + if (IsTexture) ++Result.textures; + if (IsMaterial) ++Result.materials; + if (IsSound) ++Result.sounds; + if (IsInactiveMapObject) ++Result.mapPackageCandidates; + Candidates.push_back({ + Index, + Object, + ObjectItem->SerialNumber, + static_cast(Object->Flags), + static_cast(ObjectItem->Flags), + LogDetails ? Object->GetFullName() : std::string(), + LogDetails && Object->Class ? Object->Class->GetName() : std::string(), + LogDetails ? PackageName : std::string() + }); + } + Result.candidates = Candidates.size(); + + if (ModeValue == Mode::Report) { + Result.retained = Result.candidates; + } + else { + for (Candidate& Item : Candidates) { + FUObjectItem* CurrentItem = UObject::GObjects->GetItemByIndex(Item.index); + if (!IsSameCandidateObject(CurrentItem, Item) || IsDestroying(Item.object)) continue; + Item.object->Flags = static_cast(static_cast(Item.object->Flags) & ~PublicKeepFlags); + CurrentItem->Flags = static_cast(static_cast(CurrentItem->Flags) & ~InternalRootSet); + } + + using CollectGarbage = void(*)(uint32_t, bool); + reinterpret_cast(reinterpret_cast(GetModuleHandleW(nullptr)) + CollectGarbageRva)(0, true); + + for (Candidate& Item : Candidates) { + FUObjectItem* CurrentItem = UObject::GObjects->GetItemByIndex(Item.index); + if (!CurrentItem || !CurrentItem->Object) { + Item.outcome = Outcome::Collected; + Item.reason = "object_slot_cleared"; + ++Result.collected; + } + else if (!IsSameCandidateObject(CurrentItem, Item)) { + Item.outcome = Outcome::SlotReused; + Item.reason = "object_slot_reused"; + ++Result.collected; + } + else if (IsDestroying(Item.object)) { + Item.outcome = Outcome::DestroyPending; + Item.reason = "destruction_started"; + ++Result.collected; + } + else { + RestoreSurvivingFlags(Item); + Item.outcome = Outcome::Retained; + Item.reason = "still_referenced_flags_restored"; + ++Result.retained; + } + } + } + } + catch (const std::exception& Error) { + RestoreAfterFailure(Candidates, Result, "gc_failed"); + Result.safetyGate = SafetyGate::Failed; + Result.failed = true; + Result.error = Error.what(); + } + catch (...) { + RestoreAfterFailure(Candidates, Result, "gc_failed"); + Result.safetyGate = SafetyGate::Failed; + Result.failed = true; + Result.error = "unknown asset cleanup failure"; + } + + Result.objectCountAfter = CountLiveObjects(); + const ProcessMemory After = ReadProcessMemory(); + Result.workingSetAfter = After.workingSet; + Result.privateBytesAfter = After.privateBytes; + Result.durationMs = static_cast(std::chrono::duration_cast( + std::chrono::steady_clock::now() - StartedAt).count()); + if (LogDetails) WriteManifest(Candidates, MapContext, Result); + return Result; + } + + std::string MetricsJson(const Metrics& Value) { + std::ostringstream Json; + Json << "{\"mode\":\"" << ModeName(Value.mode) + << "\",\"safetyGate\":\"" << SafetyGateName(Value.safetyGate) + << "\",\"signatureValid\":" << (Value.signatureValid ? "true" : "false") + << ",\"candidates\":" << Value.candidates + << ",\"collected\":" << Value.collected + << ",\"retained\":" << Value.retained + << ",\"textures\":" << Value.textures + << ",\"materials\":" << Value.materials + << ",\"sounds\":" << Value.sounds + << ",\"mapPackageCandidates\":" << Value.mapPackageCandidates + << ",\"activeMapPackages\":" << Value.activeMapPackages + << ",\"inactiveMapPackages\":" << Value.inactiveMapPackages + << ",\"durationMs\":" << Value.durationMs + << ",\"workingSetBefore\":" << Value.workingSetBefore + << ",\"workingSetAfter\":" << Value.workingSetAfter + << ",\"privateBytesBefore\":" << Value.privateBytesBefore + << ",\"privateBytesAfter\":" << Value.privateBytesAfter + << ",\"configuredMaxFps\":" << Value.configuredMaxFps + << ",\"observedMaxFps\":" << Value.observedMaxFps + << ",\"capSignatureValid\":" << (Value.capSignatureValid ? "true" : "false") + << ",\"capResolved\":" << (Value.capResolved ? "true" : "false") + << ",\"capApplied\":" << (Value.capApplied ? "true" : "false") + << ",\"capVerified\":" << (Value.capVerified ? "true" : "false") + << ",\"preReadyNetworkingPasses\":" << Value.preReadyNetworkingPasses + << ",\"netServerMaxTickRate\":" << Value.netServerMaxTickRate + << ",\"maxNetTickRate\":" << Value.maxNetTickRate + << ",\"bootstrapMinimumMilliseconds\":" << Value.bootstrapMinimumMilliseconds + << ",\"bootstrapMaximumMilliseconds\":" << Value.bootstrapMaximumMilliseconds + << ",\"considerCacheMaxAgeMilliseconds\":" << Value.considerCacheMaxAgeMilliseconds + << ",\"profilingEnabled\":" << (Value.profilingEnabled ? "true" : "false") + << ",\"failed\":" << (Value.failed ? "true" : "false"); + if (!Value.manifestPath.empty()) Json << ",\"manifestPath\":\"" << JsonEscape(Value.manifestPath) << "\""; + if (!Value.error.empty()) Json << ",\"error\":\"" << JsonEscape(Value.error) << "\""; + Json << "}"; + return Json.str(); + } +} diff --git a/UndauntedInternalServer/AssetOptimization.h b/UndauntedInternalServer/AssetOptimization.h new file mode 100644 index 0000000..73df613 --- /dev/null +++ b/UndauntedInternalServer/AssetOptimization.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include + +namespace AssetOptimization { + enum class Mode { + Off, + Report, + Safe, + Aggressive + }; + + enum class SafetyGate { + NotRequired, + Passed, + InitialLoad, + ConnectionsPresent, + Timeout, + SignatureMismatch, + Failed + }; + + struct Metrics { + Mode mode = Mode::Off; + SafetyGate safetyGate = SafetyGate::NotRequired; + bool signatureValid = false; + bool profilingEnabled = false; + bool failed = false; + uint64_t objectCountBefore = 0; + uint64_t objectCountAfter = 0; + uint64_t candidates = 0; + uint64_t collected = 0; + uint64_t retained = 0; + uint64_t textures = 0; + uint64_t materials = 0; + uint64_t sounds = 0; + uint64_t mapPackageCandidates = 0; + uint64_t activeMapPackages = 0; + uint64_t inactiveMapPackages = 0; + uint64_t durationMs = 0; + uint64_t workingSetBefore = 0; + uint64_t workingSetAfter = 0; + uint64_t privateBytesBefore = 0; + uint64_t privateBytesAfter = 0; + uint64_t configuredMaxFps = 30; + double observedMaxFps = 0.0; + bool capSignatureValid = false; + bool capResolved = false; + bool capApplied = false; + bool capVerified = false; + uint64_t preReadyNetworkingPasses = 0; + uint64_t netServerMaxTickRate = 0; + uint64_t maxNetTickRate = 0; + uint64_t bootstrapMinimumMilliseconds = 3000; + uint64_t bootstrapMaximumMilliseconds = 5000; + uint64_t considerCacheMaxAgeMilliseconds = 250; + bool manifestWritten = false; + std::string manifestPath; + std::string expectedGcSignature; + std::string observedGcSignature; + std::string error; + }; + + Mode ParseMode(const std::wstring& value); + const char* ModeName(Mode mode); + const char* SafetyGateName(SafetyGate gate); + bool IsInitialLoadComplete(); + Metrics MakeSkipped(Mode mode, SafetyGate gate, const std::string& reason); + Metrics Run(Mode mode, bool stripInactiveMapPackages, bool logDetails, const std::string& mapContext); + std::string MetricsJson(const Metrics& metrics); +} diff --git a/UndauntedInternalServer/Hooks/ClientCombatCompatibility.inl b/UndauntedInternalServer/Hooks/ClientCombatCompatibility.inl new file mode 100644 index 0000000..9cb568b --- /dev/null +++ b/UndauntedInternalServer/Hooks/ClientCombatCompatibility.inl @@ -0,0 +1,214 @@ +namespace ClientCombatCompatibility { + constexpr uint64_t ReviveIntentTimeoutMilliseconds = 1000; + + struct PendingReviveIntent { + StableObjectIdentity Pawn{}; + StableObjectIdentity World{}; + ELoadoutQuickItemSlot Slot = ELoadoutQuickItemSlot::Slot_Flask_Revive; + uint64_t QueuedAtMilliseconds = 0; + }; + + static PendingReviveIntent PendingRevive{}; + static StableObjectIdentity BleedoutAcknowledgedPawn{}; + static UFunction* TryUseQuickItemFunction = nullptr; + static UFunction* ReviveFromBleedoutFunction = nullptr; + static UFunction* ReviveFromBleedoutBlueprintFunction = nullptr; + static UFunction* SetBleedoutStateFunction = nullptr; + static UFunction* ClientBleedoutStartedFunction = nullptr; + static UFunction* TickStaminaFunction = nullptr; + static UFunction* EvaluateStaminaFunction = nullptr; + static uint64_t LastFunctionResolveAttemptMilliseconds = 0; + static bool ServerOnlyStaminaSuppressionEnabled = false; + + static bool AllFunctionsResolved() { + return TryUseQuickItemFunction && ReviveFromBleedoutFunction && + ReviveFromBleedoutBlueprintFunction && SetBleedoutStateFunction && + ClientBleedoutStartedFunction && TickStaminaFunction && EvaluateStaminaFunction; + } + + static void ResolveFunctions() { + if (AllFunctionsResolved()) + return; + const uint64_t NowMilliseconds = GetTickCount64(); + if (NowMilliseconds - LastFunctionResolveAttemptMilliseconds < 1000) + return; + LastFunctionResolveAttemptMilliseconds = NowMilliseconds; + + UClass* PlayerClass = ABP_PlayerCharacter_C::StaticClass(); + if (PlayerClass) { + if (!TryUseQuickItemFunction) + TryUseQuickItemFunction = PlayerClass->GetFunction( + "BP_PlayerCharacter_C", "TryUseQuickItem"); + if (!ReviveFromBleedoutBlueprintFunction) + ReviveFromBleedoutBlueprintFunction = PlayerClass->GetFunction( + "BP_PlayerCharacter_C", "BP_ReviveFromBleedout"); + if (!SetBleedoutStateFunction) + SetBleedoutStateFunction = PlayerClass->GetFunction( + "BP_PlayerCharacter_C", "Set Bleedout State"); + if (!TickStaminaFunction) + TickStaminaFunction = PlayerClass->GetFunction( + "BP_PlayerCharacter_C", "TickStamina"); + if (!EvaluateStaminaFunction) + EvaluateStaminaFunction = PlayerClass->GetFunction( + "BP_PlayerCharacter_C", "Evaluate Stamina System"); + } + UClass* CharacterClass = AArchonCharacter::StaticClass(); + if (!ReviveFromBleedoutFunction && CharacterClass) + ReviveFromBleedoutFunction = CharacterClass->GetFunction( + "ArchonCharacter", "BP_ReviveFromBleedout"); + UClass* BleedoutClass = UArchonBleedoutComponent::StaticClass(); + if (!ClientBleedoutStartedFunction && BleedoutClass) + ClientBleedoutStartedFunction = BleedoutClass->GetFunction( + "ArchonBleedoutComponent", "ClientOnBleedoutStarted"); + } + + static void ClearPendingRevive(const char* Diagnostic = nullptr) { + if (PendingRevive.Pawn.Object && Diagnostic) + OutputDebugStringA(Diagnostic); + PendingRevive = {}; + } + + static bool IsLocalPawn(APawn* Pawn) { + if (!Pawn) + return false; + const StableObjectIdentity PawnIdentity = TrackStableObject(Pawn); + if (ResolveStableObject(PawnIdentity) != Pawn) + return false; + AController* Controller = Pawn->Controller; + const StableObjectIdentity ControllerIdentity = TrackStableObject(Controller); + if (ResolveStableObject(ControllerIdentity) != Controller || + !Controller->IsA(APlayerController::StaticClass())) + return false; + APlayerController* PlayerController = static_cast(Controller); + UPlayer* Player = PlayerController->Player; + const StableObjectIdentity PlayerIdentity = TrackStableObject(Player); + return ResolveStableObject(PlayerIdentity) == Player && + Player->IsA(ULocalPlayer::StaticClass()) && PlayerController->Pawn == Pawn; + } + + static void BeginReviveIntent(ABP_PlayerCharacter_C* Pawn, + ELoadoutQuickItemSlot Slot, UWorld* World) { + if (PendingRevive.Pawn.Object) + return; + PendingRevive.Pawn = TrackStableObject(Pawn); + PendingRevive.World = TrackStableObject(World); + PendingRevive.Slot = Slot; + PendingRevive.QueuedAtMilliseconds = GetTickCount64(); + OutputDebugStringA("[Undaunted] buffered early self-revive until bleedout acknowledgement\n"); + } + + static void AcknowledgeBleedout(APawn* Pawn) { + if (!Pawn || !IsLocalPawn(Pawn)) + return; + BleedoutAcknowledgedPawn = TrackStableObject(Pawn); + if (!PendingRevive.Pawn.Object || ResolveStableObject(PendingRevive.Pawn) != Pawn || + !TryUseQuickItemFunction || !OrigProcessEvent) + return; + if (GetTickCount64() - PendingRevive.QueuedAtMilliseconds > + ReviveIntentTimeoutMilliseconds) { + ClearPendingRevive("[Undaunted] cancelled expired buffered self-revive\n"); + return; + } + + Params::BP_PlayerCharacter_C_TryUseQuickItem ReplayParams{}; + ReplayParams.QuickItemIndex = PendingRevive.Slot; + ClearPendingRevive(); + reinterpret_cast(OrigProcessEvent)( + Pawn, TryUseQuickItemFunction, &ReplayParams); + OutputDebugStringA("[Undaunted] replayed self-revive after bleedout acknowledgement\n"); + } + + static void ClearBleedout(APawn* Pawn) { + if (!Pawn || ResolveStableObject(BleedoutAcknowledgedPawn) == Pawn) + BleedoutAcknowledgedPawn = {}; + if (!Pawn || ResolveStableObject(PendingRevive.Pawn) == Pawn) + ClearPendingRevive(); + } + + static void HandleProcessEvent(UObject* Object, UFunction* Function, void* Parms) { + const auto CallOriginal = [&]() { + reinterpret_cast(OrigProcessEvent)( + Object, Function, Parms); + }; + + const bool IsPlayerCharacter = Object->IsA(ABP_PlayerCharacter_C::StaticClass()); + if (ServerOnlyStaminaSuppressionEnabled && IsPlayerCharacter && + (!TickStaminaFunction || !EvaluateStaminaFunction)) { + const std::string IncomingName = Function->GetName(); + if (!TickStaminaFunction && IncomingName == "TickStamina") + TickStaminaFunction = Function; + else if (!EvaluateStaminaFunction && IncomingName == "Evaluate Stamina System") + EvaluateStaminaFunction = Function; + } + if (ServerOnlyStaminaSuppressionEnabled && IsPlayerCharacter && + (Function == TickStaminaFunction || Function == EvaluateStaminaFunction)) { + return; + } + + if (Function == TryUseQuickItemFunction && IsPlayerCharacter && Parms) { + APawn* Pawn = static_cast(Object); + const auto* QuickItemParams = + static_cast(Parms); + if (QuickItemParams->QuickItemIndex == ELoadoutQuickItemSlot::Slot_Flask_Revive && + IsLocalPawn(Pawn) && ResolveStableObject(BleedoutAcknowledgedPawn) != Pawn) { + BeginReviveIntent(static_cast(Object), + QuickItemParams->QuickItemIndex, UWorld::GetWorld()); + return; + } + } + else if ((Function == ReviveFromBleedoutFunction || + Function == ReviveFromBleedoutBlueprintFunction) && + Object->IsA(AArchonCharacter::StaticClass())) { + const StableObjectIdentity PawnIdentity = TrackStableObject(Object); + CallOriginal(); + if (APawn* Pawn = static_cast(ResolveStableObject(PawnIdentity))) + ClearBleedout(Pawn); + return; + } + else if (Function == SetBleedoutStateFunction && IsPlayerCharacter && Parms) { + const StableObjectIdentity PawnIdentity = TrackStableObject(Object); + const bool BleedingOut = + static_cast( + Parms)->Bleeding_Out_; + CallOriginal(); + if (!BleedingOut) { + if (APawn* Pawn = static_cast(ResolveStableObject(PawnIdentity))) + ClearBleedout(Pawn); + } + return; + } + else if (Function == ClientBleedoutStartedFunction && + Object->IsA(UArchonBleedoutComponent::StaticClass())) { + const StableObjectIdentity OwnerIdentity = + TrackStableObject(FindStableOwningActor(Object)); + CallOriginal(); + AActor* Owner = static_cast(ResolveStableObject(OwnerIdentity)); + if (Owner && Owner->IsA(APawn::StaticClass())) + AcknowledgeBleedout(static_cast(Owner)); + return; + } + CallOriginal(); + } +} + +static void PrepareClientCombatCompatibility() { + if (ClientCombatCompatibility::ServerOnlyStaminaSuppressionEnabled) + ClientCombatCompatibility::ResolveFunctions(); +} + +static void TickClientCombatCompatibility(UWorld* World) { + using namespace ClientCombatCompatibility; + ResolveFunctions(); + + if (!PendingRevive.Pawn.Object) + return; + APawn* Pawn = static_cast(ResolveStableObject(PendingRevive.Pawn)); + if (!Pawn || ResolveStableObject(PendingRevive.World) != World || !IsLocalPawn(Pawn)) { + ClearPendingRevive("[Undaunted] cancelled self-revive after pawn/world transition\n"); + return; + } + if (GetTickCount64() - PendingRevive.QueuedAtMilliseconds > + ReviveIntentTimeoutMilliseconds) { + ClearPendingRevive("[Undaunted] cancelled self-revive acknowledgement timeout\n"); + } +} diff --git a/UndauntedInternalServer/Hooks/NativeNameCleanup.cpp b/UndauntedInternalServer/Hooks/NativeNameCleanup.cpp new file mode 100644 index 0000000..c6f7cae --- /dev/null +++ b/UndauntedInternalServer/Hooks/NativeNameCleanup.cpp @@ -0,0 +1,139 @@ +#include "NativeNameCleanup.h" + +#include +#include +#include +#include +#include +#include + +#include "../SDK.hpp" + +namespace Globals { + extern bool AmServer; + extern uintptr_t BaseAddress; + extern bool EnableLogging; + extern std::wstring MetagameAddress; +} + +namespace { + bool DidPatchNativeNoEpicFormat = false; + bool DidLogNativeNoEpicFormatPatch = false; + + bool IsEnabled() { + return !Globals::AmServer && !Globals::MetagameAddress.empty(); + } + + void Log(const std::string& Message) { + if (Globals::EnableLogging && IsEnabled()) { + std::cout << "[NATIVE-NAME] " << Message << std::endl; + } + } + + std::string HexAddress(uintptr_t Address) { + std::ostringstream Stream; + Stream << "0x" << std::uppercase << std::hex << Address; + return Stream.str(); + } + + std::string HexBytes(uintptr_t Address, size_t Length) { + const auto* Bytes = reinterpret_cast(Address); + std::ostringstream Stream; + Stream << std::uppercase << std::hex << std::setfill('0'); + for (size_t Index = 0; Index < Length; ++Index) { + if (Index > 0) { + Stream << ' '; + } + Stream << std::setw(2) << static_cast(Bytes[Index]); + } + + return Stream.str(); + } + + std::string WidePreview(const wchar_t* Text, size_t MaxChars) { + if (!Text) { + return ""; + } + + std::wstring Wide; + for (size_t Index = 0; Index < MaxChars && Text[Index] != L'\0'; ++Index) { + Wide.push_back(Text[Index]); + } + + return SDK::FString(Wide.c_str()).ToString(); + } + + void PatchNativeNoEpicAccountFormatString() { + if (DidPatchNativeNoEpicFormat || !Globals::BaseAddress) { + return; + } + + // TODO: Find out location of FUN_1409c1ce0 persistent across game versions + // FUN_1409c1ce0 adds [No Epic Account] to local users. We null suffix to L"%s". + constexpr uintptr_t kNoEpicAccountFunctionRva = 0x9C1CE0; + constexpr uintptr_t kNoEpicAccountFunctionEndRva = 0x9C20E8; + constexpr uintptr_t kNoEpicAccountXrefRva = 0x9C2006; + constexpr uintptr_t kNoEpicAccountCallsiteSignatureRva = 0x9C1FFA; + constexpr uintptr_t kNoEpicAccountFormatStringRva = 0x441F218; + constexpr size_t kFunctionEntrySignatureLength = 64; + constexpr size_t kCallsiteSignatureLength = 24; + + const uintptr_t FunctionAddress = Globals::BaseAddress + kNoEpicAccountFunctionRva; + const uintptr_t FunctionEndAddress = Globals::BaseAddress + kNoEpicAccountFunctionEndRva; + const uintptr_t XrefAddress = Globals::BaseAddress + kNoEpicAccountXrefRva; + const uintptr_t CallsiteSignatureAddress = Globals::BaseAddress + kNoEpicAccountCallsiteSignatureRva; + const uintptr_t FormatStringAddress = Globals::BaseAddress + kNoEpicAccountFormatStringRva; + wchar_t* FormatString = reinterpret_cast(FormatStringAddress); + + const std::string IdentifierLog = "identifier=FUN_1409c1ce0" + " functionRva=0x9C1CE0" + " functionVa=" + HexAddress(FunctionAddress) + + " functionEndRva=0x9C20E8" + + " functionEndVa=" + HexAddress(FunctionEndAddress) + + " formatStringRva=0x441F218" + + " formatStringVa=" + HexAddress(FormatStringAddress) + + " xrefRva=0x9C2006" + + " xrefVa=" + HexAddress(XrefAddress) + + " entrySig=" + HexBytes(FunctionAddress, kFunctionEntrySignatureLength) + + " callsiteSigRva=0x9C1FFA" + + " callsiteSig=" + HexBytes(CallsiteSignatureAddress, kCallsiteSignatureLength); + + if (std::wcsncmp(FormatString, L"%s [No Epic Account]", 20) != 0) { + if (!DidLogNativeNoEpicFormatPatch) { + DidLogNativeNoEpicFormatPatch = true; + Log("native [No Epic Account] format string mismatch; found=\"" + + WidePreview(FormatString, 48) + "\" " + IdentifierLog); + } + return; + } + + DWORD OldProtect = 0; + if (!VirtualProtect(&FormatString[2], sizeof(wchar_t), PAGE_READWRITE, &OldProtect)) { + if (!DidLogNativeNoEpicFormatPatch) { + DidLogNativeNoEpicFormatPatch = true; + Log("failed to make native [No Epic Account] format string writable; " + + IdentifierLog); + } + return; + } + + FormatString[2] = L'\0'; + VirtualProtect(&FormatString[2], sizeof(wchar_t), OldProtect, &OldProtect); + DidPatchNativeNoEpicFormat = true; + if (!DidLogNativeNoEpicFormatPatch) { + DidLogNativeNoEpicFormatPatch = true; + Log("patched native [No Epic Account] format string to L\"%s\"; " + + IdentifierLog); + } + } +} + +namespace NativeNameCleanup { + void Init() { + if (!IsEnabled()) { + return; + } + + PatchNativeNoEpicAccountFormatString(); + } +} diff --git a/UndauntedInternalServer/Hooks/NativeNameCleanup.h b/UndauntedInternalServer/Hooks/NativeNameCleanup.h new file mode 100644 index 0000000..c620f2c --- /dev/null +++ b/UndauntedInternalServer/Hooks/NativeNameCleanup.h @@ -0,0 +1,5 @@ +#pragma once + +namespace NativeNameCleanup { + void Init(); +} diff --git a/UndauntedInternalServer/Hooks/ProcessEventState.inl b/UndauntedInternalServer/Hooks/ProcessEventState.inl new file mode 100644 index 0000000..045813c --- /dev/null +++ b/UndauntedInternalServer/Hooks/ProcessEventState.inl @@ -0,0 +1,61 @@ +void* OrigProcessEvent = nullptr; + +struct StableObjectIdentity { + UObject* Object = nullptr; + int32_t Index = -1; + int32_t SerialNumber = 0; +}; + +static StableObjectIdentity TrackStableObject(UObject* Object) { + if (!Object || !UObject::GObjects || Object->Index < 0) + return {}; + FUObjectItem* Item = UObject::GObjects->GetItemByIndex(Object->Index); + if (!Item || Item->Object != Object) + return {}; + return { Object, Object->Index, Item->SerialNumber }; +} + +static UObject* ResolveStableObject(const StableObjectIdentity& Identity) { + if (!Identity.Object || !UObject::GObjects || Identity.Index < 0) + return nullptr; + FUObjectItem* Item = UObject::GObjects->GetItemByIndex(Identity.Index); + if (!Item || Item->Object != Identity.Object || Item->SerialNumber != Identity.SerialNumber || + (static_cast(Item->Flags) & 0x30000000u) != 0) + return nullptr; + UObject* Object = Item->Object; + if ((Object->Flags & EObjectFlags::BeginDestroyed) || + (Object->Flags & EObjectFlags::FinishDestroyed)) + return nullptr; + return Object; +} + +static AActor* FindStableOwningActor(UObject* Object) { + UObject* Current = Object; + for (uint8_t Depth = 0; Current && Depth < 8; ++Depth) { + const StableObjectIdentity Identity = TrackStableObject(Current); + if (ResolveStableObject(Identity) != Current) + return nullptr; + if (Current->IsA(AActor::StaticClass())) + return static_cast(Current); + Current = Current->Outer; + } + return nullptr; +} + +static bool HasPlayerCharacterOwner(AActor* Actor) { + AActor* Current = Actor; + for (uint8_t Depth = 0; Current && Depth < 8; ++Depth) { + const StableObjectIdentity Identity = TrackStableObject(Current); + if (ResolveStableObject(Identity) != Current) + return false; + if (Current->IsA(AArchonCharacter::StaticClass())) + return true; + AActor* Next = Current->Owner; + if (!Next && Current->Instigator) + Next = Current->Instigator; + if (Next == Current) + return false; + Current = Next; + } + return false; +} diff --git a/UndauntedInternalServer/MinHook/hook.c b/UndauntedInternalServer/MinHook/hook.c index 68a006a..600c874 100644 --- a/UndauntedInternalServer/MinHook/hook.c +++ b/UndauntedInternalServer/MinHook/hook.c @@ -30,7 +30,7 @@ #include #include -#include "../include/MinHook.h" +#include "MinHook.h" #include "buffer.h" #include "trampoline.h" diff --git a/UndauntedInternalServer/Networking.cpp b/UndauntedInternalServer/Networking.cpp deleted file mode 100644 index 0815c52..0000000 --- a/UndauntedInternalServer/Networking.cpp +++ /dev/null @@ -1,181 +0,0 @@ -#include "networking.h" - -#include - -using namespace SDK; - -namespace Networking { - UNetDriver* NetDriver = nullptr; - static uintptr_t BaseAddress = 0x0; - - static std::vector BuildConsiderList(UWorld* World, UNetDriver* Driver) { - std::vector Actors = std::vector(); - - for (ULevel* Level : World->Levels) { - for (AActor* Actor : Level->Actors) { - if (!Actor) - continue; - - if (Actor->RemoteRole == ENetRole::ROLE_None) - continue; - - if (Actor->bActorIsBeingDestroyed) - continue; - - if (!reinterpret_cast(*(void**)((uintptr_t)Actor->VTable + 0x150))(Actor)) { - continue; - } - - reinterpret_cast(BaseAddress + 0x306B150)(Actor, Driver); - - Actors.push_back(Actor); - } - } - - /* - for (int i = 0; i < SDK::UObject::GObjects->Num(); i++) - { - SDK::UObject* Obj = SDK::UObject::GObjects->GetByIndex(i); - - if (!Obj) - continue; - - if (Obj->IsDefaultObject()) - continue; - - if (Obj->IsA(SDK::AActor::StaticClass())) - { - AActor* Actor = (AActor*)Obj; - - if (Actor->RemoteRole == ENetRole::ROLE_None) - continue; - - if (Actor->bActorIsBeingDestroyed) - continue; - - if (!reinterpret_cast(*(void**)((uintptr_t)Actor->VTable + 0x150))(Actor)) { - continue; - } - - reinterpret_cast(BaseAddress + 0x306B150)(Actor, Driver); - - Actors.push_back(Actor); - } - } - */ - - return Actors; - } - - static UActorChannel* GetActorChannelForConnectionAndActor(UNetConnection* Connection, AActor* Actor) { - for (UChannel* Channel : Connection->OpenChannels) { - if (Channel->Class == UActorChannel::StaticClass() && ((UActorChannel*)Channel)->Actor == Actor) { - return ((UActorChannel*)Channel); - } - } - - return nullptr; - } - - void Listen(UEngine* Engine, int Port) { - BaseAddress = (uintptr_t)GetModuleHandleA(nullptr); - - FName GameNetDriver = UKismetStringLibrary::Conv_StringToName(L"GameNetDriver"); - - std::cout << "Net driver create: " << (int)reinterpret_cast(BaseAddress + 0x371A5E0)(Engine, UWorld::GetWorld(), GameNetDriver, GameNetDriver) << std::endl; - - for (int i = 0; i < SDK::UObject::GObjects->Num(); i++) - { - SDK::UObject* Obj = SDK::UObject::GObjects->GetByIndex(i); - - if (!Obj) - continue; - - if (Obj->IsDefaultObject()) - continue; - - if (Obj->IsA(SDK::UNetDriver::StaticClass())) - { - NetDriver = (UNetDriver*)Obj; - break; - } - } - - std::cout << NetDriver->GetFullName() << std::endl; - - reinterpret_cast(BaseAddress + 0x3491890)(NetDriver, UWorld::GetWorld()); - - FURL url = FURL(); - - url.Port = Port; - - FString empy = FString(); - - std::cout << "Listen Status: " << (*(reinterpret_cast(*(__int64*)NetDriver + 0x280)))(NetDriver, (void*)UWorld::GetWorld()->NetworkNotify, &url, false, &empy) << std::endl; - - reinterpret_cast(BaseAddress + 0x3491890)(NetDriver, UWorld::GetWorld()); - - UWorld::GetWorld()->NetDriver = NetDriver; - } - - void TickNetworking() { - UWorld::GetWorld()->NetDriver = NetDriver; - - NetDriver->World = UWorld::GetWorld(); - - static FName name = FName(); - static bool nameInit = false; - - if (!nameInit) { - nameInit = true; - name = UKismetStringLibrary::Conv_StringToName(L"Actor"); - } - - ++ * (uint32_t*)((uintptr_t)NetDriver + 0x2AC); - - std::vector Actors = BuildConsiderList(UWorld::GetWorld(), NetDriver); - - for (UNetConnection* Connection : NetDriver->ClientConnections) { - if (!Connection->OwningActor || *(uint32_t*)((uintptr_t)Connection + 0x134) != 3) - continue; - - for (AActor* Actor : Actors) { - if ((Actor->IsA(APlayerController::StaticClass()))) { - if (Actor != Connection->OwningActor) { - continue; - } - else { - //*(uint8_t*)((uintptr_t)Actor + 0xA14) = 0x1; - Connection->ViewTarget = ((APlayerController*)Actor)->GetViewTarget(); - - if (!Connection->ViewTarget) - std::cout << "NULL VIEWTARGET BAD THINGS WILL HAPPEN" << std::endl; - - reinterpret_cast(BaseAddress + 0x359F9D0)((APlayerController*)Actor); - } - } - - // - - UActorChannel* ActorChannel = GetActorChannelForConnectionAndActor(Connection, Actor); - - if (!ActorChannel) { - ActorChannel = reinterpret_cast(BaseAddress + 0x3449E10)(Connection, &name, 1 << 1, -1); - - if (ActorChannel) { - reinterpret_cast(BaseAddress + 0x3283450)(ActorChannel, Actor, 0); - } - } - - if (ActorChannel && ActorChannel->Actor) { - if (!(*(int*)((uintptr_t)ActorChannel + 0x90) & 2u)) { - *(int*)((uintptr_t)ActorChannel + 0x90) |= 2u; - } - if (reinterpret_cast(BaseAddress + 0x327E860)(ActorChannel)) { - //std::cout << ActorChannel->Actor->GetFullName() << std::endl; - } - } - } - } - } -} \ No newline at end of file diff --git a/UndauntedInternalServer/Networking.h b/UndauntedInternalServer/Networking.h index 8d38d12..0ea65a5 100644 --- a/UndauntedInternalServer/Networking.h +++ b/UndauntedInternalServer/Networking.h @@ -1,14 +1,5 @@ #pragma once -#include -#include "SDK.hpp" - -using namespace SDK; - -namespace Networking { - extern UNetDriver* NetDriver; - - void Listen(UEngine* Engine, int Port); - - void TickNetworking(); -} \ No newline at end of file +// Compatibility include for modules that still include the historical root +// header. The implementation and public declarations now live in Networking/. +#include "Networking/Networking.h" diff --git a/UndauntedInternalServer/Networking/CombatBridge.inl b/UndauntedInternalServer/Networking/CombatBridge.inl new file mode 100644 index 0000000..f59ceaa --- /dev/null +++ b/UndauntedInternalServer/Networking/CombatBridge.inl @@ -0,0 +1,158 @@ +void* OrigProcessEvent = nullptr; + +struct PendingInterruptDamageContext { + PendingInterruptDamageContext* Previous = nullptr; + int32_t TargetIndex = -1; + int32_t TargetSerial = 0; + bool InterruptObserved = false; +}; + +thread_local PendingInterruptDamageContext* ActiveInterruptDamageContext = nullptr; + +void ProcessEventHook(UObject* Object, UFunction* Function, void* Parms) { + static UFunction* ServerTryActivateAbilityWithEventData = nullptr; + static UFunction* ServerTryActivateAbility = nullptr; + static UFunction* ServerNotifyLoadedWorld = nullptr; + static UFunction* ServerAcknowledgePossession = nullptr; + static UFunction* ServerTryDoDamage = nullptr; + static UFunction* ClientOnBehemothInterrupted = nullptr; + static bool AttemptedInterruptClientFunctionResolution = false; + + const auto CallOriginal = [&]() { + reinterpret_cast(OrigProcessEvent)(Object, Function, Parms); + }; + + if (!Object || !Function) { + ServerPerformance::RecordProcessEvent(false, true); + CallOriginal(); + return; + } + + // Gameplay interception only concerns server RPCs. The one passive client + // notification below uses an exact UFunction pointer resolved once from its + // already-loaded class, avoiding name work on the global ProcessEvent path. + constexpr uint32_t NetServerFunctionFlag = static_cast(EFunctionFlags::NetServer); + const bool IsNetServerFunction = (Function->FunctionFlags & NetServerFunctionFlag) != 0; + if (!IsNetServerFunction) { + constexpr uint32_t NetClientFunctionFlag = static_cast(EFunctionFlags::NetClient); + if (Globals::AmServer && + (Function->FunctionFlags & NetClientFunctionFlag) != 0 && + Object->IsA(Aplayer_state_bp_C::StaticClass())) { + if (!AttemptedInterruptClientFunctionResolution) { + AttemptedInterruptClientFunctionResolution = true; + ClientOnBehemothInterrupted = Aplayer_state_bp_C::StaticClass()->GetFunction( + "player_state_bp_C", "ClientOnBehemothInterrupted"); + } + if (Function == ClientOnBehemothInterrupted) { + Networking::NotifyBehemothInterruptClientRpc(); + if (ActiveInterruptDamageContext) + ActiveInterruptDamageContext->InterruptObserved = true; + } + } + ServerPerformance::RecordProcessEvent(false, true); + CallOriginal(); + return; + } + + const bool IsAbilityRpc = Object->IsA(UAbilitySystemComponent::StaticClass()); + const bool IsPlayerControllerRpc = Globals::AmServer && Object->IsA(APlayerController::StaticClass()); + const bool IsDamageRpc = Globals::AmServer && Object->IsA(UDamageComponent::StaticClass()); + if (!IsAbilityRpc && !IsPlayerControllerRpc && !IsDamageRpc) { + ServerPerformance::RecordProcessEvent(true, true); + CallOriginal(); + return; + } + + std::string FunctionName; + auto MatchesFunction = [&](UFunction*& CachedFunction, const char* ExpectedName) { + if (Function == CachedFunction) { + ServerPerformance::RecordProcessEventFunctionMatch(); + return true; + } + if (CachedFunction) + return false; + if (FunctionName.empty()) { + FunctionName = Function->GetName(); + ServerPerformance::RecordProcessEventNameLookup(true); + } + if (FunctionName != ExpectedName) + return false; + CachedFunction = Function; + ServerPerformance::RecordProcessEventFunctionMatch(); + return true; + }; + + if (IsAbilityRpc && MatchesFunction(ServerTryActivateAbilityWithEventData, + "ServerTryActivateAbilityWithEventData")) { + Params::AbilitySystemComponent_ServerTryActivateAbilityWithEventData* ActivateAbilityParams = (Params::AbilitySystemComponent_ServerTryActivateAbilityWithEventData*)Parms; + + if (ActivateAbilityParams && ServerTryActivateAbilityInternal((UAbilitySystemComponent*)Object, ActivateAbilityParams->AbilityToActivate, ActivateAbilityParams->InputPressed, ActivateAbilityParams->PredictionKey, &ActivateAbilityParams->TriggerEventData)) { + ServerPerformance::RecordAbilityRpc(true, false); + ServerPerformance::RecordProcessEvent(true, false); + return; + } + ServerPerformance::RecordAbilityRpc(false, true); + } + else if (IsAbilityRpc && MatchesFunction(ServerTryActivateAbility, "ServerTryActivateAbility")) { + Params::AbilitySystemComponent_ServerTryActivateAbility* ActivateAbilityParams = (Params::AbilitySystemComponent_ServerTryActivateAbility*)Parms; + + if (ActivateAbilityParams && ServerTryActivateAbilityInternal((UAbilitySystemComponent*)Object, ActivateAbilityParams->AbilityToActivate, ActivateAbilityParams->InputPressed, ActivateAbilityParams->PredictionKey, nullptr)) { + ServerPerformance::RecordAbilityRpc(true, false); + ServerPerformance::RecordProcessEvent(true, false); + return; + } + ServerPerformance::RecordAbilityRpc(false, true); + } + + const bool IsLoadedWorldNotification = IsPlayerControllerRpc && MatchesFunction( + ServerNotifyLoadedWorld, "ServerNotifyLoadedWorld"); + const bool IsPossessionAcknowledgement = IsPlayerControllerRpc && MatchesFunction( + ServerAcknowledgePossession, "ServerAcknowledgePossession"); + const bool IsDamageRpcMatch = IsDamageRpc && MatchesFunction( + ServerTryDoDamage, "ServerTryDoDamage"); + if (IsDamageRpcMatch) + Networking::NotifyDamageRpcObserved(); + const bool IsDamageNotification = IsDamageRpcMatch && Parms; + int32_t DamageTargetIndex = -1; + int32_t DamageTargetSerial = 0; + if (IsDamageNotification) { + const auto* DamageParams = static_cast(Parms); + // Copy the weak identity while the incoming parameter block is valid. + // Networking deliberately resolves index+serial itself after native + // validation/execution; generated FWeakObjectPtr::Get ignores serials. + DamageTargetIndex = DamageParams->Payload.Hit.Actor.ObjectIndex; + DamageTargetSerial = DamageParams->Payload.Hit.Actor.ObjectSerialNumber; + } + + ServerPerformance::RecordProcessEvent(true, true); + PendingInterruptDamageContext DamageContext{ + ActiveInterruptDamageContext, + DamageTargetIndex, + DamageTargetSerial, + false + }; + if (IsDamageNotification) + ActiveInterruptDamageContext = &DamageContext; + CallOriginal(); + if (IsDamageNotification) { + ActiveInterruptDamageContext = DamageContext.Previous; + if (DamageContext.InterruptObserved) { + Networking::NotifyUrgentDamageTarget( + DamageContext.TargetIndex, DamageContext.TargetSerial); + } + } + + if (!IsPlayerControllerRpc) + return; + + APlayerController* PlayerController = static_cast(Object); + if (IsLoadedWorldNotification && Parms) { + const auto* LoadedWorldParams = static_cast(Parms); + Networking::NotifyClientLoadedWorld(PlayerController, LoadedWorldParams->WorldPackageName); + } + else if (IsPossessionAcknowledgement && Parms) { + const auto* PossessionParams = static_cast(Parms); + Networking::NotifyClientAcknowledgedPawn(PlayerController, PossessionParams->P); + } + +} diff --git a/UndauntedInternalServer/Networking/NetworkLifecycle.cpp b/UndauntedInternalServer/Networking/NetworkLifecycle.cpp new file mode 100644 index 0000000..40456c6 --- /dev/null +++ b/UndauntedInternalServer/Networking/NetworkLifecycle.cpp @@ -0,0 +1,100 @@ +#include "NetworkLifecycle.h" + +#include +#include +#include + +namespace Networking::Lifecycle { + namespace { + LifecycleConfig Config{}; + std::atomic Observation{0}; + std::atomic HeartbeatSequence{0}; + uint32_t ObservationSequence = 0; + uint16_t WorldEpoch = 0; + SDK::UWorld* ObservedWorld = nullptr; + HANDLE Timer = nullptr; + + std::string Utf8(const std::wstring& Value) { + if (Value.empty()) return {}; + const int Size = WideCharToMultiByte(CP_UTF8, 0, Value.c_str(), -1, + nullptr, 0, nullptr, nullptr); + if (Size <= 1) return {}; + std::string Result(static_cast(Size - 1), '\0'); + WideCharToMultiByte(CP_UTF8, 0, Value.c_str(), -1, Result.data(), + Size, nullptr, nullptr); + return Result; + } + + void CALLBACK Heartbeat(void*, BOOLEAN) { + const uint64_t Packed = Observation.load(std::memory_order_acquire); + const uint32_t Sequence = static_cast(Packed >> 32); + if (Sequence == 0 || Config.callbackUrl.empty()) return; + + uint32_t HeartbeatId = HeartbeatSequence.fetch_add( + 1, std::memory_order_relaxed) + 1; + if (HeartbeatId == 0) { + HeartbeatSequence.store(1, std::memory_order_relaxed); + HeartbeatId = 1; + } + + const uint32_t Epoch = static_cast((Packed >> 16) & 0xffffu); + const uint32_t Raw = static_cast((Packed >> 8) & 0xffu); + const uint32_t Tracked = static_cast(Packed & 0xffu); + const std::string Body = "{\"id\":\"" + Utf8(Config.serverId) + + "\",\"port\":" + std::to_string(Config.port) + + ",\"pid\":" + std::to_string(GetCurrentProcessId()) + + ",\"liveConnections\":" + std::to_string(Tracked) + + ",\"rawConnections\":" + std::to_string(Raw) + + ",\"protocolVersion\":" + std::to_string(Config.protocolVersion) + + ",\"heartbeatSequence\":" + std::to_string(HeartbeatId) + + ",\"observationSequence\":" + std::to_string(Sequence) + + ",\"worldEpoch\":" + std::to_string(Epoch) + "}"; + + if (Config.callback != nullptr) + Config.callback(Config.callbackUrl, L"x-undaunted-lifecycle-token", + Config.callbackToken, Body); + } + } + + bool Start(const LifecycleConfig& NewConfig) { + Stop(); + Config = NewConfig; + if (Config.callbackUrl.empty()) return false; + return CreateTimerQueueTimer(&Timer, nullptr, Heartbeat, nullptr, 0, + 5000, WT_EXECUTEDEFAULT) != FALSE; + } + + void Stop() { + if (Timer != nullptr) { + DeleteTimerQueueTimer(nullptr, Timer, INVALID_HANDLE_VALUE); + Timer = nullptr; + } + Observation.store(0, std::memory_order_release); + HeartbeatSequence.store(0, std::memory_order_release); + ObservationSequence = 0; + WorldEpoch = 0; + ObservedWorld = nullptr; + } + + void ObserveConnections(uint32_t Tracked, uint32_t Raw, SDK::UWorld* World) { + if (ObservedWorld != World) { + ObservedWorld = World; + if (++WorldEpoch == 0) WorldEpoch = 1; + } + if (++ObservationSequence == 0) ObservationSequence = 1; + const uint64_t Packed = + (static_cast(ObservationSequence) << 32) | + (static_cast(WorldEpoch) << 16) | + (static_cast((std::min)(Raw, 128u)) << 8) | + static_cast((std::min)(Tracked, 128u)); + Observation.store(Packed, std::memory_order_release); + } + + void ResetWorld(SDK::UWorld* World) { + if (ObservedWorld == World) return; + ObservedWorld = World; + ObservationSequence = 0; + Observation.store(0, std::memory_order_release); + if (++WorldEpoch == 0) WorldEpoch = 1; + } +} diff --git a/UndauntedInternalServer/Networking/NetworkLifecycle.h b/UndauntedInternalServer/Networking/NetworkLifecycle.h new file mode 100644 index 0000000..9e7116e --- /dev/null +++ b/UndauntedInternalServer/Networking/NetworkLifecycle.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +namespace SDK { + class UWorld; +} + +namespace Networking::Lifecycle { + using Callback = bool(*)(const std::wstring& Url, const std::wstring& TokenHeader, + const std::wstring& Token, const std::string& Body); + + struct LifecycleConfig { + std::wstring callbackUrl; + std::wstring callbackToken; + std::wstring serverId; + uint16_t port = 0; + uint32_t protocolVersion = 2; + Callback callback = nullptr; + }; + + bool Start(const LifecycleConfig& Config); + void Stop(); + void ObserveConnections(uint32_t Tracked, uint32_t Raw, SDK::UWorld* World); + void ResetWorld(SDK::UWorld* World); +} diff --git a/UndauntedInternalServer/Networking/Networking.cpp b/UndauntedInternalServer/Networking/Networking.cpp new file mode 100644 index 0000000..bbe1b93 --- /dev/null +++ b/UndauntedInternalServer/Networking/Networking.cpp @@ -0,0 +1,8 @@ +// Public networking facade. Implementation is split into private fragments so +// every subsystem shares one translation-unit RuntimeState without duplicated +// hook state or changed scheduling order. +#include "Private/RuntimeState.inl" +#include "Private/ConnectionState.inl" +#include "Private/ConsiderCache.inl" +#include "Private/ReplicationScheduler.inl" +#include "Private/ListenDispatch.inl" diff --git a/UndauntedInternalServer/Networking/Networking.h b/UndauntedInternalServer/Networking/Networking.h new file mode 100644 index 0000000..ad68377 --- /dev/null +++ b/UndauntedInternalServer/Networking/Networking.h @@ -0,0 +1,276 @@ +#pragma once + +#include +#include +#include +#include +#include "../SDK.hpp" + +using namespace SDK; + +namespace Networking { + using LifecycleEventSink = void(*)(const std::string& eventName, + const std::string& fieldsJson); + + constexpr size_t ReplicationBucketCount = 6; + constexpr size_t ConnectionBootstrapPhaseCount = 2; + constexpr size_t ImmediateMetricTrackedClassCount = 256; + constexpr size_t ImmediateMetricClassSlotCount = ImmediateMetricTrackedClassCount + 2; + constexpr size_t ImmediateNoDataBandCount = 6; + constexpr size_t ImmediateMetricClassNameCapacity = 64; + constexpr size_t NetFrequencyBandCount = 10; + constexpr size_t NetDormancyBandCount = 7; + constexpr size_t NetRoleCount = 5; + constexpr size_t NetRoleMatrixCount = NetRoleCount * NetRoleCount; + constexpr size_t PriorityBandCount = 5; + constexpr size_t CacheRebuildReasonCount = 6; + constexpr uint32_t ProfileSchemaVersion = 2; + + enum class UrgentReplicationReason : uint8_t { + Stagger, + Bleedout, + Revive, + Dodge, + Count + }; + constexpr size_t UrgentReplicationReasonCount = + static_cast(UrgentReplicationReason::Count); + + enum class CombatEvent : uint8_t { + BleedoutStarted, + ReviveCompleted, + ReviveQuickItem, + DodgeRequested, + DodgePerformed, + ComboStarted, + ComboNextMove, + ComboStateChanged, + ClientTryHit, + ServerAcceptHit, + ServerRefuseHit, + ProcessHitOnServer, + CombatTextQueued, + CombatTextMulticast, + StaggerConfirmed, + Count + }; + constexpr size_t CombatEventCount = static_cast(CombatEvent::Count); + + struct ImmediateClassCounters { + std::array ClassName{}; + uint64_t Attempts = 0; + uint64_t Successes = 0; + uint64_t ActorPasses = 0; + uint64_t ActorPassAttempts = 0; + uint64_t ActorPassSuccesses = 0; + uint64_t ActorPassesWithAnyData = 0; + uint64_t ActorPassesWithNoData = 0; + std::array NoDataPassStreaks{}; + uint64_t UntrackableOwnerCalls = 0; + uint64_t OwnerSensitiveDeferrals = 0; + uint64_t IrrelevantSkips = 0; + }; + + struct ProfilingCounters { + uint64_t SchedulerTickCalls = 0; + uint64_t ReplicationAttempts = 0; + uint64_t ReplicationSuccesses = 0; + uint64_t PreReplicationCalls = 0; + uint64_t PreReplicationUnavailable = 0; + uint64_t PreReplicationInvalidatedActors = 0; + uint64_t BootstrapReplicationAttempts = 0; + uint64_t BootstrapReplicationSuccesses = 0; + uint64_t UrgentDamageRpcMatches = 0; + uint64_t UrgentDamageInterruptCorrelations = 0; + uint64_t UrgentDamageWeakHandlesInvalid = 0; + uint64_t UrgentDamageDirectBehemothTargets = 0; + uint64_t UrgentDamageOwnerChainTargets = 0; + uint64_t UrgentDamageTargetsWithoutBehemoth = 0; + uint64_t UrgentDamageTargetsQueued = 0; + uint64_t UrgentDamageTargetsDeduplicated = 0; + uint64_t UrgentDamageTargetsDropped = 0; + uint64_t UrgentDamageTargetsInvalidated = 0; + uint64_t UrgentDamageTargetsExpired = 0; + uint64_t UrgentDamageSetupRetries = 0; + uint64_t UrgentDamageSetupRetryExhausted = 0; + uint64_t UrgentDamageReplicationAttempts = 0; + uint64_t UrgentDamageReplicationSuccesses = 0; + uint64_t UrgentDamageLatencyMilliseconds = 0; + uint64_t UrgentDamageLatencySamples = 0; + uint64_t BehemothInterruptClientNotifications = 0; + std::array UrgentQueuedByReason{}; + std::array UrgentDeduplicatedByReason{}; + std::array UrgentDroppedByReason{}; + std::array UrgentInvalidatedByReason{}; + std::array UrgentExpiredByReason{}; + std::array UrgentAttemptsByReason{}; + std::array UrgentSuccessesByReason{}; + std::array UrgentLatencyMillisecondsByReason{}; + std::array UrgentLatencySamplesByReason{}; + uint64_t CriticalPrepassCandidates = 0; + uint64_t CriticalPrepassAttempts = 0; + uint64_t CriticalPrepassSuccesses = 0; + uint64_t CriticalPrepassDuplicateSkips = 0; + uint64_t CriticalPrepassSetupFallbacks = 0; + uint64_t CriticalOwnedFrequencyLimited = 0; + std::array CombatEventCalls{}; + std::array CombatEventSuccesses{}; + uint64_t CombatTextEntries = 0; + uint64_t CombatTextEstimatedBytes = 0; + uint64_t CombatTextMulticastEntries = 0; + uint64_t CombatTextMulticastEstimatedBytes = 0; + std::array ReplicationAttemptsByBucket{}; + std::array ReplicationSuccessesByBucket{}; + std::array ImmediateClasses{}; + std::array BootstrapPhaseConnections{}; + uint64_t CacheRebuilds = 0; + std::array CacheRebuildsByReason{}; + uint64_t CacheRebuildMicroseconds = 0; + uint64_t CacheRebuildTimingSamples = 0; + uint64_t CandidateSelectionMicroseconds = 0; + uint64_t CandidateSelectionTimingSamples = 0; + uint64_t ChannelScanMicroseconds = 0; + uint64_t ChannelScanTimingSamples = 0; + uint64_t ReplicationMicroseconds = 0; + uint64_t ReplicationTimingSamples = 0; + uint64_t ZeroConnectionTicks = 0; + uint64_t ConnectionSamples = 0; + uint64_t ConnectionTotal = 0; + uint64_t NativeDispatchCalls = 0; + uint64_t NativeDispatchOwnershipValidBefore = 0; + uint64_t NativeDispatchOwnershipValidAfter = 0; + uint64_t NativeDispatchOwnershipLostInside = 0; + uint64_t NativeFlushCalls = 0; + uint64_t NativeFlushOwnershipValidBefore = 0; + uint64_t NativeFlushOwnershipValidAfter = 0; + uint64_t NativeFlushOwnershipLostInside = 0; + uint64_t NativeOwnershipMissingAfterEngineTick = 0; + uint64_t NativeReciprocalRepairs = 0; + uint64_t NativeDispatchBlocked = 0; + uint64_t NativeDispatchConflicts = 0; + uint64_t NativePostLogins = 0; + uint64_t LoadedWorldAccepted = 0; + uint64_t LoadedWorldMismatched = 0; + uint64_t PossessionAcknowledged = 0; + uint64_t BootstrapStarts = 0; + uint64_t BootstrapRestarts = 0; + uint64_t BootstrapCompleted = 0; + uint64_t BootstrapDeadlines = 0; + uint64_t CriticalActorsDiscovered = 0; + uint64_t CriticalChannelsAcknowledged = 0; + uint64_t ActorChannelsCreated = 0; + uint64_t ActorChannelsReused = 0; + uint64_t ActorPropertySamples = 0; + std::array NetUpdateFrequencyBands{}; + std::array MinNetUpdateFrequencyBands{}; + std::array NetDormancyBands{}; + std::array NetRoleMatrix{}; + uint64_t OwnedActorSamples = 0; + uint64_t ReplicatedMovementSamples = 0; + uint64_t CriticalActorSamples = 0; + uint64_t ImmediateActorSamples = 0; + uint64_t NetTemporarySamples = 0; + uint64_t NetStartupSamples = 0; + uint64_t NetLoadOnClientSamples = 0; + uint64_t OnlyRelevantToOwnerSamples = 0; + uint64_t AlwaysRelevantSamples = 0; + uint64_t OwnerRelevancySamples = 0; + uint64_t TearOffSamples = 0; + uint64_t ExcludedNotReplicated = 0; + uint64_t ExcludedRemoteRoleNone = 0; + uint64_t ExcludedLocalRoleNone = 0; + uint64_t UnexpectedLocalRole = 0; + uint64_t ExcludedDestroying = 0; + uint64_t ExcludedWrongWorld = 0; + uint64_t ChannelStateSamples = 0; + uint64_t ChannelOpenAcknowledgedSamples = 0; + uint64_t ChannelNotOpenAcknowledgedSamples = 0; + uint64_t LivePolicyEligible = 0; + uint64_t LivePolicyDue = 0; + uint64_t LivePolicyNotDue = 0; + uint64_t LivePolicyRelevant = 0; + uint64_t LivePolicyNotRelevant = 0; + uint64_t LivePolicyDormantDeferrals = 0; + uint64_t LivePolicyDriverMismatches = 0; + uint64_t LivePolicyTemporaryRetirements = 0; + uint64_t LivePolicyTearOffRetirements = 0; + uint64_t LivePolicyIdentityFallbacks = 0; + uint64_t LivePolicyActorIdentityFallbacks = 0; + uint64_t LivePolicyConnectionIdentityFallbacks = 0; + uint64_t LivePolicyStateCapacityFallbacks = 0; + uint64_t LivePolicyUntrackableOwners = 0; + uint64_t LivePolicyOwnerSensitiveDeferrals = 0; + uint64_t LivePolicyNativeRelevancyCalls = 0; + uint64_t LivePolicyNativeRelevancyFallbacks = 0; + uint64_t LivePolicyRelevancyAudits = 0; + uint64_t LivePolicyRelevancyRecheckMilliseconds = 0; + uint64_t LivePolicyRelevancyRecheckSamples = 0; + uint64_t LivePolicyIrrelevantSkips = 0; + uint64_t LivePolicyLevelInitializationUnavailable = 0; + uint64_t LivePolicyPrioritySorts = 0; + uint64_t LivePolicyPriorityCandidates = 0; + std::array LivePolicyPriorityBands{}; + uint64_t LivePolicyMovementPrepassAttempts = 0; + uint64_t LivePolicyMovementPrepassSuccesses = 0; + uint64_t LivePolicyCriticalRejected = 0; + uint64_t LivePolicyDuplicateSkips = 0; + uint64_t SchedulerStateInsertions = 0; + uint64_t SchedulerStateCapacityDrops = 0; + uint64_t SchedulerActorIdentityResets = 0; + uint64_t SchedulerOwnerChanges = 0; + uint64_t SchedulerStatePrunes = 0; + uint64_t LoadedWorldLatencyMilliseconds = 0; + uint64_t LoadedWorldLatencySamples = 0; + uint64_t BootstrapLatencyMilliseconds = 0; + uint64_t BootstrapLatencySamples = 0; + uint64_t DroppedLifecycleEvents = 0; + uint32_t MaximumConnections = 0; + uint32_t CurrentCandidates = 0; + uint32_t MaximumCandidates = 0; + uint32_t CurrentSchedulerStates = 0; + uint32_t MaximumSchedulerStates = 0; + uint32_t LastCompletedPhase = 0; + }; + + extern UNetDriver* NetDriver; + + void Configure(uint32_t considerCacheMaxAgeMilliseconds); + void ConfigureLifecycleEventSink(LifecycleEventSink sink); + void BeginFrameNetworkLifecycle(UWorld* World); + void EndFrameNetworkLifecycle(UWorld* World); + void ResetWorldState(UWorld* World); + void ResetDriverState(UNetDriver* Driver); + + // Compatibility overloads keep the networking category independently + // buildable before the later engine-integration patch is applied. + bool Listen(UEngine* Engine, int Port); + bool Listen(UEngine* Engine, UWorld* World, int Port); + const std::string& GetLastListenError(); + + void TickNetworking(); + void TickNetworking(UWorld* World); + // Called immediately before the native UIpNetDriver transport dispatch pass. + // Synthetic startup can briefly leave only one side of the UWorld/NetDriver + // ownership pair populated; this restores a coherent pair before packets are + // allowed to reach FNetworkNotify. + bool PrepareNativeNetworkDispatch(UNetDriver* Driver); + void NotifyNativeNetworkDispatchCompleted(UNetDriver* Driver); + bool PrepareNativeNetworkFlush(UNetDriver* Driver); + void NotifyNativeNetworkFlushCompleted(UNetDriver* Driver); + void NotifyNativeEngineTickCompleted(UWorld* World); + void NotifyNativePostLogin(APlayerController* PlayerController); + void NotifyClientLoadedWorld(APlayerController* PlayerController, FName WorldPackageName); + void NotifyClientAcknowledgedPawn(APlayerController* PlayerController, APawn* Pawn); + void NotifyDamageRpcObserved(); + void NotifyUrgentDamageTarget(int32_t objectIndex, int32_t objectSerialNumber); + void NotifyBehemothInterruptClientRpc(); + void QueueUrgentActor(AActor* actor, UrgentReplicationReason reason, + APlayerController* targetController = nullptr); + void RecordCombatEvent(CombatEvent event, bool succeeded = false, uint32_t amount = 1); + void RecordCombatTextQueue(const FArchonMultitypeCombatTextEntry& entry); + void RecordCombatTextMulticast(uint32_t entryCount); + + const std::vector& GetLiveConnections(); + + ProfilingCounters TakeProfilingCounters(); +} diff --git a/UndauntedInternalServer/Networking/Private/ConnectionState.inl b/UndauntedInternalServer/Networking/Private/ConnectionState.inl new file mode 100644 index 0000000..a5a57bf --- /dev/null +++ b/UndauntedInternalServer/Networking/Private/ConnectionState.inl @@ -0,0 +1,379 @@ + static void ResetConsiderCache() { + ConsiderCache.clear(); + CandidateActorLookup.clear(); + PriorityScratch.clear(); + CachedLevelActorCounts.clear(); + ActorReplicationStates.clear(); + ConnectionBootstrapStates.clear(); + ConnectionScheduleStates.clear(); + CurrentSchedulerActorStates = 0; + CurrentConnections.clear(); + LiveConnections.clear(); + PendingConnectionEventCount = 0; + PendingUrgentDamageTargetCount = 0; + CurrentConnectionPass = 0; + CachedWorld = nullptr; + CachedWorldIdentity = {}; + LastConsiderCacheBuildMs = 0; + ConsiderCacheDirtyForNextConnection = true; + } + + static void QueueConnectionEvent(const ConnectionEvent& Event) { + if (PendingConnectionEventCount < PendingConnectionEvents.size()) { + PendingConnectionEvents[PendingConnectionEventCount++] = Event; + } + else { + ++ProfileCounters.DroppedLifecycleEvents; + } + } + + static void EmitPendingConnectionEvents() { + if (!LifecycleEventSinkEnabled()) { + PendingConnectionEventCount = 0; + return; + } + + for (size_t Index = 0; Index < PendingConnectionEventCount; ++Index) { + const ConnectionEvent& Event = PendingConnectionEvents[Index]; + const std::string CommonFields = + "\"connection\":" + PointerValue(Event.Connection) + + ",\"networkTick\":" + std::to_string(Event.NetworkTick) + + ",\"valueA\":" + std::to_string(Event.ValueA) + + ",\"valueB\":" + std::to_string(Event.ValueB) + + ",\"elapsedMs\":" + std::to_string(Event.ElapsedMilliseconds); + + if (Event.Type == ConnectionEventType::Open) { + RecordLifecycleEvent("connection_open", CommonFields); + } + else if (Event.Type == ConnectionEventType::Close) { + RecordLifecycleEvent("connection_close", CommonFields); + } + else if (Event.Type == ConnectionEventType::BootstrapStarted) { + RecordLifecycleEvent("connection_bootstrap_started", CommonFields); + } + else if (Event.Type == ConnectionEventType::BootstrapRestarted) { + RecordLifecycleEvent("connection_bootstrap_restarted", CommonFields); + } + else if (Event.Type == ConnectionEventType::LoadedWorldAccepted) { + RecordLifecycleEvent("loaded_world_accepted", CommonFields); + } + else if (Event.Type == ConnectionEventType::LoadedWorldMismatch) { + RecordLifecycleEvent("loaded_world_mismatch", CommonFields); + } + else if (Event.Type == ConnectionEventType::PossessionAcknowledged) { + RecordLifecycleEvent("possession_acknowledged", CommonFields); + } + else if (Event.Type == ConnectionEventType::CriticalActorDiscovered) { + RecordLifecycleEvent("bootstrap_critical_actor_discovered", CommonFields + + ",\"role\":\"" + InitializationRoleName(Event.ValueA) + "\""); + } + else if (Event.Type == ConnectionEventType::CriticalChannelAcknowledged) { + RecordLifecycleEvent("bootstrap_critical_channel_acknowledged", CommonFields + + ",\"role\":\"" + InitializationRoleName(Event.ValueA) + "\""); + } + else if (Event.Type == ConnectionEventType::BootstrapCompleted) { + RecordLifecycleEvent("connection_bootstrap_completed", CommonFields); + } + else if (Event.Type == ConnectionEventType::BootstrapDeadline) { + RecordLifecycleEvent("connection_bootstrap_deadline", CommonFields); + } + else { + const char* EventName = "native_unknown"; + switch (Event.Type) { + case ConnectionEventType::NativePostLogin: + EventName = "native_post_login"; break; + case ConnectionEventType::NativeReciprocalRepair: + EventName = "native_reciprocal_repair"; break; + case ConnectionEventType::NativeDispatchConflict: + EventName = "native_dispatch_conflict"; break; + default: + break; + } + RecordLifecycleEvent(EventName, CommonFields); + } + } + PendingConnectionEventCount = 0; + } + + static void RefreshConnectionState(UNetDriver* Driver, uint32_t CurrentTick) { + CurrentConnections.clear(); + LiveConnections.clear(); + + if (!Driver) + return; + + CurrentConnections.reserve(Driver->ClientConnections.Num()); + LiveConnections.reserve(Driver->ClientConnections.Num()); + + for (UNetConnection* Connection : Driver->ClientConnections) { + if (!IsOpenConnection(Connection)) + continue; + + CurrentConnections.insert(Connection); + LiveConnections.push_back(Connection); + + auto ScheduleIt = ConnectionScheduleStates.find(Connection); + if (ScheduleIt == ConnectionScheduleStates.end()) { + ConnectionScheduleStates.emplace(Connection, + MakeConnectionScheduleState(Connection, Driver->World)); + } + else if (!IdentityMatches(ScheduleIt->second.Connection, Connection) || + !IdentityMatches(ScheduleIt->second.World, Driver->World)) { + const uint32_t Removed = static_cast(ScheduleIt->second.Actors.size()); + CurrentSchedulerActorStates = Removed <= CurrentSchedulerActorStates + ? CurrentSchedulerActorStates - Removed + : 0; + ScheduleIt->second = MakeConnectionScheduleState(Connection, Driver->World); + } + else { + const ObjectIdentity ViewTargetIdentity = + TrackObject(Connection->ViewTarget); + if (!IdentitiesEqual(ScheduleIt->second.ViewTarget, + ViewTargetIdentity)) { + ScheduleIt->second.ViewTarget = ViewTargetIdentity; + for (auto& [Actor, State] : ScheduleIt->second.Actors) + State.NextRelevancyCheckMs = 0; + } + } + + auto StateIt = ConnectionBootstrapStates.find(Connection); + if (StateIt == ConnectionBootstrapStates.end()) { + ConnectionBootstrapStates.emplace(Connection, + MakeConnectionBootstrapState(Connection, Driver->World)); + ++ProfileCounters.BootstrapStarts; + QueueConnectionEvent({ ConnectionEventType::Open, Connection, CurrentTick, + static_cast(Driver->ClientConnections.Num()), + static_cast(LiveConnections.size()), 0 }); + QueueConnectionEvent({ ConnectionEventType::BootstrapStarted, Connection, CurrentTick, + 0, 0, 0 }); + continue; + } + + APlayerController* CurrentController = Connection->PlayerController; + if (!IdentityMatches(StateIt->second.Connection, Connection) || + !IdentityMatches(StateIt->second.World, Driver->World) || + !IdentityMatches(StateIt->second.Controller, CurrentController)) { + StateIt->second = MakeConnectionBootstrapState(Connection, Driver->World); + ++ProfileCounters.BootstrapRestarts; + QueueConnectionEvent({ ConnectionEventType::BootstrapRestarted, Connection, CurrentTick, + CurrentController ? 1u : 0u, 0, 0 }); + } + } + + for (auto It = ConnectionBootstrapStates.begin(); It != ConnectionBootstrapStates.end();) { + if (!CurrentConnections.contains(It->first)) { + QueueConnectionEvent({ ConnectionEventType::Close, It->first, CurrentTick, 0, 0, 0 }); + ClearConnectionScheduleState(It->first); + It = ConnectionBootstrapStates.erase(It); + } + else { + ++It; + } + } + } + + static UWorld* GetActorWorld(AActor* Actor) { + if (!Actor || !Actor->VTable) + return nullptr; + + return reinterpret_cast(*(void**)((uintptr_t)Actor->VTable + OffsetActorGetWorldVTable))(Actor); + } + + static bool IsActorReplicationCandidate(UWorld* World, AActor* Actor) { + return World + && Actor + && Actor->Class + && Actor->bReplicates + && Actor->RemoteRole != ENetRole::ROLE_None + && Actor->Role != ENetRole::ROLE_None + && !Actor->bActorIsBeingDestroyed + && (!NetDriver || Actor->NetDriverName.IsNone() || + Actor->NetDriverName == NetDriver->NetDriverName) + && GetActorWorld(Actor) == World; + } + + static __forceinline bool IsCachedActorStillReplicatable(AActor* Actor) { + return Actor + && Actor->Class + && Actor->bReplicates + && Actor->RemoteRole != ENetRole::ROLE_None + && Actor->Role != ENetRole::ROLE_None + && !Actor->bActorIsBeingDestroyed; + } + + static int CountWorldActors(UWorld* World) { + int TotalActorCount = 0; + + for (ULevel* Level : World->Levels) { + if (Level) + TotalActorCount += Level->Actors.Num(); + } + + return TotalActorCount; + } + + static bool GetCacheRebuildReason( + UWorld* World, + CacheRebuildReason& Reason + ) { + if (!World) + return false; + + if (CachedWorld != World) { + Reason = CacheRebuildReason::WorldChanged; + return true; + } + + if (!IsStableIdentity(CachedWorldIdentity, CachedWorld)) { + Reason = CacheRebuildReason::WorldIdentity; + return true; + } + + if (GetTickCount64() - LastConsiderCacheBuildMs >= ConsiderCacheMaxAgeMs) { + Reason = CacheRebuildReason::MaximumAge; + return true; + } + + if (CachedLevelActorCounts.size() != World->Levels.Num()) { + Reason = CacheRebuildReason::LevelCount; + return true; + } + + for (int i = 0; i < World->Levels.Num(); i++) { + ULevel* Level = World->Levels[i]; + int ActorCount = Level ? Level->Actors.Num() : -1; + + if (CachedLevelActorCounts[i] != ActorCount) { + Reason = CacheRebuildReason::ActorCount; + return true; + } + } + + return false; + } + + static __forceinline float VectorDistanceSquared(const FVector& A, const FVector& B) { + float X = A.X - B.X; + float Y = A.Y - B.Y; + float Z = A.Z - B.Z; + + return X * X + Y * Y + Z * Z; + } + + static __forceinline float RotationDelta(const FRotator& A, const FRotator& B) { + float Pitch = A.Pitch > B.Pitch ? A.Pitch - B.Pitch : B.Pitch - A.Pitch; + float Yaw = A.Yaw > B.Yaw ? A.Yaw - B.Yaw : B.Yaw - A.Yaw; + float Roll = A.Roll > B.Roll ? A.Roll - B.Roll : B.Roll - A.Roll; + float MaxPitchYaw = Pitch > Yaw ? Pitch : Yaw; + + return MaxPitchYaw > Roll ? MaxPitchYaw : Roll; + } + + static __forceinline TransformSample SampleActorTransform(AActor* Actor) { + TransformSample Sample{}; + + if (Actor->RootComponent) { + Sample.Location = Actor->RootComponent->RelativeLocation; + Sample.Rotation = Actor->RootComponent->RelativeRotation; + } + else { + Sample.Location = Actor->ReplicatedMovement.Location; + Sample.Rotation = Actor->ReplicatedMovement.Rotation; + } + + Sample.Valid = true; + return Sample; + } + + static __forceinline bool HasAnyMovementSignal(AActor* Actor, const TransformSample& Sample) { + return !Sample.Location.IsZero() + || !Actor->ReplicatedMovement.LinearVelocity.IsZero() + || !Actor->ReplicatedMovement.AngularVelocity.IsZero(); + } + + static uint32_t ClassifyReplicationClass( + AActor* Actor, + const ReplicationClassCache& ClassCache + ) { + uint32_t Flags = 0; + if (Actor->IsA(ClassCache.PlayerController)) Flags |= ClassPlayerController; + if (Actor->IsA(ClassCache.ArchonCharacter)) Flags |= ClassArchonCharacter; + if (Actor->IsA(ClassCache.ArchonBehemoth)) Flags |= ClassArchonBehemoth; + if (Actor->IsA(ClassCache.ArchonLantern)) Flags |= ClassArchonLantern; + if (Actor->IsA(ClassCache.Projectile)) Flags |= ClassProjectile; + if (Actor->IsA(ClassCache.Pawn)) Flags |= ClassPawn; + if (Actor->IsA(ClassCache.ArchonEquipment)) Flags |= ClassArchonEquipment; + if (Actor->IsA(ClassCache.ArchonLoadout)) Flags |= ClassArchonLoadout; + if (Actor->IsA(ClassCache.ArchonAoe)) Flags |= ClassArchonAoe; + if (Actor->IsA(ClassCache.ArchonBeam)) Flags |= ClassArchonBeam; + if (Actor->IsA(ClassCache.AbilityActor)) Flags |= ClassAbilityActor; + return Flags; + } + + static ReplicationBucket ClassifyReplicationBucket( + AActor* Actor, + uint32_t ClassFlags, + const TransformSample& Sample + ) { + if ((ClassFlags & (ClassPlayerController | ClassArchonLantern)) != 0 || + !Actor->bReplicateMovement) + return ReplicationBucket::Immediate; + + if ((ClassFlags & ClassArchonCharacter) != 0) + return ReplicationBucket::ArchonCharacter; + + if ((ClassFlags & ClassArchonBehemoth) != 0) + return ReplicationBucket::ArchonBehemoth; + + if ((ClassFlags & ClassProjectile) != 0) + return ReplicationBucket::Projectile; + + if ((ClassFlags & ClassPawn) != 0) + return ReplicationBucket::PawnOrCharacter; + + return HasAnyMovementSignal(Actor, Sample) + ? ReplicationBucket::MovementActor + : ReplicationBucket::Immediate; + } + + static bool IsCriticalReplicationActor(uint32_t ClassFlags) { + if (!kAlwaysReplicateCriticalActors) + return false; + + return (ClassFlags & (ClassPlayerController | + ClassArchonCharacter | ClassArchonLantern)) != 0; + } + + static bool IsLatencySensitiveOwnedActor(uint32_t ClassFlags) { + return (ClassFlags & ( + ClassArchonEquipment | + ClassArchonLoadout | + ClassArchonAoe | + ClassArchonBeam | + ClassAbilityActor)) != 0; + } + + static bool IsChannelOpenAcknowledged(UActorChannel* Channel); + static uint32_t CurrentNetworkTick(); + + static void PruneReplicationState(UWorld* World) { + std::erase_if(ActorReplicationStates, + [World](const auto& Entry) { + return !IsStableIdentity(Entry.second.Identity, Entry.first) || + !IsActorReplicationCandidate(World, Entry.first); + }); + } + + static void PruneSchedulerState(UWorld* World) { + for (auto& [Connection, Schedule] : ConnectionScheduleStates) { + const size_t Removed = std::erase_if(Schedule.Actors, + [World](const auto& Entry) { + return !IsStableIdentity(Entry.second.Actor, Entry.first) || + !IsActorReplicationCandidate(World, Entry.first); + }); + CurrentSchedulerActorStates = Removed <= CurrentSchedulerActorStates + ? CurrentSchedulerActorStates - static_cast(Removed) + : 0; + ProfileCounters.SchedulerStatePrunes += Removed; + } +} diff --git a/UndauntedInternalServer/Networking/Private/ConsiderCache.inl b/UndauntedInternalServer/Networking/Private/ConsiderCache.inl new file mode 100644 index 0000000..abd0ad9 --- /dev/null +++ b/UndauntedInternalServer/Networking/Private/ConsiderCache.inl @@ -0,0 +1,708 @@ + static void RebuildConsiderCache( + UWorld* World, + UNetDriver* Driver, + const ReplicationClassCache& ClassCache, + CacheRebuildReason Reason + ) { + const int64_t TimingStartedAt = PerformanceTimestamp(); + const bool CaptureActorPropertyDistribution = ActorPropertyDistributionDirty; + ++ProfileCounters.CacheRebuilds; + ++ProfileCounters.CacheRebuildsByReason[static_cast(Reason)]; + if (!World || !Driver) { + ResetConsiderCache(); + ProfileCounters.CacheRebuildMicroseconds += ElapsedMicroseconds(TimingStartedAt); + ++ProfileCounters.CacheRebuildTimingSamples; + return; + } + + int TotalActorCount = CountWorldActors(World); + + ConsiderCache.clear(); + CandidateActorLookup.clear(); + CachedLevelActorCounts.clear(); + ConsiderCache.reserve(TotalActorCount); + CandidateActorLookup.reserve(static_cast(TotalActorCount)); + if (PriorityScratch.capacity() < static_cast(TotalActorCount)) + PriorityScratch.reserve(TotalActorCount); + ActorReplicationStates.reserve(TotalActorCount); + CachedLevelActorCounts.reserve(World->Levels.Num()); + PruneReplicationState(World); + PruneSchedulerState(World); + for (auto& [Connection, Schedule] : ConnectionScheduleStates) { + if (Schedule.Actors.bucket_count() < ConsiderCache.capacity()) + Schedule.Actors.reserve((std::min)( + MaximumScheduledActorsPerConnection, ConsiderCache.capacity())); + } + + for (ULevel* Level : World->Levels) { + CachedLevelActorCounts.push_back(Level ? Level->Actors.Num() : -1); + + if (!Level) + continue; + + for (AActor* Actor : Level->Actors) { + if (!Actor || !Actor->Class) + continue; + if (CaptureActorPropertyDistribution) { + const size_t LocalRole = (std::min)( + static_cast(Actor->Role), NetRoleCount - 1); + const size_t RemoteRole = (std::min)( + static_cast(Actor->RemoteRole), NetRoleCount - 1); + ++ProfileCounters.NetRoleMatrix[ + LocalRole * NetRoleCount + RemoteRole]; + } + if (!Actor->bReplicates) { + if (CaptureActorPropertyDistribution) + ++ProfileCounters.ExcludedNotReplicated; + continue; + } + if (Actor->RemoteRole == ENetRole::ROLE_None) { + if (CaptureActorPropertyDistribution) + ++ProfileCounters.ExcludedRemoteRoleNone; + continue; + } + if (Actor->Role == ENetRole::ROLE_None) { + if (CaptureActorPropertyDistribution) + ++ProfileCounters.ExcludedLocalRoleNone; + continue; + } + if (CaptureActorPropertyDistribution && + Actor->Role != ENetRole::ROLE_Authority) + ++ProfileCounters.UnexpectedLocalRole; + if (Actor->bActorIsBeingDestroyed) { + if (CaptureActorPropertyDistribution) + ++ProfileCounters.ExcludedDestroying; + continue; + } + if (GetActorWorld(Actor) != World) { + if (CaptureActorPropertyDistribution) + ++ProfileCounters.ExcludedWrongWorld; + continue; + } + if (!Actor->NetDriverName.IsNone() && + Actor->NetDriverName != Driver->NetDriverName) { + ++ProfileCounters.LivePolicyDriverMismatches; + continue; + } + + TransformSample Sample = SampleActorTransform(Actor); + const uint32_t ClassFlags = + ClassifyReplicationClass(Actor, ClassCache); + const bool IsPlayerController = + (ClassFlags & ClassPlayerController) != 0; + ReplicationBucket Bucket = + ClassifyReplicationBucket(Actor, ClassFlags, Sample); + const bool IsCritical = + IsCriticalReplicationActor(ClassFlags); + if (CaptureActorPropertyDistribution) + RecordActorPropertyDistribution(Actor, Bucket, IsCritical); + const ObjectIdentity ActorIdentity = TrackObject(Actor); + if (ResolveObject(ActorIdentity) != Actor) { + ++ProfileCounters.SchedulerActorIdentityResets; + ConsiderCacheDirtyForNextConnection = true; + continue; + } + auto [StateIt, StateInserted] = ActorReplicationStates.try_emplace(Actor); + ActorReplicationState& State = StateIt->second; + if (!StateInserted && (!IsStableIdentity(ActorIdentity, Actor) || + !IdentitiesEqual(State.Identity, ActorIdentity))) { + State = {}; + ++ProfileCounters.SchedulerActorIdentityResets; + } + State.Identity = ActorIdentity; + + ConsiderCache.push_back({ Actor, Bucket, ClassFlags, IsPlayerController, + IsCritical, &State }); + CandidateActorLookup[Actor] = &ConsiderCache.back(); + } + } + + CachedWorld = World; + CachedWorldIdentity = TrackObject(World); + // Rebuild after the vector is complete; vector growth invalidates the + // addresses stored in the urgent candidate lookup. + CandidateActorLookup.clear(); + CandidateActorLookup.reserve(ConsiderCache.size()); + for (ReplicationCandidate& Candidate : ConsiderCache) + CandidateActorLookup[Candidate.Actor] = &Candidate; + LastConsiderCacheBuildMs = GetTickCount64(); + ProfileCounters.CurrentCandidates = static_cast(ConsiderCache.size()); + ProfileCounters.MaximumCandidates = (std::max)(ProfileCounters.MaximumCandidates, + ProfileCounters.CurrentCandidates); + ProfileCounters.CacheRebuildMicroseconds += ElapsedMicroseconds(TimingStartedAt); + ++ProfileCounters.CacheRebuildTimingSamples; + ActorPropertyDistributionDirty = false; + } + + static void BuildReplicateList(uint32_t CurrentTick) { + for (ReplicationCandidate& Candidate : ConsiderCache) { + AActor* Actor = Candidate.Actor; + + if (!Candidate.ReplicationState || + ResolveObject(Candidate.ReplicationState->Identity) != Actor) { + ConsiderCacheDirtyForNextConnection = true; + ++ProfileCounters.SchedulerActorIdentityResets; + continue; + } + if (!IsCachedActorStillReplicatable(Actor)) + continue; + + Candidate.Bucket = ClassifyReplicationBucket( + Actor, Candidate.ClassFlags, SampleActorTransform(Actor)); + if (Candidate.Bucket == ReplicationBucket::Immediate) { + ActorReplicationState& State = *Candidate.ReplicationState; + if (State.ImmediatePassNetworkTick != CurrentTick) { + State.ImmediatePassNetworkTick = CurrentTick; + State.ImmediatePassAttempts = 0; + State.ImmediatePassSuccesses = 0; + } + } + + } + } + + static void FinalizeImmediatePassMetrics(uint32_t CurrentTick) { + for (ReplicationCandidate& Candidate : ConsiderCache) { + if (Candidate.Bucket != ReplicationBucket::Immediate || !Candidate.ReplicationState) + continue; + + ActorReplicationState& State = *Candidate.ReplicationState; + if (State.ImmediatePassNetworkTick != CurrentTick || + State.ImmediatePassFinalizedTick == CurrentTick || + State.ImmediatePassAttempts == 0) { + continue; + } + + uint16_t MetricSlot = ResolveImmediateMetricSlot( + Candidate.Actor, Candidate.ReplicationState); + if (MetricSlot >= ProfileCounters.ImmediateClasses.size()) + MetricSlot = ImmediateMetricInvalidSlot; + ImmediateClassCounters& Counters = ProfileCounters.ImmediateClasses[MetricSlot]; + ++Counters.ActorPasses; + Counters.ActorPassAttempts += State.ImmediatePassAttempts; + Counters.ActorPassSuccesses += State.ImmediatePassSuccesses; + + if (State.ImmediatePassSuccesses > 0) { + ++Counters.ActorPassesWithAnyData; + State.ConsecutiveImmediateNoDataPasses = 0; + } + else { + ++Counters.ActorPassesWithNoData; + if (State.ConsecutiveImmediateNoDataPasses < + (std::numeric_limits::max)()) { + ++State.ConsecutiveImmediateNoDataPasses; + } + ++Counters.NoDataPassStreaks[ + ImmediateNoDataBand(State.ConsecutiveImmediateNoDataPasses)]; + } + State.ImmediatePassFinalizedTick = CurrentTick; + } + } + + static UNetDriver* FindNetDriverByName(UEngine* Engine, UWorld* World, + const FName& DriverName) { + if (!Engine || !World || BaseAddress == 0) + return nullptr; + FindNamedNetDriverFn FindNamedNetDriver = + reinterpret_cast(BaseAddress + OffsetFindNamedNetDriver); + return FindNamedNetDriver(Engine, World, DriverName); + } + + static bool IsOwnedByConnection( + UWorld* World, + UNetConnection* Connection, + AActor* Actor + ) { + if (!World || !Connection || !Actor) + return false; + + APlayerController* Controller = Connection->PlayerController; + AActor* Pawn = Controller ? Controller->Pawn : nullptr; + AActor* PlayerState = Controller ? Controller->PlayerState : nullptr; + AActor* Current = Actor; + for (size_t Depth = 0; Current && Depth <= MaximumOwnerChainDepth; ++Depth) { + if (Current == Connection->OwningActor || Current == Connection->ViewTarget || + Current == Controller || Current == Pawn || Current == PlayerState) { + return true; + } + + const ObjectIdentity Identity = TrackObject(Current); + if (ResolveObject(Identity) != Current || + Current->bActorIsBeingDestroyed || + GetActorWorld(Current) != World) { + return false; + } + Current = Current->Owner; + } + return false; + } + + static bool IsDirectConnectionActor( + UNetConnection* Connection, + AActor* Actor + ) { + if (!Connection || !Actor) + return false; + APlayerController* Controller = Connection->PlayerController; + return Actor == Connection->OwningActor || + Actor == Connection->ViewTarget || + Actor == Controller || + (Controller && + (Actor == Controller->Pawn || Actor == Controller->PlayerState)); + } + + static ActorScheduleState* ResolveSchedulingState( + UWorld* World, + UNetConnection* Connection, + AActor* Actor, + ActorReplicationState* ReplicationState + ) { + if (!World || !Connection || !Actor || !ReplicationState || + ResolveObject(ReplicationState->Identity) != Actor) { + ++ProfileCounters.LivePolicyActorIdentityFallbacks; + return nullptr; + } + + auto ConnectionIt = ConnectionScheduleStates.find(Connection); + if (ConnectionIt == ConnectionScheduleStates.end() || + !IsStableIdentity(ConnectionIt->second.Connection, Connection) || + !IsStableIdentity(ConnectionIt->second.World, World)) { + ++ProfileCounters.LivePolicyConnectionIdentityFallbacks; + return nullptr; + } + + AActor* const Owner = Actor->Owner; + const ObjectIdentity OwnerIdentity = TrackObject(Owner); + const bool OwnerTrackable = !Owner || + ResolveObject(OwnerIdentity) == Owner; + if (!OwnerTrackable) { + ++ProfileCounters.LivePolicyUntrackableOwners; + const uint16_t Slot = ResolveImmediateMetricSlot( + Actor, ReplicationState); + ++ProfileCounters.ImmediateClasses[Slot].UntrackableOwnerCalls; + } + + auto& ActorStates = ConnectionIt->second.Actors; + auto ActorIt = ActorStates.find(Actor); + if (ActorIt == ActorStates.end()) { + if (ActorStates.size() >= MaximumScheduledActorsPerConnection) { + ++ProfileCounters.LivePolicyStateCapacityFallbacks; + ++ProfileCounters.SchedulerStateCapacityDrops; + return nullptr; + } + ActorScheduleState NewState{}; + NewState.Actor = ReplicationState->Identity; + NewState.OwnerPointer = Owner; + NewState.OwnerTrackable = OwnerTrackable; + NewState.Owner = OwnerTrackable ? OwnerIdentity : ObjectIdentity{}; + ActorIt = ActorStates.emplace(Actor, NewState).first; + ++CurrentSchedulerActorStates; + ++ProfileCounters.SchedulerStateInsertions; + ProfileCounters.MaximumSchedulerStates = (std::max)( + ProfileCounters.MaximumSchedulerStates, + CurrentSchedulerActorStates); + } + else if (!IdentitiesEqual(ActorIt->second.Actor, ReplicationState->Identity)) { + ActorIt->second = {}; + ActorIt->second.Actor = ReplicationState->Identity; + ActorIt->second.OwnerPointer = Owner; + ActorIt->second.OwnerTrackable = OwnerTrackable; + ActorIt->second.Owner = + OwnerTrackable ? OwnerIdentity : ObjectIdentity{}; + ++ProfileCounters.SchedulerActorIdentityResets; + } + else if (ActorIt->second.OwnerPointer != Owner || + ActorIt->second.OwnerTrackable != OwnerTrackable || + (OwnerTrackable && + !IdentitiesEqual(ActorIt->second.Owner, OwnerIdentity))) { + ActorIt->second = {}; + ActorIt->second.Actor = ReplicationState->Identity; + ActorIt->second.OwnerPointer = Owner; + ActorIt->second.OwnerTrackable = OwnerTrackable; + ActorIt->second.Owner = + OwnerTrackable ? OwnerIdentity : ObjectIdentity{}; + ++ProfileCounters.SchedulerOwnerChanges; + } + return &ActorIt->second; + } + + static FVector ViewerLocation(UNetConnection* Connection) { + AActor* ViewTarget = Connection ? Connection->ViewTarget : nullptr; + if (!ViewTarget && Connection) + ViewTarget = Connection->PlayerController; + if (!ViewTarget) + return {}; + if (ViewTarget->RootComponent) + return ViewTarget->RootComponent->RelativeLocation; + return ViewTarget->ReplicatedMovement.Location; + } + + static bool IsRelevantForConnection( + UWorld* World, + UNetConnection* Connection, + AActor* Actor + ) { + if (!World || !Connection || !Actor) + return false; + if (Actor->bAlwaysRelevant) + return true; + + AActor* RealViewer = Connection->PlayerController + ? static_cast(Connection->PlayerController) + : Connection->OwningActor; + AActor* ViewTarget = Connection->ViewTarget ? Connection->ViewTarget : RealViewer; + + // Login can briefly precede controller/view-target creation. Omitting + // actors in that interval is less safe than one conservative pass. + if (!RealViewer || !ViewTarget) { + ++ProfileCounters.LivePolicyNativeRelevancyFallbacks; + return !Actor->bOnlyRelevantToOwner || + IsOwnedByConnection(World, Connection, Actor); + } + + const ObjectIdentity RealViewerIdentity = TrackObject(RealViewer); + const ObjectIdentity ViewTargetIdentity = TrackObject(ViewTarget); + if (ResolveObject(RealViewerIdentity) != RealViewer || + ResolveObject(ViewTargetIdentity) != ViewTarget) { + ++ProfileCounters.LivePolicyNativeRelevancyFallbacks; + return true; + } + + if (NativeIsNetRelevantFor) { + const FVector SourceLocation = ViewerLocation(Connection); + ++ProfileCounters.LivePolicyNativeRelevancyCalls; + return NativeIsNetRelevantFor( + Actor, RealViewer, ViewTarget, &SourceLocation); + } + + ++ProfileCounters.LivePolicyNativeRelevancyFallbacks; + if (Actor->bOnlyRelevantToOwner || Actor->bNetUseOwnerRelevancy) + return IsOwnedByConnection(World, Connection, Actor); + return true; + } + + static float EffectiveFrequency( + const AActor* Actor, + uint8_t NoDataCalls, + float AdaptiveFloorHz = 0.0f + ) { + float Frequency = Actor ? Actor->NetUpdateFrequency : SchedulerMaximumFrequencyHz; + if (!std::isfinite(Frequency) || Frequency <= 0.0f) + Frequency = SchedulerMaximumFrequencyHz; + Frequency = (std::clamp)( + Frequency, 0.1f, SchedulerMaximumFrequencyHz); + + if (Actor && NoDataCalls >= AdaptiveNoDataThreshold && + std::isfinite(Actor->MinNetUpdateFrequency) && + Actor->MinNetUpdateFrequency > 0.0f) { + const float Minimum = (std::clamp)( + Actor->MinNetUpdateFrequency, + 0.1f, + SchedulerMaximumFrequencyHz); + const float BoundedMinimum = AdaptiveFloorHz > 0.0f + ? (std::max)(Minimum, AdaptiveFloorHz) + : Minimum; + Frequency = (std::min)(Frequency, BoundedMinimum); + } + return Frequency; + } + + static LivePolicyResult EvaluateLivePolicy( + UWorld* World, + UNetConnection* Connection, + AActor* Actor, + ActorReplicationState* ReplicationState, + UActorChannel* ExistingChannel, + bool BypassFrequency + ) { + LivePolicyResult Result{}; + ActorScheduleState* State = ResolveSchedulingState( + World, Connection, Actor, ReplicationState); + if (!State) { + // Identity/capacity uncertainty must never turn into a stale-history + // skip. Replicate conservatively when the live actor itself is valid. + ++ProfileCounters.LivePolicyIdentityFallbacks; + Result.Decision = LivePolicyDecision::Eligible; + return Result; + } + Result.State = State; + State->LastConsideredAtMs = GetTickCount64(); + + if (!Actor->NetDriverName.IsNone() && + Actor->NetDriverName != NetDriver->NetDriverName) { + ++ProfileCounters.LivePolicyDriverMismatches; + Result.Decision = LivePolicyDecision::DriverMismatch; + return Result; + } + if (State->TemporaryRetired) { + Result.Decision = LivePolicyDecision::TemporaryRetired; + return Result; + } + if (State->TearOffRetired) { + Result.Decision = LivePolicyDecision::TearOffRetired; + return Result; + } + if (!ExistingChannel && Actor->NetDormancy != ENetDormancy::DORM_Awake && + Actor->NetDormancy != ENetDormancy::DORM_Never) { + ++ProfileCounters.LivePolicyDormantDeferrals; + Result.Decision = LivePolicyDecision::Dormant; + return Result; + } + + const bool OwnerSensitive = + Actor->bOnlyRelevantToOwner || Actor->bNetUseOwnerRelevancy; + if (OwnerSensitive && Actor->Owner && !State->OwnerTrackable && + !ExistingChannel && !IsDirectConnectionActor(Connection, Actor)) { + ++ProfileCounters.LivePolicyOwnerSensitiveDeferrals; + const uint16_t Slot = ResolveImmediateMetricSlot( + Actor, ReplicationState); + ++ProfileCounters.ImmediateClasses[Slot].OwnerSensitiveDeferrals; + Result.Decision = LivePolicyDecision::NotRelevant; + return Result; + } + if (!ExistingChannel && (Actor->bNetStartup || Actor->bNetLoadOnClient)) + ++ProfileCounters.LivePolicyLevelInitializationUnavailable; + + const TransformSample CurrentSample = SampleActorTransform(Actor); + const bool DormancyChanged = State->LastDormancy != Actor->NetDormancy; + Result.Moved = Actor->bReplicateMovement && + (!State->LastSample.Valid || + VectorDistanceSquared(CurrentSample.Location, State->LastSample.Location) > 0.01f || + RotationDelta(CurrentSample.Rotation, State->LastSample.Rotation) > 0.01f); + if (DormancyChanged || Result.Moved) { + State->ConsecutiveNoDataCalls = 0; + State->NextFrequencyDeadlineMs = 0; + State->NextRelevancyCheckMs = 0; + } + State->LastDormancy = Actor->NetDormancy; + State->LastSample = CurrentSample; + + const ULONGLONG NowMs = State->LastConsideredAtMs; + if (!BypassFrequency && State->RelevancyKnown && !State->LastRelevant && + NowMs < State->NextRelevancyCheckMs) { + ++ProfileCounters.LivePolicyIrrelevantSkips; + const uint16_t Slot = ResolveImmediateMetricSlot( + Actor, ReplicationState); + ++ProfileCounters.ImmediateClasses[Slot].IrrelevantSkips; + Result.Decision = LivePolicyDecision::NotRelevant; + return Result; + } + if (!BypassFrequency && ExistingChannel && + State->NextFrequencyDeadlineMs != 0 && + NowMs < State->NextFrequencyDeadlineMs) { + ++ProfileCounters.LivePolicyNotDue; + Result.Decision = LivePolicyDecision::NotDue; + return Result; + } + if (!BypassFrequency && ExistingChannel) + ++ProfileCounters.LivePolicyDue; + + const bool MustCheckRelevancy = + BypassFrequency || !ExistingChannel || OwnerSensitive || + Actor->bReplicateMovement || !State->RelevancyKnown || + NowMs >= State->NextRelevancyCheckMs; + if (MustCheckRelevancy) { + ++ProfileCounters.LivePolicyRelevancyAudits; + if (State->RelevancyKnown && !State->LastRelevant && + State->LastRelevancyCheckMs && NowMs >= State->LastRelevancyCheckMs) { + ProfileCounters.LivePolicyRelevancyRecheckMilliseconds += + NowMs - State->LastRelevancyCheckMs; + ++ProfileCounters.LivePolicyRelevancyRecheckSamples; + } + const bool Relevant = + OwnerSensitive && Actor->Owner && !State->OwnerTrackable + ? true + : IsRelevantForConnection(World, Connection, Actor); + if (OwnerSensitive && Actor->Owner && !State->OwnerTrackable) + ++ProfileCounters.LivePolicyNativeRelevancyFallbacks; + State->RelevancyKnown = true; + State->LastRelevant = Relevant; + State->LastRelevancyCheckMs = NowMs; + State->NextRelevancyCheckMs = NowMs + + (Relevant && !OwnerSensitive && !Actor->bReplicateMovement + ? StaticRelevancyAuditMs + : IrrelevantRecheckMs); + if (!Relevant) { + ++ProfileCounters.LivePolicyNotRelevant; + Result.Decision = LivePolicyDecision::NotRelevant; + return Result; + } + ++ProfileCounters.LivePolicyRelevant; + } + + ++ProfileCounters.LivePolicyEligible; + Result.Decision = LivePolicyDecision::Eligible; + return Result; + } + + static void FinishLivePolicy( + AActor* Actor, + UActorChannel* Channel, + ActorScheduleState* State, + bool Attempted, + bool ProducedData, + float AdaptiveFloorHz = 0.0f + ) { + if (!Actor || !State || !Attempted) + return; + + const ULONGLONG NowMs = GetTickCount64(); + State->LastAttemptAtMs = NowMs; + if (ProducedData) { + State->LastSuccessfulDataAtMs = NowMs; + State->ConsecutiveNoDataCalls = 0; + if (Actor->bTearOff) { + State->TearOffRetired = true; + ++ProfileCounters.LivePolicyTearOffRetirements; + } + } + else if (State->ConsecutiveNoDataCalls < + (std::numeric_limits::max)()) { + ++State->ConsecutiveNoDataCalls; + } + + if (Actor->bNetTemporary && Channel && + IsChannelOpenAcknowledged(Channel)) { + State->TemporaryRetired = true; + ++ProfileCounters.LivePolicyTemporaryRetirements; + } + + const float Frequency = EffectiveFrequency( + Actor, State->ConsecutiveNoDataCalls, AdaptiveFloorHz); + State->NextFrequencyDeadlineMs = NowMs + static_cast( + std::ceil(1000.0f / Frequency)); + } + + static float ComputeLivePriority( + UNetConnection* Connection, + AActor* Actor, + ActorScheduleState* State, + UActorChannel* ExistingChannel + ) { + float BasePriority = Actor && std::isfinite(Actor->NetPriority) + ? (std::clamp)(Actor->NetPriority, 0.1f, 100.0f) + : 1.0f; + const ULONGLONG NowMs = GetTickCount64(); + const ULONGLONG LastSuccess = State ? State->LastSuccessfulDataAtMs : 0; + const float AgeSeconds = LastSuccess && NowMs > LastSuccess + ? static_cast(NowMs - LastSuccess) / 1000.0f + : 1.0f; + float Priority = BasePriority * (1.0f + (std::min)(AgeSeconds, 4.0f)); + if (!ExistingChannel) Priority += 1000.0f; + if (Actor && Actor->bReplicateMovement) Priority += 100.0f; + if (Actor && (IsDirectConnectionActor(Connection, Actor) || + (State && State->OwnerTrackable && + IsOwnedByConnection(NetDriver->World, Connection, Actor)))) + Priority += 500.0f; + return Priority; + } + + static size_t PriorityBand(float Priority) { + if (!std::isfinite(Priority) || Priority <= 1.0f) return 0; + if (Priority <= 10.0f) return 1; + if (Priority <= 100.0f) return 2; + if (Priority <= 1000.0f) return 3; + return 4; + } + + static bool ReplicateActorForConnection( + UNetConnection* Connection, + AActor* Actor, + bool IsPlayerController, + FName& ChannelName, + CreateChannelByNameFn CreateChannelByName, + SetChannelActorFn SetChannelActor, + ReplicateActorFn ReplicateActor, + UpdateCameraFn UpdateCamera, + ReplicationBucket Bucket, + bool IsBootstrapReplication, + ActorReplicationState* ReplicationState, + bool* ReplicationAttempted = nullptr + ) { + if (ReplicationAttempted) + *ReplicationAttempted = false; + if (!Connection || !Actor || !ReplicationState || + ResolveObject(ReplicationState->Identity) != Actor) { + ConsiderCacheDirtyForNextConnection = true; + return false; + } + + const uint32_t NetworkTick = CurrentNetworkTick(); + if (ReplicationState->LastPreReplicationTick != NetworkTick) { + if (!NativeCallActorPreReplication || !NetDriver) { + ++ProfileCounters.PreReplicationUnavailable; + return false; + } + + NativeCallActorPreReplication(Actor, NetDriver); + ReplicationState->LastPreReplicationTick = NetworkTick; + ++ProfileCounters.PreReplicationCalls; + + // PreReplication is arbitrary native/game code. Match Unreal's + // safety boundary by revalidating the actor before touching a + // channel after the callback returns. + if (ResolveObject(ReplicationState->Identity) != Actor || + Actor->bActorIsBeingDestroyed || !Actor->bReplicates) { + ++ProfileCounters.PreReplicationInvalidatedActors; + ConsiderCacheDirtyForNextConnection = true; + return false; + } + } + + if (IsPlayerController) { + if (Actor != Connection->OwningActor) + return false; + + APlayerController* PlayerController = static_cast(Actor); + Connection->ViewTarget = PlayerController->GetViewTarget(); + UpdateCamera(PlayerController); + } + + UActorChannel* ActorChannel = nullptr; + auto ActorChannelIt = ScratchActorChannels.find(Actor); + + if (ActorChannelIt != ScratchActorChannels.end()) { + ActorChannel = ActorChannelIt->second; + ++ProfileCounters.ActorChannelsReused; + } + else { + ActorChannel = CreateChannelByName(Connection, &ChannelName, OpenChannelFlag, -1); + + if (ActorChannel) { + SetChannelActor(ActorChannel, Actor, 0); + ScratchActorChannels[Actor] = ActorChannel; + ++ProfileCounters.ActorChannelsCreated; + } + } + + if (!ActorChannel || !ActorChannel->Actor) + return false; + + int& ReplicationFlags = *reinterpret_cast((uintptr_t)ActorChannel + OffsetActorChannelReplicationFlags); + + if (!(ReplicationFlags & ReplicationFlagNeedsTick)) + ReplicationFlags |= ReplicationFlagNeedsTick; + + ++ProfileCounters.ChannelStateSamples; + if (IsChannelOpenAcknowledged(ActorChannel)) + ++ProfileCounters.ChannelOpenAcknowledgedSamples; + else + ++ProfileCounters.ChannelNotOpenAcknowledgedSamples; + + ++ProfileCounters.ReplicationAttempts; + ++ProfileCounters.ReplicationAttemptsByBucket[static_cast(Bucket)]; + if (IsBootstrapReplication) + ++ProfileCounters.BootstrapReplicationAttempts; + if (ReplicationAttempted) + *ReplicationAttempted = true; + const bool ProducedData = ReplicateActor(ActorChannel); + RecordClassReplicationResult(Actor, ReplicationState, Bucket, ProducedData); + if (ProducedData) { + ++ProfileCounters.ReplicationSuccesses; + ++ProfileCounters.ReplicationSuccessesByBucket[static_cast(Bucket)]; + if (IsBootstrapReplication) + ++ProfileCounters.BootstrapReplicationSuccesses; + } + + return ProducedData; +} diff --git a/UndauntedInternalServer/Networking/Private/ListenDispatch.inl b/UndauntedInternalServer/Networking/Private/ListenDispatch.inl new file mode 100644 index 0000000..ebdf927 --- /dev/null +++ b/UndauntedInternalServer/Networking/Private/ListenDispatch.inl @@ -0,0 +1,690 @@ + bool Listen(UEngine* Engine, int Port) { + return Listen(Engine, UWorld::GetWorld(), Port); + } + + const std::string& GetLastListenError() { + return LastListenError; + } + + bool Listen(UEngine* Engine, UWorld* World, int Port) { + BaseAddress = (uintptr_t)GetModuleHandleA(nullptr); + LastListenError.clear(); + NativeDispatchOwnershipMismatchLogged = false; + + const auto FailListen = [](const std::string& Reason) { + LastListenError = Reason; + std::cout << "Listen failed: " << Reason << std::endl; + return false; + }; + + if (!Engine || !World) + return FailListen("engine or world missing"); + + constexpr uint8_t IsNetRelevantForSignature[] = { + 0x48, 0x89, 0x5C, 0x24, 0x18, 0x56, 0x57, 0x41, + 0x56, 0x48, 0x83, 0xEC, 0x70, 0x48, 0x8B, 0xF2 + }; + constexpr uint8_t CallPreReplicationSignature[] = { + 0x48, 0x85, 0xD2, 0x0F, 0x84, 0xB1, 0x01, 0x00, + 0x00, 0x53, 0x55, 0x48, 0x83, 0xEC, 0x38 + }; + const uintptr_t IsNetRelevantForAddress = + BaseAddress + OffsetActorIsNetRelevantFor; + MEMORY_BASIC_INFORMATION RelevancyMemory{}; + const bool RelevancyExecutable = + VirtualQuery(reinterpret_cast(IsNetRelevantForAddress), + &RelevancyMemory, sizeof(RelevancyMemory)) == sizeof(RelevancyMemory) && + RelevancyMemory.State == MEM_COMMIT && + (RelevancyMemory.Protect & ( + PAGE_EXECUTE | PAGE_EXECUTE_READ | + PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY)) != 0; + NativeRelevancySignatureValid = RelevancyExecutable && + std::memcmp(reinterpret_cast(IsNetRelevantForAddress), + IsNetRelevantForSignature, + sizeof(IsNetRelevantForSignature)) == 0; + NativeIsNetRelevantFor = NativeRelevancySignatureValid + ? reinterpret_cast(IsNetRelevantForAddress) + : nullptr; + std::cout << "Native actor relevancy: " + << (NativeIsNetRelevantFor ? "AActor::IsNetRelevantFor" : + "conservative custom fallback") + << std::endl; + + const uintptr_t CallPreReplicationAddress = + BaseAddress + OffsetActorCallPreReplication; + MEMORY_BASIC_INFORMATION PreReplicationMemory{}; + const bool PreReplicationExecutable = + VirtualQuery(reinterpret_cast(CallPreReplicationAddress), + &PreReplicationMemory, sizeof(PreReplicationMemory)) == + sizeof(PreReplicationMemory) && + PreReplicationMemory.State == MEM_COMMIT && + (PreReplicationMemory.Protect & ( + PAGE_EXECUTE | PAGE_EXECUTE_READ | + PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY)) != 0; + NativePreReplicationSignatureValid = PreReplicationExecutable && + std::memcmp(reinterpret_cast(CallPreReplicationAddress), + CallPreReplicationSignature, + sizeof(CallPreReplicationSignature)) == 0; + NativeCallActorPreReplication = NativePreReplicationSignatureValid + ? reinterpret_cast( + CallPreReplicationAddress) + : nullptr; + std::cout << "Native actor pre-replication: " + << (NativeCallActorPreReplication + ? "AActor::CallPreReplication" + : "signature mismatch") + << std::endl; + if (!NativeCallActorPreReplication) + return FailListen( + "AActor::CallPreReplication signature validation failed"); + + FName GameNetDriver = UKismetStringLibrary::Conv_StringToName(L"GameNetDriver"); + NativeGameNetDriverName = GameNetDriver; + NativeOwnershipConflictLogged = false; + CreateNamedNetDriverFn CreateNamedNetDriver = reinterpret_cast(BaseAddress + OffsetCreateNamedNetDriver); + + const bool NetDriverCreated = CreateNamedNetDriver( + Engine, World, GameNetDriver, GameNetDriver) != 0; + std::cout << "Net driver create: " << static_cast(NetDriverCreated) << std::endl; + if (!NetDriverCreated) + return FailListen("CreateNamedNetDriver failed for GameNetDriver"); + + NetDriver = FindNetDriverByName(Engine, World, GameNetDriver); + + if (!NetDriver) + return FailListen("created GameNetDriver is absent from the active world context"); + + const ObjectIdentity NetDriverIdentity = TrackObject(NetDriver); + if (ResolveObject(NetDriverIdentity) != NetDriver || + !NetDriver->IsA(UIpNetDriver::StaticClass())) { + return FailListen("GameNetDriver identity or IpNetDriver class validation failed"); + } + + std::cout << NetDriver->GetFullName() << std::endl; + + ResetConsiderCache(); + + // Match Unreal's native listen ordering. SetReplicationDriver calls + // InitializeActorsInWorld from inside InitListen, so the world's + // authoritative NetDriver pointer must already be visible then. + World->NetDriver = NetDriver; + SetNetDriverWorldFn SetNetDriverWorld = reinterpret_cast(BaseAddress + OffsetSetNetDriverWorld); + SetNetDriverWorld(NetDriver, World); + + // The synthetic dedicated server uses UIpNetDriver for transport and + // the scheduler below for actor selection. The client-cooked Archon + // graph cannot be initialized faithfully here. + NetDriver->ReplicationDriverClass = nullptr; + + FURL url = FURL(); + url.Port = Port; + + FString empty = FString(); + InitListenFn InitListen = *(reinterpret_cast( + *reinterpret_cast(NetDriver) + OffsetNetDriverInitListen)); + // UWorld implements FNetworkNotify as an embedded secondary base at + // World+0x28. InitListen expects the address of that subobject, not the + // vtable value stored in its first field. + void* const ExpectedNetworkNotify = static_cast(&World->NetworkNotify); + bool ListenStatus = InitListen(NetDriver, ExpectedNetworkNotify, + &url, false, &empty); + + std::cout << "Listen Status: " << ListenStatus << std::endl; + + if (!ListenStatus) + return FailListen("UIpNetDriver::InitListen returned false"); + + void* const InstalledNetworkNotify = *reinterpret_cast( + reinterpret_cast(NetDriver) + OffsetNetDriverNotify); + std::cout << "Native network notify validation: installed=" << InstalledNetworkNotify + << " expected=" << ExpectedNetworkNotify + << " world=" << World + << " worldNetDriver=" << World->NetDriver + << " driverWorld=" << NetDriver->World << std::endl; + if (InstalledNetworkNotify != ExpectedNetworkNotify || + World->NetDriver != NetDriver || NetDriver->World != World) { + return FailListen("native NetDriver world/network-notify validation failed"); + } + + if (NetDriver->ReplicationDriver) + return FailListen("unexpected native replication driver with custom scheduler"); + std::cout << "Native network transport: " << NetDriver->GetFullName() + << " (optimized actor scheduler)" << std::endl; + return true; + } + + static bool IsAuthoritativeNativeOwnership(UNetDriver* Driver, UWorld* World, + bool RequireReciprocal) { + if (!Driver || Driver != NetDriver || !World) + return false; + const ObjectIdentity DriverIdentity = TrackObject(Driver); + const ObjectIdentity WorldIdentity = TrackObject(World); + if (ResolveObject(DriverIdentity) != Driver || ResolveObject(WorldIdentity) != World || + !Driver->IsA(UIpNetDriver::StaticClass())) + return false; + UEngine* Engine = UEngine::GetEngine(); + if (!Engine || FindNetDriverByName(Engine, World, NativeGameNetDriverName) != Driver) + return false; + void* const ExpectedNotify = static_cast(&World->NetworkNotify); + void* const InstalledNotify = *reinterpret_cast( + reinterpret_cast(Driver) + OffsetNetDriverNotify); + return Driver->World == World && InstalledNotify == ExpectedNotify && + (!RequireReciprocal || World->NetDriver == Driver); + } + + static bool EnsureNativeReciprocalOwnership(UNetDriver* Driver, const char* Stage) { + UWorld* World = UWorld::GetWorld(); + if (IsAuthoritativeNativeOwnership(Driver, World, true)) + return true; + if (!IsAuthoritativeNativeOwnership(Driver, World, false)) { + ++ProfileCounters.NativeDispatchBlocked; + if (World && World->NetDriver && World->NetDriver != Driver) { + ++ProfileCounters.NativeDispatchConflicts; + if (!NativeOwnershipConflictLogged) { + NativeOwnershipConflictLogged = true; + std::cout << "Native network ownership conflict at " << Stage + << ": expected=" << Driver << " actual=" << World->NetDriver << std::endl; + QueueConnectionEvent({ ConnectionEventType::NativeDispatchConflict, nullptr, + CurrentNetworkTick(), 0, 0, 0 }); + } + } + return false; + } + if (World->NetDriver != nullptr) + return false; + + World->NetDriver = Driver; + if (World->NetDriver != Driver) { + ++ProfileCounters.NativeDispatchBlocked; + return false; + } + ++ProfileCounters.NativeReciprocalRepairs; + if (!NativeDispatchOwnershipMismatchLogged) { + NativeDispatchOwnershipMismatchLogged = true; + std::cout << "Native reciprocal ownership restored at " << Stage + << ": driver=" << Driver << " world=" << World << std::endl; + QueueConnectionEvent({ ConnectionEventType::NativeReciprocalRepair, nullptr, + CurrentNetworkTick(), 0, 0, 0 }); + } + return true; + } + + bool PrepareNativeNetworkDispatch(UNetDriver* Driver) { + if (!Driver || Driver != NetDriver) + return true; + const bool OwnershipValidAtEntry = + IsAuthoritativeNativeOwnership(Driver, UWorld::GetWorld(), true); + if (OwnershipValidAtEntry) + ++ProfileCounters.NativeDispatchOwnershipValidBefore; + const bool Ready = EnsureNativeReciprocalOwnership(Driver, "before TickDispatch"); + return Ready; + } + + void NotifyNativeNetworkDispatchCompleted(UNetDriver* Driver) { + if (!Driver || Driver != NetDriver) + return; + ++ProfileCounters.NativeDispatchCalls; + const bool ValidAfter = + IsAuthoritativeNativeOwnership(Driver, UWorld::GetWorld(), true); + if (ValidAfter) + ++ProfileCounters.NativeDispatchOwnershipValidAfter; + else { + ++ProfileCounters.NativeDispatchOwnershipLostInside; + EnsureNativeReciprocalOwnership(Driver, "after TickDispatch"); + } + } + + bool PrepareNativeNetworkFlush(UNetDriver* Driver) { + if (!Driver || Driver != NetDriver) + return true; + const bool OwnershipValidAtEntry = + IsAuthoritativeNativeOwnership(Driver, UWorld::GetWorld(), true); + if (OwnershipValidAtEntry) + ++ProfileCounters.NativeFlushOwnershipValidBefore; + const bool Ready = EnsureNativeReciprocalOwnership(Driver, "before TickFlush"); + return Ready; + } + + void NotifyNativeNetworkFlushCompleted(UNetDriver* Driver) { + if (!Driver || Driver != NetDriver) + return; + ++ProfileCounters.NativeFlushCalls; + const bool ValidAfter = + IsAuthoritativeNativeOwnership(Driver, UWorld::GetWorld(), true); + if (ValidAfter) + ++ProfileCounters.NativeFlushOwnershipValidAfter; + else + ++ProfileCounters.NativeFlushOwnershipLostInside; + } + + void NotifyNativeEngineTickCompleted(UWorld* World) { + if (!NetDriver || !World) + return; + if (!IsAuthoritativeNativeOwnership(NetDriver, World, true)) + ++ProfileCounters.NativeOwnershipMissingAfterEngineTick; + } + + void NotifyNativePostLogin(APlayerController* PlayerController) { + const ObjectIdentity ControllerIdentity = TrackObject(PlayerController); + if (!PlayerController || ResolveObject(ControllerIdentity) != PlayerController) + return; + + UNetConnection* Connection = + FindConnectionForController(PlayerController); + ++ProfileCounters.NativePostLogins; + QueueConnectionEvent({ ConnectionEventType::NativePostLogin, Connection, + CurrentNetworkTick(), static_cast(ControllerIdentity.Index), + Connection ? 1u : 0u, 0 }); + } + + void TickNetworking() { + TickNetworking(UWorld::GetWorld()); + } + + + void TickNetworking(UWorld* World) { + ProfileCounters.LastCompletedPhase = 1; + if (!World || !NetDriver) { + if (!ConnectionBootstrapStates.empty() || !ActorReplicationStates.empty() || CachedWorld) + ResetConsiderCache(); + CurrentConnections.clear(); + LiveConnections.clear(); + return; + } + + if (CachedWorld && CachedWorld != World) + ResetConsiderCache(); + + World->NetDriver = NetDriver; + NetDriver->World = World; + + static FName ChannelName = UKismetStringLibrary::Conv_StringToName(L"Actor"); + static UClass* ActorChannelClass = UActorChannel::StaticClass(); + static ReplicationClassCache ClassCache = { + APlayerController::StaticClass(), + AArchonCharacter::StaticClass(), + AArchonBehemoth::StaticClass(), + AArchonLantern::StaticClass(), + AArchonEquipment::StaticClass(), + Aprojectile_base_bp_C::StaticClass(), + APawn::StaticClass(), + APlayerState::StaticClass(), + AArchonLoadout::StaticClass(), + AArchonInventory::StaticClass(), + AArchonBuff::StaticClass(), + AArchonAoe::StaticClass(), + AArchonBeam::StaticClass(), + AAbilityActor::StaticClass(), + AMonsterPartActor::StaticClass() + }; + static CreateChannelByNameFn CreateChannelByName = reinterpret_cast(BaseAddress + OffsetCreateChannelByName); + static SetChannelActorFn SetChannelActor = reinterpret_cast(BaseAddress + OffsetSetChannelActor); + static ReplicateActorFn ReplicateActor = reinterpret_cast(BaseAddress + OffsetActorChannelReplicateActor); + static UpdateCameraFn UpdateCamera = reinterpret_cast(BaseAddress + OffsetPlayerControllerUpdateCamera); + + uint32_t& NetworkTick = *reinterpret_cast((uintptr_t)NetDriver + OffsetNetDriverTickCount); + ++NetworkTick; + RefreshConnectionState(NetDriver, NetworkTick); + ProfileCounters.LastCompletedPhase = 2; + ++ProfileCounters.SchedulerTickCalls; + ++ProfileCounters.ConnectionSamples; + ProfileCounters.ConnectionTotal += LiveConnections.size(); + ProfileCounters.MaximumConnections = (std::max)(ProfileCounters.MaximumConnections, + static_cast(LiveConnections.size())); + + // There is no useful actor-channel work without a client. Preserve the + // driver/world bookkeeping above, then force a fresh candidate snapshot + // as soon as the first connection arrives. + if (LiveConnections.empty()) { + ++ProfileCounters.ZeroConnectionTicks; + ConsiderCacheDirtyForNextConnection = true; + EmitPendingConnectionEvents(); + ProfileCounters.BootstrapPhaseConnections.fill(0); + ProfileCounters.LastCompletedPhase = 6; + return; + } + + UrgentCandidateBatch UrgentCandidates{}; + CacheRebuildReason RebuildReason = CacheRebuildReason::Forced; + if (ConsiderCacheDirtyForNextConnection || + GetCacheRebuildReason(World, RebuildReason)) { + RebuildConsiderCache(World, NetDriver, ClassCache, RebuildReason); + ConsiderCacheDirtyForNextConnection = false; + } + const int64_t CandidateSelectionStartedAt = PerformanceTimestamp(); + BuildReplicateList(NetworkTick); + ProfileCounters.CandidateSelectionMicroseconds += + ElapsedMicroseconds(CandidateSelectionStartedAt); + ++ProfileCounters.CandidateSelectionTimingSamples; + UrgentCandidates = BuildUrgentCandidateBatch(GetTickCount64()); + ProfileCounters.LastCompletedPhase = 3; + + for (UNetConnection* Connection : LiveConnections) { + const int64_t ChannelScanStartedAt = PerformanceTimestamp(); + BuildActorChannelScratchMap(Connection, ActorChannelClass); + ProfileCounters.ChannelScanMicroseconds += ElapsedMicroseconds(ChannelScanStartedAt); + ++ProfileCounters.ChannelScanTimingSamples; + + if (Connection->PlayerController) + Connection->ViewTarget = Connection->PlayerController->GetViewTarget(); + + auto StateIt = ConnectionBootstrapStates.find(Connection); + ConnectionBootstrapState* Bootstrap = StateIt != ConnectionBootstrapStates.end() + ? &StateIt->second + : nullptr; + const ULONGLONG NowMs = GetTickCount64(); + if (Bootstrap) + RefreshPawnState(Connection, *Bootstrap, NetworkTick, NowMs); + const bool IsBootstrapping = Bootstrap && + Bootstrap->Phase == ConnectionBootstrapPhase::Bootstrapping; + if (++CurrentConnectionPass == 0) + ++CurrentConnectionPass; + const uint64_t ConnectionPass = CurrentConnectionPass; + + if (IsBootstrapping) { + const int64_t BootstrapSelectionStartedAt = PerformanceTimestamp(); + DiscoverCriticalActors(World, Connection, *Bootstrap, NetworkTick, NowMs); + ProfileCounters.CandidateSelectionMicroseconds += + ElapsedMicroseconds(BootstrapSelectionStartedAt); + ++ProfileCounters.CandidateSelectionTimingSamples; + } + + const int64_t ReplicationStartedAt = PerformanceTimestamp(); + const auto TryCandidate = [&](ReplicationCandidate& Candidate, + bool BypassFrequency, bool BootstrapReplication, + bool MovementPrepass, float AdaptiveFloorHz = 0.0f, + bool* OutAttempted = nullptr) { + if (OutAttempted) + *OutAttempted = false; + if (Candidate.LastReplicatedConnectionPass == ConnectionPass) { + ++ProfileCounters.LivePolicyDuplicateSkips; + return false; + } + + AActor* Actor = Candidate.Actor; + if (!Candidate.ReplicationState || + ResolveObject(Candidate.ReplicationState->Identity) != Actor || + !IsActorReplicationCandidate(World, Actor)) { + ConsiderCacheDirtyForNextConnection = true; + ++ProfileCounters.SchedulerActorIdentityResets; + return false; + } + + UActorChannel* ExistingChannel = nullptr; + const auto ExistingIt = ScratchActorChannels.find(Actor); + if (ExistingIt != ScratchActorChannels.end()) + ExistingChannel = ExistingIt->second; + + const LivePolicyResult Policy = EvaluateLivePolicy( + World, Connection, Actor, Candidate.ReplicationState, + ExistingChannel, BypassFrequency); + Candidate.LastReplicatedConnectionPass = ConnectionPass; + if (Policy.Decision != LivePolicyDecision::Eligible) { + if (BypassFrequency) + ++ProfileCounters.LivePolicyCriticalRejected; + return false; + } + + bool ReplicationAttempted = false; + const bool ProducedData = ReplicateActorForConnection( + Connection, Actor, Candidate.IsPlayerController, + ChannelName, CreateChannelByName, SetChannelActor, + ReplicateActor, UpdateCamera, Candidate.Bucket, + BootstrapReplication, Candidate.ReplicationState, + &ReplicationAttempted); + UActorChannel* ResultingChannel = nullptr; + const auto ResultingIt = ScratchActorChannels.find(Actor); + if (ResultingIt != ScratchActorChannels.end()) + ResultingChannel = ResultingIt->second; + FinishLivePolicy(Actor, ResultingChannel, Policy.State, + ReplicationAttempted, ProducedData, AdaptiveFloorHz); + + if (MovementPrepass && ReplicationAttempted) { + ++ProfileCounters.LivePolicyMovementPrepassAttempts; + if (ProducedData) + ++ProfileCounters.LivePolicyMovementPrepassSuccesses; + } + if (OutAttempted) + *OutAttempted = ReplicationAttempted; + return ProducedData; + }; + + for (size_t UrgentIndex = 0; UrgentIndex < UrgentCandidates.Count; ++UrgentIndex) { + UrgentCandidateEntry& Urgent = UrgentCandidates.Entries[UrgentIndex]; + ReplicationCandidate* Candidate = Urgent.Candidate; + if (!Candidate || (Urgent.TargetConnection.Object && + ResolveObject(Urgent.TargetConnection) != Connection)) { + continue; + } + // Multiple outcomes can promote the same actor in one engine pass. + // Preserve each reason's queued metrics without replicating the actor twice. + if (Candidate->LastReplicatedConnectionPass == ConnectionPass) + continue; + + bool ReplicationAttempted = false; + const bool ProducedData = TryCandidate(*Candidate, true, false, + false, 0.0f, &ReplicationAttempted); + const size_t ReasonIndex = UrgentReasonIndex(Urgent.Reason); + if (ProducedData) { + ++ProfileCounters.UrgentDamageReplicationSuccesses; + ++ProfileCounters.UrgentSuccessesByReason[ReasonIndex]; + } + if (ReplicationAttempted) { + Candidate->LastReplicatedConnectionPass = ConnectionPass; + ++ProfileCounters.UrgentDamageReplicationAttempts; + ++ProfileCounters.UrgentAttemptsByReason[ReasonIndex]; + ++ProfileCounters.UrgentDamageLatencySamples; + ProfileCounters.UrgentDamageLatencyMilliseconds += + NowMs - Urgent.QueuedAtMs; + ++ProfileCounters.UrgentLatencySamplesByReason[ReasonIndex]; + ProfileCounters.UrgentLatencyMillisecondsByReason[ReasonIndex] += + NowMs - Urgent.QueuedAtMs; + } + else { + RetryUrgentCandidateForConnection(Urgent, Connection); + } + } + + // Critical and connection-owned actors are considered first on every + // 30 Hz pass. Native relevancy still prevents owner-only leakage. + for (ReplicationCandidate& Candidate : ConsiderCache) { + AActor* Actor = Candidate.Actor; + if (Candidate.LastReplicatedConnectionPass == ConnectionPass || + IsUrgentCandidate(UrgentCandidates, &Candidate, Connection)) { + continue; + } + if (!Candidate.ReplicationState || + ResolveObject(Candidate.ReplicationState->Identity) != Actor) { + ConsiderCacheDirtyForNextConnection = true; + ++ProfileCounters.SchedulerActorIdentityResets; + continue; + } + if (!IsActorReplicationCandidate(World, Actor)) + continue; + + InitializationActorRole Role{}; + uint8_t OwnerDepth = 0; + const bool ConnectionCritical = + FindInitializationRole(World, Connection, Actor, Role, OwnerDepth); + if (!ConnectionCritical && !Candidate.IsCritical) + continue; + + ++ProfileCounters.CriticalPrepassCandidates; + bool ReplicationAttempted = false; + const bool CoreConnectionActor = + ConnectionCritical && + Role != InitializationActorRole::OwnedActor; + const bool LatencySensitiveOwned = + ConnectionCritical && + IsLatencySensitiveOwnedActor(Candidate.ClassFlags); + const bool ForceEveryPass = + IsBootstrapping || Candidate.IsCritical || + CoreConnectionActor || LatencySensitiveOwned; + const float AdaptiveFloorHz = + ConnectionCritical && !ForceEveryPass + ? OwnedCriticalAdaptiveFloorHz + : 0.0f; + if (AdaptiveFloorHz > 0.0f) + ++ProfileCounters.CriticalOwnedFrequencyLimited; + const bool ProducedData = TryCandidate( + Candidate, ForceEveryPass, + IsBootstrapping && ConnectionCritical, false, + AdaptiveFloorHz, &ReplicationAttempted); + if (ReplicationAttempted) { + ++ProfileCounters.CriticalPrepassAttempts; + if (ProducedData) + ++ProfileCounters.CriticalPrepassSuccesses; + } + else { + ++ProfileCounters.CriticalPrepassSetupFallbacks; + } + } + + // Movement-sensitive gameplay actors are next. Their live + // NetUpdateFrequency remains authoritative; observed movement + // merely cancels an adaptive idle deadline. + for (ReplicationCandidate& Candidate : ConsiderCache) { + AActor* Actor = Candidate.Actor; + if (!Actor || Candidate.LastReplicatedConnectionPass == ConnectionPass) + continue; + if (!Actor->bReplicateMovement && + Candidate.Bucket == ReplicationBucket::Immediate) + continue; + TryCandidate(Candidate, false, false, true); + } + + PriorityScratch.clear(); + if (PriorityScratch.capacity() < ConsiderCache.size()) + PriorityScratch.reserve(ConsiderCache.size()); + for (ReplicationCandidate& Candidate : ConsiderCache) { + AActor* Actor = Candidate.Actor; + if (!Actor || Candidate.LastReplicatedConnectionPass == ConnectionPass) + continue; + if (!Candidate.ReplicationState || + ResolveObject(Candidate.ReplicationState->Identity) != Actor || + !IsActorReplicationCandidate(World, Actor)) { + ConsiderCacheDirtyForNextConnection = true; + ++ProfileCounters.SchedulerActorIdentityResets; + continue; + } + UActorChannel* ExistingChannel = nullptr; + const auto ExistingIt = ScratchActorChannels.find(Actor); + if (ExistingIt != ScratchActorChannels.end()) + ExistingChannel = ExistingIt->second; + const LivePolicyResult Policy = EvaluateLivePolicy( + World, Connection, Actor, Candidate.ReplicationState, + ExistingChannel, false); + Candidate.LastReplicatedConnectionPass = ConnectionPass; + if (Policy.Decision != LivePolicyDecision::Eligible) + continue; + const float Priority = ComputeLivePriority( + Connection, Actor, Policy.State, ExistingChannel); + ++ProfileCounters.LivePolicyPriorityBands[ + PriorityBand(Priority)]; + PriorityScratch.push_back({ + &Candidate, Priority, Policy.State + }); + } + std::sort(PriorityScratch.begin(), PriorityScratch.end(), + [](const PrioritizedCandidate& Left, + const PrioritizedCandidate& Right) { + if (Left.Priority != Right.Priority) + return Left.Priority > Right.Priority; + const int32_t LeftIndex = + Left.Candidate && Left.Candidate->ReplicationState + ? Left.Candidate->ReplicationState->Identity.Index : -1; + const int32_t RightIndex = + Right.Candidate && Right.Candidate->ReplicationState + ? Right.Candidate->ReplicationState->Identity.Index : -1; + return LeftIndex < RightIndex; + }); + ++ProfileCounters.LivePolicyPrioritySorts; + ProfileCounters.LivePolicyPriorityCandidates += PriorityScratch.size(); + for (const PrioritizedCandidate& Prioritized : PriorityScratch) { + if (!Prioritized.Candidate) + continue; + ReplicationCandidate& Candidate = *Prioritized.Candidate; + bool ReplicationAttempted = false; + const bool ProducedData = ReplicateActorForConnection( + Connection, Candidate.Actor, Candidate.IsPlayerController, + ChannelName, CreateChannelByName, SetChannelActor, + ReplicateActor, UpdateCamera, Candidate.Bucket, false, + Candidate.ReplicationState, &ReplicationAttempted); + UActorChannel* ResultingChannel = nullptr; + const auto ResultingIt = + ScratchActorChannels.find(Candidate.Actor); + if (ResultingIt != ScratchActorChannels.end()) + ResultingChannel = ResultingIt->second; + FinishLivePolicy(Candidate.Actor, ResultingChannel, + Prioritized.State, ReplicationAttempted, ProducedData); + } + ProfileCounters.ReplicationMicroseconds += ElapsedMicroseconds(ReplicationStartedAt); + ++ProfileCounters.ReplicationTimingSamples; + + if (IsBootstrapping) { + RefreshCriticalChannelAcknowledgements(Connection, *Bootstrap, NetworkTick, NowMs); + TryCompleteBootstrap(Connection, *Bootstrap, NetworkTick, NowMs); + } + ProfileCounters.LastCompletedPhase = 4; + } + FinalizeImmediatePassMetrics(NetworkTick); + EmitPendingConnectionEvents(); + ProfileCounters.BootstrapPhaseConnections.fill(0); + for (const auto& [Connection, Bootstrap] : ConnectionBootstrapStates) { + const size_t PhaseIndex = static_cast(Bootstrap.Phase); + if (PhaseIndex < ProfileCounters.BootstrapPhaseConnections.size()) + ++ProfileCounters.BootstrapPhaseConnections[PhaseIndex]; + } + ProfileCounters.LastCompletedPhase = 6; + } + + const std::vector& GetLiveConnections() { + return LiveConnections; + } + + ProfilingCounters TakeProfilingCounters() { + ProfileCounters.CurrentSchedulerStates = CurrentSchedulerActorStates; + ProfileCounters.MaximumSchedulerStates = (std::max)( + ProfileCounters.MaximumSchedulerStates, CurrentSchedulerActorStates); + ProfilingCounters Snapshot = ProfileCounters; + PopulateImmediateClassNames(Snapshot); + ProfileCounters = {}; + ProfileCounters.LastCompletedPhase = Snapshot.LastCompletedPhase; + ProfileCounters.CurrentCandidates = static_cast(ConsiderCache.size()); + ProfileCounters.CurrentSchedulerStates = CurrentSchedulerActorStates; + ProfileCounters.MaximumSchedulerStates = CurrentSchedulerActorStates; + ProfileCounters.BootstrapPhaseConnections = Snapshot.BootstrapPhaseConnections; + ResetImmediateClassRegistry(); + ActorPropertyDistributionDirty = true; + return Snapshot; + } + + void ResetWorldState(UWorld* World) { + (void)World; + ResetConsiderCache(); + CachedWorld = nullptr; + CachedWorldIdentity = {}; + NativeDispatchOwnershipMismatchLogged = false; + NativeOwnershipConflictLogged = false; + } + + void ResetDriverState(UNetDriver* Driver) { + if (Driver == NetDriver) + NetDriver = nullptr; + ResetConsiderCache(); + NativeDispatchOwnershipMismatchLogged = false; + NativeOwnershipConflictLogged = false; + } + + void BeginFrameNetworkLifecycle(UWorld* World) { + if (CachedWorld != nullptr && CachedWorld != World) + ResetWorldState(World); + if (NetDriver != nullptr && NetDriver->World != World) + ResetDriverState(NetDriver); + } + + void EndFrameNetworkLifecycle(UWorld* World) { + if (World == nullptr) + ResetWorldState(nullptr); + } +} diff --git a/UndauntedInternalServer/Networking/Private/ReplicationScheduler.inl b/UndauntedInternalServer/Networking/Private/ReplicationScheduler.inl new file mode 100644 index 0000000..1aad46b --- /dev/null +++ b/UndauntedInternalServer/Networking/Private/ReplicationScheduler.inl @@ -0,0 +1,716 @@ + static void BuildActorChannelScratchMap(UNetConnection* Connection, UClass* ActorChannelClass) { + ScratchActorChannels.clear(); + ScratchActorChannels.reserve(Connection ? Connection->OpenChannels.Num() : 0); + + if (!Connection) + return; + + for (UChannel* Channel : Connection->OpenChannels) { + if (!Channel || Channel->Class != ActorChannelClass) + continue; + + UActorChannel* ActorChannel = static_cast(Channel); + + // An actor channel owns the authoritative association here. A newly + // created replicated actor can be valid before a global object-index + // identity probe is stable; filtering it out permits a duplicate + // channel and breaks the initial actor/subobject bunch sequence. + if (ActorChannel->Actor) + ScratchActorChannels[ActorChannel->Actor] = ActorChannel; + } + } + + static uint32_t CurrentNetworkTick() { + return NetDriver + ? *reinterpret_cast((uintptr_t)NetDriver + OffsetNetDriverTickCount) + : 0; + } + + static bool FindInitializationRole( + UWorld* World, + UNetConnection* Connection, + AActor* Actor, + InitializationActorRole& Role, + uint8_t& OwnerDepth + ) { + if (!World || !Connection || !Actor || !Connection->PlayerController) + return false; + + APlayerController* Controller = Connection->PlayerController; + APlayerState* PlayerState = Controller->PlayerState; + APawn* Pawn = Controller->Pawn; + AActor* ViewTarget = Connection->ViewTarget; + + if (Actor == Controller) { + Role = InitializationActorRole::Controller; + OwnerDepth = 0; + return true; + } + if (Actor == World->GameState) { + Role = InitializationActorRole::GameState; + OwnerDepth = 0; + return true; + } + if (Actor == PlayerState) { + Role = InitializationActorRole::PlayerState; + OwnerDepth = 0; + return true; + } + if (Actor == Pawn) { + Role = InitializationActorRole::Pawn; + OwnerDepth = 0; + return true; + } + if (Actor == ViewTarget) { + Role = InitializationActorRole::ViewTarget; + OwnerDepth = 0; + return true; + } + + AActor* Owner = Actor->Owner; + for (size_t Depth = 1; Owner && Depth <= MaximumOwnerChainDepth; ++Depth) { + if (Owner == Controller || Owner == PlayerState || Owner == Pawn) { + Role = InitializationActorRole::OwnedActor; + OwnerDepth = static_cast(Depth); + return true; + } + + const ObjectIdentity OwnerIdentity = TrackObject(Owner); + if (ResolveObject(OwnerIdentity) != Owner) + break; + + if (Owner->bActorIsBeingDestroyed || GetActorWorld(Owner) != World) + break; + + Owner = Owner->Owner; + } + + return false; + } + + static CriticalActorState* EnsureCriticalActor( + UNetConnection* Connection, + ConnectionBootstrapState& State, + AActor* Actor, + InitializationActorRole Role, + uint8_t OwnerDepth, + uint32_t NetworkTick, + ULONGLONG NowMs + ) { + if (!Actor) + return nullptr; + + auto Existing = State.CriticalActors.find(Actor); + if (Existing != State.CriticalActors.end()) + return &Existing->second; + + ObjectIdentity Identity = TrackObject(Actor); + if (!Identity.Object) + return nullptr; + + auto [Inserted, WasInserted] = State.CriticalActors.emplace(Actor, + CriticalActorState{ Identity, Role, OwnerDepth, false }); + if (WasInserted) { + State.LastCriticalActorDiscoveredAtMs = NowMs; + ++ProfileCounters.CriticalActorsDiscovered; + QueueConnectionEvent({ ConnectionEventType::CriticalActorDiscovered, Connection, NetworkTick, + static_cast(Role), OwnerDepth, NowMs - State.StartedAtMs }); + } + return &Inserted->second; + } + + static void PruneCriticalActors(ConnectionBootstrapState& State) { + for (auto It = State.CriticalActors.begin(); It != State.CriticalActors.end();) { + if (!ResolveObject(It->second.Identity)) + It = State.CriticalActors.erase(It); + else + ++It; + } + } + + static void RestartBootstrap( + UNetConnection* Connection, + ConnectionBootstrapState& State, + UWorld* World, + uint32_t NetworkTick, + uint32_t Reason, + ULONGLONG NowMs + ) { + State = MakeConnectionBootstrapState(Connection, World); + State.StartedAtMs = NowMs; + State.LastCriticalActorDiscoveredAtMs = NowMs; + ++ProfileCounters.BootstrapRestarts; + QueueConnectionEvent({ ConnectionEventType::BootstrapRestarted, Connection, NetworkTick, + Reason, 0, 0 }); + } + + static void RefreshPawnState( + UNetConnection* Connection, + ConnectionBootstrapState& State, + uint32_t NetworkTick, + ULONGLONG NowMs + ) { + APawn* CurrentPawn = Connection && Connection->PlayerController + ? Connection->PlayerController->Pawn + : nullptr; + + if (IdentityMatches(State.Pawn, CurrentPawn)) + return; + + RestartBootstrap(Connection, State, NetDriver ? NetDriver->World : nullptr, + NetworkTick, 2u, NowMs); + } + + static void DiscoverCriticalActors( + UWorld* World, + UNetConnection* Connection, + ConnectionBootstrapState& State, + uint32_t NetworkTick, + ULONGLONG NowMs + ) { + if (State.Phase != ConnectionBootstrapPhase::Bootstrapping) + return; + + PruneCriticalActors(State); + + for (ReplicationCandidate& Candidate : ConsiderCache) { + if (!Candidate.ReplicationState || + ResolveObject(Candidate.ReplicationState->Identity) != Candidate.Actor) { + ConsiderCacheDirtyForNextConnection = true; + ++ProfileCounters.SchedulerActorIdentityResets; + continue; + } + + if (!IsCachedActorStillReplicatable(Candidate.Actor)) + continue; + + InitializationActorRole Role{}; + uint8_t OwnerDepth = 0; + if (!FindInitializationRole(World, Connection, Candidate.Actor, Role, OwnerDepth)) + continue; + + EnsureCriticalActor(Connection, State, Candidate.Actor, Role, OwnerDepth, + NetworkTick, NowMs); + } + } + + static UrgentCandidateBatch BuildUrgentCandidateBatch(ULONGLONG NowMs) { + UrgentCandidateBatch Batch{}; + size_t RetainedCount = 0; + + for (size_t Index = 0; Index < PendingUrgentDamageTargetCount; ++Index) { + const UrgentDamageTarget& Pending = PendingUrgentDamageTargets[Index]; + UObject* Resolved = ResolveObject(Pending.Identity); + if (!Resolved) { + ++ProfileCounters.UrgentDamageTargetsInvalidated; + ++ProfileCounters.UrgentInvalidatedByReason[UrgentReasonIndex(Pending.Reason)]; + continue; + } + if (Pending.TargetConnection.Object && !ResolveObject(Pending.TargetConnection)) { + ++ProfileCounters.UrgentDamageTargetsInvalidated; + ++ProfileCounters.UrgentInvalidatedByReason[UrgentReasonIndex(Pending.Reason)]; + continue; + } + if (NowMs - Pending.QueuedAtMs > UrgentDamageTargetMaximumAgeMs) { + ++ProfileCounters.UrgentDamageTargetsExpired; + ++ProfileCounters.UrgentExpiredByReason[UrgentReasonIndex(Pending.Reason)]; + continue; + } + + ReplicationCandidate* Found = nullptr; + const auto CandidateIt = CandidateActorLookup.find(static_cast(Resolved)); + if (CandidateIt != CandidateActorLookup.end()) { + ReplicationCandidate& Candidate = *CandidateIt->second; + if (!Candidate.ReplicationState || + ResolveObject(Candidate.ReplicationState->Identity) != Candidate.Actor) { + ConsiderCacheDirtyForNextConnection = true; + ++ProfileCounters.SchedulerActorIdentityResets; + } else { + Found = &Candidate; + } + } + + if (!Found) { + ConsiderCacheDirtyForNextConnection = true; + PendingUrgentDamageTargets[RetainedCount++] = Pending; + continue; + } + + // Damage processing can destroy or retire the target before this + // next networking pass. Urgency changes scheduling priority only; + // it must never bypass the ordinary replication eligibility gate. + UWorld* ExpectedWorld = NetDriver ? NetDriver->World : nullptr; + if (!IsActorReplicationCandidate(ExpectedWorld, Found->Actor)) { + ++ProfileCounters.UrgentDamageTargetsInvalidated; + ++ProfileCounters.UrgentInvalidatedByReason[UrgentReasonIndex(Pending.Reason)]; + continue; + } + + if (Batch.Count < Batch.Entries.size()) { + Batch.Entries[Batch.Count++] = { + Found, + Pending.Identity, + Pending.TargetConnection, + Pending.QueuedAtMs, + Pending.SetupRetries, + Pending.Reason + }; + } + else { + ++ProfileCounters.UrgentDamageTargetsDropped; + ++ProfileCounters.UrgentDroppedByReason[UrgentReasonIndex(Pending.Reason)]; + } + } + + PendingUrgentDamageTargetCount = RetainedCount; + return Batch; + } + + static void RetryUrgentCandidateForConnection( + const UrgentCandidateEntry& Entry, + UNetConnection* Connection + ) { + if (Entry.SetupRetries >= 1) { + ++ProfileCounters.UrgentDamageSetupRetryExhausted; + ++ProfileCounters.UrgentDamageTargetsDropped; + ++ProfileCounters.UrgentDroppedByReason[UrgentReasonIndex(Entry.Reason)]; + return; + } + + const ObjectIdentity ConnectionIdentity = TrackObject(Connection); + if (!IsStableIdentity(ConnectionIdentity, Connection)) { + ++ProfileCounters.UrgentDamageTargetsInvalidated; + ++ProfileCounters.UrgentInvalidatedByReason[UrgentReasonIndex(Entry.Reason)]; + return; + } + + for (size_t PendingIndex = 0; + PendingIndex < PendingUrgentDamageTargetCount; ++PendingIndex) { + const UrgentDamageTarget& Pending = PendingUrgentDamageTargets[PendingIndex]; + if (IdentitiesEqual(Pending.Identity, Entry.Identity) && + (!Pending.TargetConnection.Object || + IdentitiesEqual(Pending.TargetConnection, ConnectionIdentity))) { + return; + } + } + if (PendingUrgentDamageTargetCount >= PendingUrgentDamageTargets.size()) { + ++ProfileCounters.UrgentDamageTargetsDropped; + ++ProfileCounters.UrgentDroppedByReason[UrgentReasonIndex(Entry.Reason)]; + return; + } + + PendingUrgentDamageTargets[PendingUrgentDamageTargetCount++] = { + Entry.Identity, + ConnectionIdentity, + Entry.QueuedAtMs, + static_cast(Entry.SetupRetries + 1), + Entry.Reason + }; + ++ProfileCounters.UrgentDamageSetupRetries; + } + + static bool IsUrgentCandidate( + const UrgentCandidateBatch& Batch, + const ReplicationCandidate* Candidate, + UNetConnection* Connection + ) { + for (size_t Index = 0; Index < Batch.Count; ++Index) { + const UrgentCandidateEntry& Entry = Batch.Entries[Index]; + if (Entry.Candidate == Candidate && + (!Entry.TargetConnection.Object || + ResolveObject(Entry.TargetConnection) == Connection)) { + return true; + } + } + return false; + } + + static bool IsChannelOpenAcknowledged(UActorChannel* Channel) { + if (!Channel) + return false; + + const uint32_t Flags = *reinterpret_cast( + reinterpret_cast(Channel) + OffsetChannelFlags); + return (Flags & ChannelOpenAcknowledgedMask) == ChannelOpenAcknowledgedMask; + } + + static void RefreshCriticalChannelAcknowledgements( + UNetConnection* Connection, + ConnectionBootstrapState& State, + uint32_t NetworkTick, + ULONGLONG NowMs + ) { + if (State.Phase != ConnectionBootstrapPhase::Bootstrapping) + return; + + PruneCriticalActors(State); + for (auto& [Actor, Critical] : State.CriticalActors) { + if (!ResolveObject(Critical.Identity)) + continue; + + const auto ChannelIt = ScratchActorChannels.find(Actor); + const bool IsAcknowledged = ChannelIt != ScratchActorChannels.end() && + IsChannelOpenAcknowledged(ChannelIt->second); + if (IsAcknowledged && !Critical.ChannelAcknowledged) { + ++ProfileCounters.CriticalChannelsAcknowledged; + QueueConnectionEvent({ ConnectionEventType::CriticalChannelAcknowledged, + Connection, NetworkTick, static_cast(Critical.Role), + Critical.OwnerDepth, NowMs - State.StartedAtMs }); + } + if (IsAcknowledged) + Critical.ChannelAcknowledged = true; + } + } + + static void TryCompleteBootstrap( + UNetConnection* Connection, + ConnectionBootstrapState& State, + uint32_t NetworkTick, + ULONGLONG NowMs + ) { + if (State.Phase != ConnectionBootstrapPhase::Bootstrapping) + return; + + const ULONGLONG ElapsedMs = NowMs - State.StartedAtMs; + if (ElapsedMs >= BootstrapMaximumDurationMs) { + State.Phase = ConnectionBootstrapPhase::Active; + ++ProfileCounters.BootstrapDeadlines; + QueueConnectionEvent({ ConnectionEventType::BootstrapDeadline, Connection, NetworkTick, + static_cast(State.CriticalActors.size()), + State.PossessionAcknowledged ? 1u : 0u, ElapsedMs }); + return; + } + + bool HasController = false; + bool HasPlayerState = false; + bool HasPawn = false; + bool AllCriticalChannelsAcknowledged = true; + for (const auto& [Actor, Critical] : State.CriticalActors) { + if (!ResolveObject(Critical.Identity)) + continue; + HasController |= Critical.Role == InitializationActorRole::Controller; + HasPlayerState |= Critical.Role == InitializationActorRole::PlayerState; + HasPawn |= Critical.Role == InitializationActorRole::Pawn; + AllCriticalChannelsAcknowledged &= Critical.ChannelAcknowledged; + } + + const bool MinimumDurationElapsed = ElapsedMs >= BootstrapMinimumDurationMs; + const bool Stable = NowMs - State.LastCriticalActorDiscoveredAtMs >= BootstrapStableMs; + if (!MinimumDurationElapsed || !HasController || !HasPlayerState || !HasPawn || + !AllCriticalChannelsAcknowledged || !State.PossessionAcknowledged || !Stable) + return; + + State.Phase = ConnectionBootstrapPhase::Active; + ++ProfileCounters.BootstrapCompleted; + ++ProfileCounters.BootstrapLatencySamples; + ProfileCounters.BootstrapLatencyMilliseconds += ElapsedMs; + QueueConnectionEvent({ ConnectionEventType::BootstrapCompleted, Connection, NetworkTick, + static_cast(State.CriticalActors.size()), 0, ElapsedMs }); + } + + static UNetConnection* FindConnectionForController(APlayerController* PlayerController) { + if (!PlayerController || !NetDriver) + return nullptr; + + for (UNetConnection* Connection : NetDriver->ClientConnections) { + if (!IsOpenConnection(Connection) || Connection->PlayerController != PlayerController) + continue; + + auto StateIt = ConnectionBootstrapStates.find(Connection); + if (StateIt == ConnectionBootstrapStates.end()) { + StateIt = ConnectionBootstrapStates.emplace(Connection, + MakeConnectionBootstrapState(Connection, NetDriver->World)).first; + ++ProfileCounters.BootstrapStarts; + QueueConnectionEvent({ ConnectionEventType::Open, Connection, CurrentNetworkTick(), + static_cast(NetDriver->ClientConnections.Num()), 0, 0 }); + QueueConnectionEvent({ ConnectionEventType::BootstrapStarted, Connection, + CurrentNetworkTick(), 0, 0, 0 }); + } + else if (!IdentityMatches(StateIt->second.Connection, Connection) || + !IdentityMatches(StateIt->second.World, NetDriver->World) || + !IdentityMatches(StateIt->second.Controller, PlayerController)) { + RestartBootstrap(Connection, StateIt->second, NetDriver->World, + CurrentNetworkTick(), 1u, GetTickCount64()); + } + return Connection; + } + return nullptr; + } + + static ConnectionBootstrapState* FindStateForConnection( + UNetConnection* Connection, + APlayerController* PlayerController + ) { + if (!Connection || !PlayerController) + return nullptr; + + auto StateIt = ConnectionBootstrapStates.find(Connection); + if (StateIt == ConnectionBootstrapStates.end() || + ResolveObject(StateIt->second.Connection) != Connection || + ResolveObject(StateIt->second.Controller) != PlayerController) { + return nullptr; + } + return &StateIt->second; + } + + static FName ExpectedWorldPackageName() { + if (NetDriver && NetDriver->WorldPackage && ResolveObject(TrackObject(NetDriver->WorldPackage))) + return NetDriver->WorldPackage->Name; + + UObject* Outer = NetDriver ? NetDriver->World : nullptr; + while (Outer && Outer->Outer) + Outer = Outer->Outer; + return Outer ? Outer->Name : FName{}; + } + + void Configure(uint32_t CacheMaxAgeMilliseconds) { + ConsiderCacheMaxAgeMs = (std::clamp)(static_cast(CacheMaxAgeMilliseconds), 50ull, 5000ull); + } + + void NotifyDamageRpcObserved() { + ++ProfileCounters.UrgentDamageRpcMatches; + } + + void NotifyUrgentDamageTarget(int32_t ObjectIndex, int32_t ObjectSerialNumber) { + ++ProfileCounters.UrgentDamageInterruptCorrelations; + if (ObjectIndex < 0 || ObjectIndex >= UObject::GObjects->Num() || + ObjectSerialNumber == 0) { + ++ProfileCounters.UrgentDamageWeakHandlesInvalid; + return; + } + + FUObjectItem* Item = UObject::GObjects->GetItemByIndex(ObjectIndex); + if (!Item || !Item->Object || Item->SerialNumber != ObjectSerialNumber || + (static_cast(Item->Flags) & 0x30000000u) != 0) { + ++ProfileCounters.UrgentDamageWeakHandlesInvalid; + return; + } + + UObject* RawObject = Item->Object; + if ((RawObject->Flags & EObjectFlags::BeginDestroyed) || + (RawObject->Flags & EObjectFlags::FinishDestroyed) || + !RawObject->IsA(AActor::StaticClass())) { + ++ProfileCounters.UrgentDamageWeakHandlesInvalid; + return; + } + + AActor* Current = static_cast(RawObject); + UWorld* ExpectedWorld = NetDriver ? NetDriver->World : nullptr; + std::array Visited{}; + ObjectIdentity BehemothIdentity{}; + uint32_t BehemothDepth = 0; + + for (size_t Depth = 0; Current && Depth <= MaximumOwnerChainDepth; ++Depth) { + bool Cycle = false; + for (size_t VisitedIndex = 0; VisitedIndex < Depth; ++VisitedIndex) { + if (Visited[VisitedIndex] == Current) { + Cycle = true; + break; + } + } + if (Cycle) + break; + Visited[Depth] = Current; + + const ObjectIdentity CurrentIdentity = TrackObject(Current); + if (!IsStableIdentity(CurrentIdentity, Current) || + (ExpectedWorld && GetActorWorld(Current) != ExpectedWorld)) { + ++ProfileCounters.UrgentDamageWeakHandlesInvalid; + return; + } + if (Current->IsA(AArchonBehemoth::StaticClass())) { + BehemothIdentity = CurrentIdentity; + BehemothDepth = static_cast(Depth); + break; + } + + AActor* Owner = Current->Owner; + if (!Owner) + break; + const ObjectIdentity OwnerIdentity = TrackObject(Owner); + if (!IsStableIdentity(OwnerIdentity, Owner)) { + ++ProfileCounters.UrgentDamageWeakHandlesInvalid; + return; + } + Current = Owner; + } + + if (!BehemothIdentity.Object) { + ++ProfileCounters.UrgentDamageTargetsWithoutBehemoth; + return; + } + if (BehemothDepth == 0) + ++ProfileCounters.UrgentDamageDirectBehemothTargets; + else + ++ProfileCounters.UrgentDamageOwnerChainTargets; + + for (size_t Index = 0; Index < PendingUrgentDamageTargetCount; ++Index) { + const UrgentDamageTarget& Pending = PendingUrgentDamageTargets[Index]; + if (Pending.Reason == UrgentReplicationReason::Stagger && + IdentitiesEqual(Pending.Identity, BehemothIdentity) && + !Pending.TargetConnection.Object) { + ++ProfileCounters.UrgentDamageTargetsDeduplicated; + ++ProfileCounters.UrgentDeduplicatedByReason[ + UrgentReasonIndex(UrgentReplicationReason::Stagger)]; + return; + } + } + + size_t RetainedPendingCount = 0; + for (size_t Index = 0; Index < PendingUrgentDamageTargetCount; ++Index) { + const UrgentDamageTarget& Pending = PendingUrgentDamageTargets[Index]; + // A fresh authoritative interrupt supersedes any connection-local + // setup retry left from the previous interrupt for this behemoth. + if (Pending.Reason == UrgentReplicationReason::Stagger && + IdentitiesEqual(Pending.Identity, BehemothIdentity)) + continue; + PendingUrgentDamageTargets[RetainedPendingCount++] = Pending; + } + PendingUrgentDamageTargetCount = RetainedPendingCount; + if (PendingUrgentDamageTargetCount >= PendingUrgentDamageTargets.size()) { + ++ProfileCounters.UrgentDamageTargetsDropped; + return; + } + + PendingUrgentDamageTargets[PendingUrgentDamageTargetCount++] = { + BehemothIdentity, {}, GetTickCount64(), 0, + UrgentReplicationReason::Stagger + }; + ++ProfileCounters.UrgentDamageTargetsQueued; + ++ProfileCounters.UrgentQueuedByReason[ + UrgentReasonIndex(UrgentReplicationReason::Stagger)]; + } + + void NotifyBehemothInterruptClientRpc() { + ++ProfileCounters.BehemothInterruptClientNotifications; + } + + void QueueUrgentActor(AActor* Actor, UrgentReplicationReason Reason, + APlayerController* TargetController) { + const size_t ReasonIndex = UrgentReasonIndex(Reason); + const ObjectIdentity ActorIdentity = TrackObject(Actor); + UWorld* ExpectedWorld = NetDriver ? NetDriver->World : nullptr; + if (!ActorIdentity.Object || ResolveObject(ActorIdentity) != Actor || + (ExpectedWorld && GetActorWorld(Actor) != ExpectedWorld)) { + ++ProfileCounters.UrgentInvalidatedByReason[ReasonIndex]; + return; + } + + ObjectIdentity ConnectionIdentity{}; + if (TargetController) { + UNetConnection* Connection = FindConnectionForController(TargetController); + ConnectionIdentity = TrackObject(Connection); + if (!ConnectionIdentity.Object || ResolveObject(ConnectionIdentity) != Connection) { + ++ProfileCounters.UrgentInvalidatedByReason[ReasonIndex]; + return; + } + } + + for (size_t Index = 0; Index < PendingUrgentDamageTargetCount; ++Index) { + const UrgentDamageTarget& Pending = PendingUrgentDamageTargets[Index]; + if (Pending.Reason == Reason && + IdentitiesEqual(Pending.Identity, ActorIdentity) && + IdentitiesEqual(Pending.TargetConnection, ConnectionIdentity)) { + ++ProfileCounters.UrgentDeduplicatedByReason[ReasonIndex]; + ++ProfileCounters.UrgentDamageTargetsDeduplicated; + return; + } + } + + if (PendingUrgentDamageTargetCount >= PendingUrgentDamageTargets.size()) { + ++ProfileCounters.UrgentDroppedByReason[ReasonIndex]; + ++ProfileCounters.UrgentDamageTargetsDropped; + return; + } + + PendingUrgentDamageTargets[PendingUrgentDamageTargetCount++] = { + ActorIdentity, ConnectionIdentity, GetTickCount64(), 0, Reason + }; + ++ProfileCounters.UrgentQueuedByReason[ReasonIndex]; + ++ProfileCounters.UrgentDamageTargetsQueued; + } + + void RecordCombatEvent(CombatEvent Event, bool Succeeded, uint32_t Amount) { + const size_t Index = static_cast(Event); + if (Index >= CombatEventCount || Amount == 0) + return; + ProfileCounters.CombatEventCalls[Index] += Amount; + if (Succeeded) + ProfileCounters.CombatEventSuccesses[Index] += Amount; + } + + void RecordCombatTextQueue(const FArchonMultitypeCombatTextEntry& Entry) { + RecordCombatEvent(CombatEvent::CombatTextQueued, true); + ++ProfileCounters.CombatTextEntries; + ProfileCounters.CombatTextEstimatedBytes += sizeof(Entry); + } + + void RecordCombatTextMulticast(uint32_t EntryCount) { + RecordCombatEvent(CombatEvent::CombatTextMulticast, true, + EntryCount > 0 ? EntryCount : 1); + ProfileCounters.CombatTextMulticastEntries += EntryCount; + ProfileCounters.CombatTextMulticastEstimatedBytes += + static_cast(EntryCount) * sizeof(FArchonMultitypeCombatTextEntry); + } + + void NotifyClientLoadedWorld(APlayerController* PlayerController, FName WorldPackageName) { + if (!PlayerController) + return; + + UNetConnection* Connection = FindConnectionForController(PlayerController); + ConnectionBootstrapState* State = FindStateForConnection(Connection, PlayerController); + if (!Connection || !State) + return; + + const FName ExpectedPackage = ExpectedWorldPackageName(); + const ULONGLONG NowMs = GetTickCount64(); + const ULONGLONG ElapsedMs = NowMs - State->StartedAtMs; + if (ExpectedPackage.IsNone() || WorldPackageName != ExpectedPackage) { + ++ProfileCounters.LoadedWorldMismatched; + QueueConnectionEvent({ ConnectionEventType::LoadedWorldMismatch, Connection, CurrentNetworkTick(), + static_cast(WorldPackageName.ComparisonIndex), + static_cast(ExpectedPackage.ComparisonIndex), ElapsedMs }); + return; + } + + if (State->LoadedWorldAtMs && State->LastLoadedWorldPackage == WorldPackageName) + return; + + ++ProfileCounters.LoadedWorldAccepted; + ++ProfileCounters.LoadedWorldLatencySamples; + ProfileCounters.LoadedWorldLatencyMilliseconds += ElapsedMs; + QueueConnectionEvent({ ConnectionEventType::LoadedWorldAccepted, Connection, CurrentNetworkTick(), + static_cast(WorldPackageName.ComparisonIndex), WorldPackageName.Number, + ElapsedMs }); + + // This RPC is a seamless-travel lifecycle hint in this build. It may + // restart the bounded extra eligibility window, but never gates the + // ordinary replication pass. + RestartBootstrap(Connection, *State, NetDriver ? NetDriver->World : nullptr, + CurrentNetworkTick(), 3u, NowMs); + State->LoadedWorldAtMs = NowMs; + State->LastLoadedWorldPackage = WorldPackageName; + } + + void NotifyClientAcknowledgedPawn(APlayerController* PlayerController, APawn* Pawn) { + if (!PlayerController || !Pawn || PlayerController->Pawn != Pawn) + return; + + UNetConnection* Connection = FindConnectionForController(PlayerController); + ConnectionBootstrapState* State = FindStateForConnection(Connection, PlayerController); + if (!Connection || !State) + return; + + const ULONGLONG NowMs = GetTickCount64(); + if (!IdentityMatches(State->Pawn, Pawn)) + RestartBootstrap(Connection, *State, NetDriver ? NetDriver->World : nullptr, + CurrentNetworkTick(), 2u, NowMs); + if (State->PossessionAcknowledged) + return; + + State->PossessionAcknowledged = true; + State->PossessionAcknowledgedAtMs = NowMs; + ++ProfileCounters.PossessionAcknowledged; + QueueConnectionEvent({ ConnectionEventType::PossessionAcknowledged, Connection, CurrentNetworkTick(), + 0, 0, NowMs - State->StartedAtMs }); +} diff --git a/UndauntedInternalServer/Networking/Private/RuntimeState.inl b/UndauntedInternalServer/Networking/Private/RuntimeState.inl new file mode 100644 index 0000000..85e87db --- /dev/null +++ b/UndauntedInternalServer/Networking/Private/RuntimeState.inl @@ -0,0 +1,641 @@ +#include "../Networking.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace SDK; + +namespace Networking { + UNetDriver* NetDriver = nullptr; + static std::string LastListenError; + static bool NativeDispatchOwnershipMismatchLogged = false; + static FName NativeGameNetDriverName{}; + static bool NativeOwnershipConflictLogged = false; + static LifecycleEventSink LifecycleSink = nullptr; + + void ConfigureLifecycleEventSink(LifecycleEventSink Sink) { + LifecycleSink = Sink; + } + + static bool LifecycleEventSinkEnabled() { + return LifecycleSink != nullptr; + } + + static void RecordLifecycleEvent(const std::string& EventName, + const std::string& FieldsJson) { + if (LifecycleSink != nullptr) + LifecycleSink(EventName, FieldsJson); + } + + namespace { + constexpr bool kAlwaysReplicateCriticalActors = true; + + constexpr uintptr_t OffsetActorGetWorldVTable = 0x150; + // AActor::CallPreReplication. Native ServerReplicateActors invokes this + // once per considered actor and replication frame before any connection + // serializes the actor. It refreshes ReplicatedMovement and calls the + // actor/component PreReplication virtuals. + constexpr uintptr_t OffsetActorCallPreReplication = 0x306B150; + constexpr uintptr_t OffsetCreateNamedNetDriver = 0x371A5E0; + constexpr uintptr_t OffsetFindNamedNetDriver = 0x37213A0; + constexpr uintptr_t OffsetSetNetDriverWorld = 0x3491890; + constexpr uintptr_t OffsetNetDriverNotify = 0x208; + constexpr uintptr_t OffsetNetDriverTickCount = 0x2AC; + constexpr uintptr_t OffsetConnectionState = 0x134; + constexpr uintptr_t OffsetChannelFlags = 0x30; + // Verified UIpNetDriver vtable slots in this build: +0x268 is the + // trivial IsAvailable method, while +0x280 is InitListen and invokes + // InitBase before creating/binding the socket. + constexpr uintptr_t OffsetNetDriverInitListen = 0x280; + constexpr uintptr_t OffsetCreateChannelByName = 0x3449E10; + constexpr uintptr_t OffsetSetChannelActor = 0x3283450; + constexpr uintptr_t OffsetActorChannelReplicationFlags = 0x90; + constexpr uintptr_t OffsetActorChannelReplicateActor = 0x327E860; + constexpr uintptr_t OffsetPlayerControllerUpdateCamera = 0x359F9D0; + constexpr uintptr_t OffsetActorIsNetRelevantFor = 0x307C970; + + constexpr uint32_t OpenChannelFlag = 1 << 1; + constexpr uint32_t ReplicationFlagNeedsTick = 2u; + constexpr uint32_t OpenConnectionState = 3; + constexpr uint32_t ChannelOpenAcknowledgedMask = 0x801u; + constexpr ULONGLONG BootstrapMinimumDurationMs = 3000; + constexpr ULONGLONG BootstrapMaximumDurationMs = 5000; + constexpr ULONGLONG BootstrapStableMs = 1000; + constexpr ULONGLONG DefaultConsiderCacheMaxAgeMs = 250; + constexpr size_t MaximumOwnerChainDepth = 8; + constexpr size_t MaximumScheduledActorsPerConnection = 4096; + constexpr size_t MaximumUrgentDamageTargets = 32; + constexpr ULONGLONG UrgentDamageTargetMaximumAgeMs = 1000; + constexpr uint8_t AdaptiveNoDataThreshold = 4; + constexpr float SchedulerMaximumFrequencyHz = 30.0f; + // Once bootstrap has completed, quiet non-core owner-chain actors may + // follow a conservative half-rate floor. Core presentation/combat + // classes remain at 30 Hz and any produced data restores full rate. + constexpr float OwnedCriticalAdaptiveFloorHz = 15.0f; + constexpr ULONGLONG StaticRelevancyAuditMs = 1000; + constexpr ULONGLONG IrrelevantRecheckMs = 250; + constexpr uint16_t ImmediateMetricOverflowSlot = + static_cast(ImmediateMetricTrackedClassCount); + constexpr uint16_t ImmediateMetricInvalidSlot = + static_cast(ImmediateMetricTrackedClassCount + 1); + constexpr uint16_t ImmediateMetricNotApplicable = 0xFFFF; + + using GetActorWorldFn = UWorld* (*)(AActor*); + using CallActorPreReplicationFn = void (*)(AActor*, UNetDriver*); + using CreateNamedNetDriverFn = uint8_t (*)(UEngine*, void*, FName, FName); + using FindNamedNetDriverFn = UNetDriver* (*)(UEngine*, void*, FName); + using SetNetDriverWorldFn = void (*)(UNetDriver*, UWorld*); + using InitListenFn = bool (*)(UNetDriver*, void*, FURL*, bool, FString*); + using CreateChannelByNameFn = UActorChannel* (*)(UNetConnection*, FName*, unsigned int, int); + using SetChannelActorFn = void (*)(UActorChannel*, AActor*, unsigned int); + using ReplicateActorFn = bool (*)(UActorChannel*); + using UpdateCameraFn = void (*)(APlayerController*); + using IsNetRelevantForFn = bool (*)(AActor*, AActor*, AActor*, const FVector*); + + enum class ReplicationBucket : uint8_t { + Immediate, + ArchonCharacter, + ArchonBehemoth, + Projectile, + PawnOrCharacter, + MovementActor + }; + + enum ReplicationClassFlag : uint32_t { + ClassPlayerController = 1u << 0, + ClassArchonCharacter = 1u << 1, + ClassArchonBehemoth = 1u << 2, + ClassArchonLantern = 1u << 3, + ClassProjectile = 1u << 4, + ClassPawn = 1u << 5, + ClassArchonEquipment = 1u << 6, + ClassArchonLoadout = 1u << 7, + ClassArchonAoe = 1u << 8, + ClassArchonBeam = 1u << 9, + ClassAbilityActor = 1u << 10 + }; + + struct TransformSample { + FVector Location{}; + FRotator Rotation{}; + bool Valid = false; + }; + + struct ObjectIdentity { + UObject* Object = nullptr; + int32_t Index = -1; + int32_t SerialNumber = 0; + }; + + struct ActorReplicationState { + ObjectIdentity Identity{}; + uint32_t LastPreReplicationTick = 0; + uint16_t ConsecutiveImmediateNoDataPasses = 0; + uint32_t ImmediatePassAttempts = 0; + uint32_t ImmediatePassSuccesses = 0; + uint32_t ImmediatePassNetworkTick = 0; + uint32_t ImmediatePassFinalizedTick = 0; + uint32_t ImmediateMetricGeneration = 0; + uint16_t ImmediateMetricSlot = ImmediateMetricNotApplicable; + }; + + struct ReplicationClassCache { + UClass* PlayerController = nullptr; + UClass* ArchonCharacter = nullptr; + UClass* ArchonBehemoth = nullptr; + UClass* ArchonLantern = nullptr; + UClass* ArchonEquipment = nullptr; + UClass* Projectile = nullptr; + UClass* Pawn = nullptr; + UClass* PlayerState = nullptr; + UClass* ArchonLoadout = nullptr; + UClass* ArchonInventory = nullptr; + UClass* ArchonBuff = nullptr; + UClass* ArchonAoe = nullptr; + UClass* ArchonBeam = nullptr; + UClass* AbilityActor = nullptr; + UClass* MonsterPartActor = nullptr; + }; + + struct ActorScheduleState; + + struct ReplicationCandidate { + AActor* Actor = nullptr; + ReplicationBucket Bucket = ReplicationBucket::Immediate; + uint32_t ClassFlags = 0; + bool IsPlayerController = false; + bool IsCritical = false; + ActorReplicationState* ReplicationState = nullptr; + uint64_t LastReplicatedConnectionPass = 0; + }; + + struct PrioritizedCandidate { + ReplicationCandidate* Candidate = nullptr; + float Priority = 0.0f; + ActorScheduleState* State = nullptr; + }; + + enum class ConnectionBootstrapPhase : uint8_t { + Bootstrapping, + Active + }; + + enum class InitializationActorRole : uint8_t { + Controller, + GameState, + PlayerState, + Pawn, + ViewTarget, + OwnedActor + }; + + struct CriticalActorState { + ObjectIdentity Identity{}; + InitializationActorRole Role = InitializationActorRole::OwnedActor; + uint8_t OwnerDepth = 0; + bool ChannelAcknowledged = false; + }; + + struct ConnectionBootstrapState { + ObjectIdentity Connection{}; + ObjectIdentity World{}; + ObjectIdentity Controller{}; + ObjectIdentity Pawn{}; + ConnectionBootstrapPhase Phase = ConnectionBootstrapPhase::Bootstrapping; + ULONGLONG StartedAtMs = 0; + ULONGLONG LoadedWorldAtMs = 0; + ULONGLONG PossessionAcknowledgedAtMs = 0; + ULONGLONG LastCriticalActorDiscoveredAtMs = 0; + FName LastLoadedWorldPackage{}; + bool PossessionAcknowledged = false; + std::unordered_map CriticalActors{}; + }; + + struct ImmediateClassRegistryEntry { + UClass* Class = nullptr; + int32_t Index = -1; + int32_t SerialNumber = 0; + }; + + struct ActorScheduleState { + ObjectIdentity Actor{}; + ObjectIdentity Owner{}; + AActor* OwnerPointer = nullptr; + TransformSample LastSample{}; + uint8_t ConsecutiveNoDataCalls = 0; + ULONGLONG LastConsideredAtMs = 0; + ULONGLONG LastAttemptAtMs = 0; + ULONGLONG LastSuccessfulDataAtMs = 0; + ULONGLONG NextFrequencyDeadlineMs = 0; + ULONGLONG NextRelevancyCheckMs = 0; + ULONGLONG LastRelevancyCheckMs = 0; + ENetDormancy LastDormancy = ENetDormancy::DORM_MAX; + bool OwnerTrackable = true; + bool RelevancyKnown = false; + bool LastRelevant = true; + bool TemporaryRetired = false; + bool TearOffRetired = false; + }; + + struct ConnectionScheduleState { + ObjectIdentity Connection{}; + ObjectIdentity World{}; + ObjectIdentity ViewTarget{}; + std::unordered_map Actors{}; + }; + + enum class LivePolicyDecision : uint8_t { + Eligible, + InvalidIdentity, + DriverMismatch, + Dormant, + NotRelevant, + NotDue, + TemporaryRetired, + TearOffRetired + }; + + enum class CacheRebuildReason : uint8_t { + Forced, + WorldChanged, + WorldIdentity, + MaximumAge, + LevelCount, + ActorCount + }; + + struct LivePolicyResult { + LivePolicyDecision Decision = LivePolicyDecision::InvalidIdentity; + ActorScheduleState* State = nullptr; + bool Moved = false; + }; + + struct UrgentDamageTarget { + ObjectIdentity Identity{}; + ObjectIdentity TargetConnection{}; + ULONGLONG QueuedAtMs = 0; + uint8_t SetupRetries = 0; + UrgentReplicationReason Reason = UrgentReplicationReason::Stagger; + }; + + struct UrgentCandidateEntry { + ReplicationCandidate* Candidate = nullptr; + ObjectIdentity Identity{}; + ObjectIdentity TargetConnection{}; + ULONGLONG QueuedAtMs = 0; + uint8_t SetupRetries = 0; + UrgentReplicationReason Reason = UrgentReplicationReason::Stagger; + }; + + struct UrgentCandidateBatch { + std::array Entries{}; + size_t Count = 0; + }; + } + + static uintptr_t BaseAddress = 0x0; + static std::vector ConsiderCache{}; + static std::unordered_map CandidateActorLookup{}; + static std::vector PriorityScratch{}; + static std::vector CachedLevelActorCounts{}; + static UWorld* CachedWorld = nullptr; + static ObjectIdentity CachedWorldIdentity{}; + static ULONGLONG LastConsiderCacheBuildMs = 0; + static std::unordered_map ScratchActorChannels{}; + static std::unordered_map ActorReplicationStates{}; + static std::unordered_map ConnectionBootstrapStates{}; + static std::unordered_map ConnectionScheduleStates{}; + static std::unordered_set CurrentConnections{}; + static std::vector LiveConnections{}; + static ProfilingCounters ProfileCounters{}; + static uint32_t CurrentSchedulerActorStates = 0; + static IsNetRelevantForFn NativeIsNetRelevantFor = nullptr; + static CallActorPreReplicationFn NativeCallActorPreReplication = nullptr; + static bool NativeRelevancySignatureValid = false; + static bool NativePreReplicationSignatureValid = false; + static ULONGLONG ConsiderCacheMaxAgeMs = DefaultConsiderCacheMaxAgeMs; + static bool ConsiderCacheDirtyForNextConnection = true; + static bool ActorPropertyDistributionDirty = true; + static uint32_t ImmediateMetricGeneration = 1; + static std::array + ImmediateClassRegistry{}; + static std::array PendingUrgentDamageTargets{}; + static size_t PendingUrgentDamageTargetCount = 0; + static uint64_t CurrentConnectionPass = 0; + + enum class ConnectionEventType : uint8_t { + Open, + Close, + BootstrapStarted, + BootstrapRestarted, + LoadedWorldAccepted, + LoadedWorldMismatch, + PossessionAcknowledged, + CriticalActorDiscovered, + CriticalChannelAcknowledged, + BootstrapCompleted, + BootstrapDeadline, + NativePostLogin, + NativeReciprocalRepair, + NativeDispatchConflict + }; + struct ConnectionEvent { + ConnectionEventType Type{}; + UNetConnection* Connection = nullptr; + uint32_t NetworkTick = 0; + uint32_t ValueA = 0; + uint32_t ValueB = 0; + uint64_t ElapsedMilliseconds = 0; + }; + static std::array PendingConnectionEvents{}; + static size_t PendingConnectionEventCount = 0; + + static int64_t PerformanceTimestamp() { + LARGE_INTEGER Value{}; + QueryPerformanceCounter(&Value); + return Value.QuadPart; + } + + static uint64_t ElapsedMicroseconds(int64_t StartedAt) { + static const int64_t Frequency = []() { + LARGE_INTEGER Value{}; + QueryPerformanceFrequency(&Value); + return Value.QuadPart; + }(); + + const int64_t Elapsed = PerformanceTimestamp() - StartedAt; + return Frequency > 0 && Elapsed > 0 + ? static_cast((Elapsed * 1000000ll) / Frequency) + : 0ull; + } + + static std::string PointerValue(const void* Value) { + return std::to_string(reinterpret_cast(Value)); + } + + static const char* InitializationRoleName(uint32_t Value) { + switch (static_cast(Value)) { + case InitializationActorRole::Controller: return "controller"; + case InitializationActorRole::GameState: return "game_state"; + case InitializationActorRole::PlayerState: return "player_state"; + case InitializationActorRole::Pawn: return "pawn"; + case InitializationActorRole::ViewTarget: return "view_target"; + default: return "owned_actor"; + } + } + + static bool IsOpenConnection(UNetConnection* Connection) { + return Connection + && Connection->OwningActor + && *reinterpret_cast((uintptr_t)Connection + OffsetConnectionState) == OpenConnectionState; + } + + static ObjectIdentity TrackObject(UObject* Object) { + if (!Object || Object->Index < 0) + return {}; + + FUObjectItem* Item = UObject::GObjects->GetItemByIndex(Object->Index); + if (!Item || Item->Object != Object) + return {}; + + return { Object, Object->Index, Item->SerialNumber }; + } + + static UObject* ResolveObject(const ObjectIdentity& Identity) { + if (!Identity.Object || Identity.Index < 0) + return nullptr; + + FUObjectItem* Item = UObject::GObjects->GetItemByIndex(Identity.Index); + if (!Item || Item->Object != Identity.Object || Item->SerialNumber != Identity.SerialNumber || + (static_cast(Item->Flags) & 0x30000000u) != 0) + return nullptr; + + UObject* Object = Item->Object; + if ((Object->Flags & EObjectFlags::BeginDestroyed) || (Object->Flags & EObjectFlags::FinishDestroyed)) + return nullptr; + + return Object; + } + + static bool IdentityMatches(const ObjectIdentity& Stored, UObject* Current) { + if (!Current) + return !Stored.Object; + + const ObjectIdentity Fresh = TrackObject(Current); + return Fresh.Object == Current + && Stored.Object == Fresh.Object + && Stored.Index == Fresh.Index + && Stored.SerialNumber == Fresh.SerialNumber; + } + + static bool IdentitiesEqual(const ObjectIdentity& Left, const ObjectIdentity& Right) { + return Left.Object == Right.Object + && Left.Index == Right.Index + && Left.SerialNumber == Right.SerialNumber; + } + + static size_t UrgentReasonIndex(UrgentReplicationReason Reason) { + const size_t Index = static_cast(Reason); + return Index < UrgentReplicationReasonCount ? Index : 0; + } + + static bool IsStableIdentity(const ObjectIdentity& Identity, const UObject* Expected) { + return Expected && ResolveObject(Identity) == Expected; + } + + static ConnectionScheduleState MakeConnectionScheduleState( + UNetConnection* Connection, + UWorld* World + ) { + ConnectionScheduleState State{}; + State.Connection = TrackObject(Connection); + State.World = TrackObject(World); + State.ViewTarget = TrackObject(Connection ? Connection->ViewTarget : nullptr); + State.Actors.reserve((std::min)(MaximumScheduledActorsPerConnection, + (std::max)(size_t{ 256 }, ConsiderCache.size()))); + return State; + } + + static void ClearConnectionScheduleState(UNetConnection* Connection) { + const auto It = ConnectionScheduleStates.find(Connection); + if (It == ConnectionScheduleStates.end()) + return; + + const uint32_t Removed = static_cast(It->second.Actors.size()); + CurrentSchedulerActorStates = Removed <= CurrentSchedulerActorStates + ? CurrentSchedulerActorStates - Removed + : 0; + ConnectionScheduleStates.erase(It); + } + + static uint16_t RegisterImmediateClass(UClass* Class) { + const ObjectIdentity Identity = TrackObject(Class); + if (!Identity.Object || ResolveObject(Identity) != Class) + return ImmediateMetricInvalidSlot; + + const uint32_t IdentityHash = + static_cast(Identity.Index) * 0x9E3779B1u + ^ static_cast(Identity.SerialNumber); + for (size_t Probe = 0; Probe < ImmediateClassRegistry.size(); ++Probe) { + const size_t Slot = (IdentityHash + Probe) % ImmediateClassRegistry.size(); + ImmediateClassRegistryEntry& Entry = ImmediateClassRegistry[Slot]; + if (Entry.Index == Identity.Index && Entry.SerialNumber == Identity.SerialNumber) + return static_cast(Slot); + if (Entry.Index >= 0) + continue; + + Entry.Class = Class; + Entry.Index = Identity.Index; + Entry.SerialNumber = Identity.SerialNumber; + return static_cast(Slot); + } + return ImmediateMetricOverflowSlot; + } + + static size_t ImmediateNoDataBand(uint16_t Streak) { + if (Streak <= 1) return 0; + if (Streak <= 3) return 1; + if (Streak <= 15) return 2; + if (Streak <= 63) return 3; + if (Streak <= 255) return 4; + return 5; + } + + static size_t NetFrequencyBand(float Frequency) { + if (!std::isfinite(Frequency)) return 0; + if (Frequency <= 0.0f) return 1; + if (Frequency <= 2.0f) return 2; + if (Frequency <= 5.0f) return 3; + if (Frequency <= 10.0f) return 4; + if (Frequency <= 15.0f) return 5; + if (Frequency <= 30.0f) return 6; + if (Frequency <= 60.0f) return 7; + if (Frequency <= 100.0f) return 8; + return 9; + } + + static size_t NetDormancyBand(ENetDormancy Dormancy) { + const size_t Value = static_cast(Dormancy); + return Value < NetDormancyBandCount - 1 ? Value : NetDormancyBandCount - 1; + } + + static void RecordActorPropertyDistribution( + AActor* Actor, + ReplicationBucket Bucket, + bool IsCritical + ) { + if (!Actor) + return; + + ++ProfileCounters.ActorPropertySamples; + ++ProfileCounters.NetUpdateFrequencyBands[NetFrequencyBand(Actor->NetUpdateFrequency)]; + ++ProfileCounters.MinNetUpdateFrequencyBands[NetFrequencyBand(Actor->MinNetUpdateFrequency)]; + ++ProfileCounters.NetDormancyBands[NetDormancyBand(Actor->NetDormancy)]; + if (Actor->Owner) ++ProfileCounters.OwnedActorSamples; + if (Actor->bReplicateMovement) ++ProfileCounters.ReplicatedMovementSamples; + if (IsCritical) ++ProfileCounters.CriticalActorSamples; + if (Bucket == ReplicationBucket::Immediate) ++ProfileCounters.ImmediateActorSamples; + if (Actor->bNetTemporary) ++ProfileCounters.NetTemporarySamples; + if (Actor->bNetStartup) ++ProfileCounters.NetStartupSamples; + if (Actor->bNetLoadOnClient) ++ProfileCounters.NetLoadOnClientSamples; + if (Actor->bOnlyRelevantToOwner) ++ProfileCounters.OnlyRelevantToOwnerSamples; + if (Actor->bAlwaysRelevant) ++ProfileCounters.AlwaysRelevantSamples; + if (Actor->bNetUseOwnerRelevancy) ++ProfileCounters.OwnerRelevancySamples; + if (Actor->bTearOff) ++ProfileCounters.TearOffSamples; + } + + static uint16_t ResolveImmediateMetricSlot( + AActor* Actor, + ActorReplicationState* State + ) { + if (!Actor || !Actor->Class || !State) + return ImmediateMetricInvalidSlot; + if (State->ImmediateMetricGeneration != ImmediateMetricGeneration) { + State->ImmediateMetricGeneration = ImmediateMetricGeneration; + State->ImmediateMetricSlot = RegisterImmediateClass(Actor->Class); + } + return State->ImmediateMetricSlot; + } + + static void RecordClassReplicationResult( + AActor* Actor, + ActorReplicationState* State, + ReplicationBucket Bucket, + bool ProducedData + ) { + uint16_t MetricSlot = ResolveImmediateMetricSlot(Actor, State); + if (MetricSlot >= ProfileCounters.ImmediateClasses.size()) + MetricSlot = ImmediateMetricInvalidSlot; + + ImmediateClassCounters& Counters = ProfileCounters.ImmediateClasses[MetricSlot]; + ++Counters.Attempts; + if (Bucket == ReplicationBucket::Immediate && State && + State->ImmediatePassAttempts < (std::numeric_limits::max)()) + ++State->ImmediatePassAttempts; + if (ProducedData) { + ++Counters.Successes; + if (Bucket == ReplicationBucket::Immediate && State && + State->ImmediatePassSuccesses < (std::numeric_limits::max)()) { + ++State->ImmediatePassSuccesses; + } + } + } + + static void PopulateImmediateClassNames(ProfilingCounters& Snapshot) { + for (size_t Index = 0; Index < ImmediateClassRegistry.size(); ++Index) { + const ImmediateClassRegistryEntry& Entry = ImmediateClassRegistry[Index]; + if (Entry.Index < 0) + continue; + + // Name work is intentionally deferred until after the replication + // pass. Resolve the interval-scoped class identity again rather than + // trusting a pointer retained by the hot-path registry. + const ObjectIdentity Identity{ + Entry.Class, + Entry.Index, + Entry.SerialNumber + }; + UObject* Resolved = ResolveObject(Identity); + const std::string Name = Resolved + ? static_cast(Resolved)->GetName() + : "invalidated"; + auto& Destination = Snapshot.ImmediateClasses[Index].ClassName; + const size_t CopyLength = (std::min)(Name.size(), Destination.size() - 1); + if (CopyLength) + std::memcpy(Destination.data(), Name.data(), CopyLength); + } + + constexpr char OverflowName[] = "overflow"; + constexpr char InvalidName[] = "invalid"; + std::memcpy(Snapshot.ImmediateClasses[ImmediateMetricOverflowSlot].ClassName.data(), + OverflowName, sizeof(OverflowName)); + std::memcpy(Snapshot.ImmediateClasses[ImmediateMetricInvalidSlot].ClassName.data(), + InvalidName, sizeof(InvalidName)); + } + + static void ResetImmediateClassRegistry() { + ImmediateClassRegistry = {}; + if (++ImmediateMetricGeneration == 0) + ImmediateMetricGeneration = 1; + } + + static ConnectionBootstrapState MakeConnectionBootstrapState(UNetConnection* Connection, UWorld* World) { + ConnectionBootstrapState State{}; + State.Connection = TrackObject(Connection); + State.World = TrackObject(World); + State.Controller = TrackObject(Connection ? Connection->PlayerController : nullptr); + State.Pawn = TrackObject(Connection && Connection->PlayerController ? Connection->PlayerController->Pawn : nullptr); + State.Phase = ConnectionBootstrapPhase::Bootstrapping; + State.StartedAtMs = GetTickCount64(); + State.LastCriticalActorDiscoveredAtMs = State.StartedAtMs; + State.CriticalActors.reserve(32); + return State; +} diff --git a/UndauntedInternalServer/SDK/Basic.hpp b/UndauntedInternalServer/SDK/Basic.hpp index 478f897..69d9870 100644 --- a/UndauntedInternalServer/SDK/Basic.hpp +++ b/UndauntedInternalServer/SDK/Basic.hpp @@ -222,7 +222,10 @@ struct FUObjectItem final { public: class UObject* Object; // 0x0000(0x0008)(NOT AUTO-GENERATED PROPERTY) - uint8 Pad_8[0x10]; // 0x0008(0x0010)(Fixing Struct Size After Last Property [ Dumper-7 ]) + int32 Flags; // 0x0008(0x0004)(NOT AUTO-GENERATED PROPERTY) + int32 ClusterRootIndex; // 0x000C(0x0004)(NOT AUTO-GENERATED PROPERTY) + int32 SerialNumber; // 0x0010(0x0004)(NOT AUTO-GENERATED PROPERTY) + uint8 Pad_14[0x4]; // 0x0014(0x0004)(Fixing Struct Size After Last Property [ Dumper-7 ]) }; DUMPER7_ASSERTS_FUObjectItem; @@ -269,6 +272,18 @@ class TUObjectArray final return ChunkPtr[InChunkIdx].Object; } + + inline FUObjectItem* GetItemByIndex(const int32 Index) const + { + const int32 ChunkIndex = Index / ElementsPerChunk; + const int32 InChunkIdx = Index % ElementsPerChunk; + + if (Index < 0 || ChunkIndex >= NumChunks || Index >= NumElements) + return nullptr; + + FUObjectItem* ChunkPtr = GetDecrytedObjPtr()[ChunkIndex]; + return ChunkPtr ? &ChunkPtr[InChunkIdx] : nullptr; + } }; DUMPER7_ASSERTS_TUObjectArray; diff --git a/UndauntedInternalServer/SDK/CoreUObject_functions.cpp b/UndauntedInternalServer/SDK/CoreUObject_functions.cpp index 428f214..4a47ae7 100644 --- a/UndauntedInternalServer/SDK/CoreUObject_functions.cpp +++ b/UndauntedInternalServer/SDK/CoreUObject_functions.cpp @@ -194,7 +194,6 @@ class UFunction* UClass::GetFunction(const char* ClassName, const char* FuncName { for(const UStruct* Clss = this; Clss; Clss = Clss->SuperStruct) { - std::cout << Clss->GetName() << std::endl; if (Clss->GetName() != ClassName) continue; diff --git a/UndauntedInternalServer/ServerPacing.cpp b/UndauntedInternalServer/ServerPacing.cpp new file mode 100644 index 0000000..9d0b50b --- /dev/null +++ b/UndauntedInternalServer/ServerPacing.cpp @@ -0,0 +1,875 @@ +#include "ServerPacing.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "MinHook/MinHook.h" + +namespace ServerPacing { + namespace { + constexpr uintptr_t PlatformSleepRva = 0x1D85370; + constexpr uintptr_t CoarseSleepCallRva = 0x373F044; + constexpr uintptr_t CoarseSleepReturnRva = 0x373F049; + constexpr uintptr_t YieldSleepCallRva = 0x373F074; + constexpr uintptr_t YieldSleepReturnRva = 0x373F079; + // FUN_14373EDA0 writes double values to each of these globals after + // completing the native coarse-wait plus QPC/yield pacing loop. + constexpr uintptr_t NativePacingWaitRva = 0x5E6BCD0; + constexpr uintptr_t NativePacingOvershootRva = 0x5E6BCD8; + constexpr uintptr_t NativeDeltaSecondsRva = 0x5971690; + + constexpr uint32_t ExpectedExecutableTimestamp = 0x5F9A37D4; + constexpr uint32_t ExpectedExecutableImageSize = 0x066CB000; + constexpr double ExpectedMaxFps = 30.0; + constexpr double MaxFpsTolerance = 0.5; + constexpr double ActivationMaximumCadenceHz = 28.0; + constexpr double ActiveMinimumAcceptedCadenceHz = 29.5; + constexpr double ActivationMaximumEngineWorkP95Milliseconds = 5.0; + constexpr double ActivationMinimumOvershootMedianMilliseconds = 2.0; + constexpr double ExcessiveFineSpinP95Milliseconds = 5.0; + constexpr double GuardExtensionMilliseconds = 1.0; + constexpr double GuardActivationMinimumCadenceHz = 29.5; + constexpr double GuardActivationMaximumCadenceHz = 30.5; + constexpr double GuardActivationMinimumFineSpinP95Milliseconds = 1.5; + constexpr double GuardMaximumNativeOvershootP95Milliseconds = 1.0; + constexpr double GuardMinimumFineSpinImprovementMilliseconds = 0.5; + constexpr uint32_t GuardBadWindowLimit = 2; + constexpr std::size_t MinimumCoarseSamplesPerWindow = 96; + constexpr uint32_t UnimprovedWindowLimit = 2; + constexpr double CorrectedWaitTimeoutSlackMilliseconds = 25.0; + constexpr DWORD CorrectedWaitMaximumTimeoutMilliseconds = 250; + constexpr DWORD CreateWaitableTimerHighResolution = 0x00000002; + + constexpr uint8_t ExpectedPlatformSleepSignature[] = { + 0xF3, 0x0F, 0x59, 0x05, 0xB8, 0x01, 0x63, 0x02, + 0xF3, 0x48, 0x0F, 0x2C, 0xC0, 0x85, 0xC0, 0x75, + 0x07, 0x48, 0xFF, 0x25, 0xA8, 0x60, 0x59, 0x02, + 0x8B, 0xC8, 0x48, 0xFF, 0x25, 0x67, 0x66, 0x59, + 0x02 + }; + constexpr uint8_t ExpectedCoarseCallSignature[] = { + 0xF3, 0x0F, 0x5C, 0x05, 0xB8, 0xCB, 0xD7, 0x00, + 0xE8, 0x27, 0x63, 0x64, 0xFE + }; + constexpr uint8_t ExpectedYieldCallSignature[] = { + 0x41, 0x0F, 0x28, 0xC0, 0xE8, 0xF7, 0x62, 0x64, 0xFE + }; + + using PlatformSleepFn = void(*)(float); + using CreateWaitableTimerExWFn = HANDLE(WINAPI*)( + LPSECURITY_ATTRIBUTES, LPCWSTR, DWORD, DWORD); + using SetWaitableTimerExFn = BOOL(WINAPI*)( + HANDLE, const LARGE_INTEGER*, LONG, PTIMERAPCROUTINE, LPVOID, + PREASON_CONTEXT, ULONG); + + enum class WaitKind { + Coarse, + Yield, + Other + }; + + struct CorrectedSleepAttempt { + bool attempted = false; + bool completed = false; + uint64_t elapsedNanoseconds = 0; + float remainingSeconds = 0.0f; + }; + + struct AtomicWaitCounters { + std::atomic calls{0}; + std::atomic measuredCalls{0}; + std::atomic requestedNanoseconds{0}; + std::atomic actualNanoseconds{0}; + std::atomic maximumActualNanoseconds{0}; + }; + + struct HistogramData { + uint64_t buckets[HistogramBucketCount]{}; + uint64_t count = 0; + double totalMilliseconds = 0.0; + double minimumMilliseconds = (std::numeric_limits::max)(); + double maximumMilliseconds = 0.0; + }; + + struct EvidenceSample { + double cadenceHz = 0.0; + double engineWorkMilliseconds = 0.0; + double coarseOvershootMilliseconds = 0.0; + double fineSpinMilliseconds = 0.0; + double nativeOvershootMilliseconds = 0.0; + bool ratesValid = false; + bool fixedFrameRate = false; + bool hasCoarseWait = false; + }; + + struct MutableState { + Snapshot snapshot{}; + HistogramData nativePacingWaitHistogram{}; + HistogramData nativePacingOvershootHistogram{}; + HistogramData nativeDeltaHistogram{}; + HistogramData engineWorkHistogram{}; + HistogramData frameGapHistogram{}; + EvidenceSample evidence[EvidenceWindowSize]{}; + std::size_t evidenceCount = 0; + double overshootEwmaMilliseconds = 0.0; + double cadenceDriftEwmaHz = 0.0; + uint32_t guardReductionCooldownWindows = 0; + }; + + static_assert(std::is_trivially_copyable_v); + static_assert(std::is_trivially_copyable_v); + + SRWLOCK StateLock = SRWLOCK_INIT; + MutableState State{}; + uintptr_t ExecutableBase = 0; + PlatformSleepFn OriginalPlatformSleep = nullptr; + LARGE_INTEGER PerformanceFrequency{}; + CreateWaitableTimerExWFn CreateWaitableTimerEx = nullptr; + SetWaitableTimerExFn SetWaitableTimerEx = nullptr; + HANDLE HighResolutionTimer = nullptr; + + std::atomic CurrentCorrectionState{CorrectionState::Disabled}; + std::atomic GuardReductionActive{false}; + std::atomic CurrentFrameCoarseCalls{0}; + std::atomic CurrentFrameCoarseRequestedNanoseconds{0}; + std::atomic CurrentFrameCoarseActualNanoseconds{0}; + std::atomic TimerFailures{0}; + std::atomic GuardReductionAppliedCalls{0}; + std::atomic GuardReductionAppliedNanoseconds{0}; + AtomicWaitCounters CoarseWait{}; + AtomicWaitCounters YieldWait{}; + AtomicWaitCounters OtherWait{}; + AtomicWaitCounters CorrectedWait{}; + AtomicWaitCounters GuardExtendedWait{}; + + bool IsReadableMemory(const void* address, std::size_t size) { + if (!address || size == 0) + return false; + + MEMORY_BASIC_INFORMATION information{}; + if (VirtualQuery(address, &information, sizeof(information)) != sizeof(information) || + information.State != MEM_COMMIT || (information.Protect & PAGE_GUARD) != 0 || + (information.Protect & 0xFF) == PAGE_NOACCESS) { + return false; + } + + const uintptr_t start = reinterpret_cast(address); + const uintptr_t regionEnd = reinterpret_cast(information.BaseAddress) + information.RegionSize; + return start <= regionEnd && size <= regionEnd - start; + } + + template + bool MatchesSignature(uintptr_t address, const uint8_t (&expected)[Size]) { + const auto* bytes = reinterpret_cast(address); + return IsReadableMemory(bytes, Size) && + std::equal(expected, expected + Size, bytes); + } + + bool IsExpectedExecutable(uintptr_t base) { + const auto* dosHeader = reinterpret_cast(base); + if (!IsReadableMemory(dosHeader, sizeof(*dosHeader)) || + dosHeader->e_magic != IMAGE_DOS_SIGNATURE || dosHeader->e_lfanew <= 0 || + dosHeader->e_lfanew > 0x1000) { + return false; + } + + const auto* ntHeaders = reinterpret_cast(base + dosHeader->e_lfanew); + return IsReadableMemory(ntHeaders, sizeof(*ntHeaders)) && + ntHeaders->Signature == IMAGE_NT_SIGNATURE && + ntHeaders->FileHeader.TimeDateStamp == ExpectedExecutableTimestamp && + ntHeaders->OptionalHeader.SizeOfImage == ExpectedExecutableImageSize; + } + + bool ValidateSignatures(uintptr_t base) { + if (!MatchesSignature(base + PlatformSleepRva, ExpectedPlatformSleepSignature) || + !MatchesSignature(base + CoarseSleepCallRva - 8, ExpectedCoarseCallSignature) || + !MatchesSignature(base + YieldSleepCallRva - 4, ExpectedYieldCallSignature)) { + return false; + } + + // The byte signatures include both relative calls. Explicitly + // resolve them as a second guard against an accidental call-site match. + const auto resolvesToPlatformSleep = [base](uintptr_t callRva) { + const auto* call = reinterpret_cast(base + callRva); + if (!IsReadableMemory(call, 5) || call[0] != 0xE8) + return false; + const int32_t displacement = *reinterpret_cast(call + 1); + const uintptr_t target = reinterpret_cast(call + 5) + displacement; + return target == base + PlatformSleepRva; + }; + + return resolvesToPlatformSleep(CoarseSleepCallRva) && + resolvesToPlatformSleep(YieldSleepCallRva) && + IsReadableMemory(reinterpret_cast(base + NativePacingWaitRva), sizeof(double)) && + IsReadableMemory(reinterpret_cast(base + NativePacingOvershootRva), sizeof(double)) && + IsReadableMemory(reinterpret_cast(base + NativeDeltaSecondsRva), sizeof(double)); + } + + uint64_t SecondsToNanoseconds(double seconds) { + if (!std::isfinite(seconds) || seconds <= 0.0) + return 0; + const long double nanoseconds = static_cast(seconds) * 1'000'000'000.0L; + if (nanoseconds >= static_cast((std::numeric_limits::max)())) + return (std::numeric_limits::max)(); + return static_cast(nanoseconds + 0.5L); + } + + uint64_t CounterDeltaToNanoseconds(LONGLONG delta) { + if (delta <= 0 || PerformanceFrequency.QuadPart <= 0) + return 0; + const long double nanoseconds = static_cast(delta) * 1'000'000'000.0L / + static_cast(PerformanceFrequency.QuadPart); + return static_cast(nanoseconds + 0.5L); + } + + void AtomicMaximum(std::atomic& destination, uint64_t value) { + uint64_t current = destination.load(std::memory_order_relaxed); + while (current < value && !destination.compare_exchange_weak( + current, value, std::memory_order_relaxed, std::memory_order_relaxed)) { + } + } + + void RecordWait(AtomicWaitCounters& counters, uint64_t requestedNanoseconds, + uint64_t actualNanoseconds, bool measured) { + counters.calls.fetch_add(1, std::memory_order_relaxed); + counters.requestedNanoseconds.fetch_add(requestedNanoseconds, std::memory_order_relaxed); + if (!measured) + return; + counters.measuredCalls.fetch_add(1, std::memory_order_relaxed); + counters.actualNanoseconds.fetch_add(actualNanoseconds, std::memory_order_relaxed); + AtomicMaximum(counters.maximumActualNanoseconds, actualNanoseconds); + } + + WaitCounters ReadWaitCounters(const AtomicWaitCounters& counters) { + WaitCounters result{}; + result.calls = counters.calls.load(std::memory_order_relaxed); + result.measuredCalls = counters.measuredCalls.load(std::memory_order_relaxed); + result.requestedNanoseconds = counters.requestedNanoseconds.load(std::memory_order_relaxed); + result.actualNanoseconds = counters.actualNanoseconds.load(std::memory_order_relaxed); + result.maximumActualNanoseconds = counters.maximumActualNanoseconds.load(std::memory_order_relaxed); + return result; + } + + void AddHistogramSample(HistogramData& histogram, double milliseconds) { + if (!std::isfinite(milliseconds) || milliseconds < 0.0) + return; + std::size_t bucket = 0; + while (bucket < HistogramBucketCount - 1 && + milliseconds > MillisecondHistogramUpperBounds[bucket]) { + ++bucket; + } + ++histogram.buckets[bucket]; + ++histogram.count; + histogram.totalMilliseconds += milliseconds; + histogram.minimumMilliseconds = (std::min)(histogram.minimumMilliseconds, milliseconds); + histogram.maximumMilliseconds = (std::max)(histogram.maximumMilliseconds, milliseconds); + } + + HistogramSnapshot CopyHistogram(const HistogramData& histogram) { + HistogramSnapshot result{}; + std::copy(std::begin(histogram.buckets), std::end(histogram.buckets), + std::begin(result.buckets)); + result.count = histogram.count; + result.totalMilliseconds = histogram.totalMilliseconds; + result.minimumMilliseconds = histogram.count == 0 ? 0.0 : histogram.minimumMilliseconds; + result.maximumMilliseconds = histogram.maximumMilliseconds; + return result; + } + + template + double Percentile(const EvidenceSample* samples, std::size_t count, double percentile, + Projection projection) { + std::array values{}; + std::size_t valueCount = 0; + for (std::size_t index = 0; index < count; ++index) { + const double value = projection(samples[index]); + if (std::isfinite(value) && value >= 0.0) + values[valueCount++] = value; + } + if (valueCount == 0) + return 0.0; + std::sort(values.begin(), values.begin() + valueCount); + const double clamped = (std::clamp)(percentile, 0.0, 1.0); + const std::size_t position = static_cast( + std::ceil(clamped * static_cast(valueCount)) - 1.0); + return values[(std::min)(position, valueCount - 1)]; + } + + void SetFallbackLocked(FallbackReason reason) { + CurrentCorrectionState.store(CorrectionState::Fallback, std::memory_order_release); + GuardReductionActive.store(false, std::memory_order_release); + State.snapshot.state = CorrectionState::Fallback; + State.snapshot.guardReductionActive = false; + if (State.snapshot.fallbackReason == FallbackReason::None) + State.snapshot.fallbackReason = reason; + } + + void DisableGuardReductionLocked(GuardReductionFallbackReason reason) { + GuardReductionActive.store(false, std::memory_order_release); + State.snapshot.guardReductionActive = false; + State.snapshot.guardReductionPermanentlyDisabled = true; + if (State.snapshot.guardReductionFallbackReason == + GuardReductionFallbackReason::None) { + State.snapshot.guardReductionFallbackReason = reason; + } + } + + void SetFallback(FallbackReason reason) { + AcquireSRWLockExclusive(&StateLock); + SetFallbackLocked(reason); + ReleaseSRWLockExclusive(&StateLock); + } + + bool CreateCorrectionTimerLocked() { + if (!CreateWaitableTimerEx || !SetWaitableTimerEx) { + SetFallbackLocked(FallbackReason::TimerApiUnavailable); + return false; + } + if (HighResolutionTimer) + return true; + + HighResolutionTimer = CreateWaitableTimerEx( + nullptr, nullptr, CreateWaitableTimerHighResolution, TIMER_ALL_ACCESS); + if (!HighResolutionTimer) { + TimerFailures.fetch_add(1, std::memory_order_relaxed); + SetFallbackLocked(FallbackReason::TimerCreateFailed); + return false; + } + + State.snapshot.timerCreated = true; + return true; + } + + CorrectedSleepAttempt CorrectedSleep(float originalSeconds, float timerSeconds) { + CorrectedSleepAttempt result{}; + result.remainingSeconds = originalSeconds; + if (!HighResolutionTimer || originalSeconds <= 0.0f || timerSeconds <= 0.0f) + return result; + + const uint64_t requestedNanoseconds = SecondsToNanoseconds(timerSeconds); + const LONGLONG requestedHundredNanoseconds = + (std::max)(LONGLONG{1}, static_cast((requestedNanoseconds + 99) / 100)); + LARGE_INTEGER dueTime{}; + dueTime.QuadPart = -requestedHundredNanoseconds; + + LARGE_INTEGER started{}; + LARGE_INTEGER completed{}; + QueryPerformanceCounter(&started); + result.attempted = true; + if (!SetWaitableTimerEx(HighResolutionTimer, &dueTime, 0, nullptr, nullptr, nullptr, 0)) { + QueryPerformanceCounter(&completed); + result.elapsedNanoseconds = CounterDeltaToNanoseconds( + completed.QuadPart - started.QuadPart); + result.remainingSeconds = static_cast((std::max)( + 0.0, static_cast(originalSeconds) - + static_cast(result.elapsedNanoseconds) / 1'000'000'000.0)); + TimerFailures.fetch_add(1, std::memory_order_relaxed); + SetFallback(FallbackReason::TimerSetFailed); + RecordWait(CorrectedWait, requestedNanoseconds, result.elapsedNanoseconds, true); + return result; + } + // A waitable-timer failure must never strand the game thread. The + // native coarse wait is one frame-pacer slice (normally about 30 ms), + // so requested time plus bounded slack is enough to distinguish an + // oversleep from a timer that did not signal. + const double requestedMilliseconds = static_cast(timerSeconds) * 1000.0; + const DWORD waitTimeoutMilliseconds = static_cast((std::clamp)( + std::ceil(requestedMilliseconds + CorrectedWaitTimeoutSlackMilliseconds), + 1.0, + static_cast(CorrectedWaitMaximumTimeoutMilliseconds))); + const DWORD waitResult = WaitForSingleObject( + HighResolutionTimer, waitTimeoutMilliseconds); + QueryPerformanceCounter(&completed); + result.elapsedNanoseconds = CounterDeltaToNanoseconds( + completed.QuadPart - started.QuadPart); + result.remainingSeconds = static_cast((std::max)( + 0.0, static_cast(originalSeconds) - + static_cast(result.elapsedNanoseconds) / 1'000'000'000.0)); + RecordWait(CorrectedWait, requestedNanoseconds, result.elapsedNanoseconds, true); + if (waitResult != WAIT_OBJECT_0) { + CancelWaitableTimer(HighResolutionTimer); + TimerFailures.fetch_add(1, std::memory_order_relaxed); + SetFallback(waitResult == WAIT_TIMEOUT + ? FallbackReason::TimerWaitTimedOut + : FallbackReason::TimerWaitFailed); + return result; + } + + result.completed = true; + return result; + } + + __declspec(noinline) void PlatformSleepHook(float seconds) { + const uintptr_t returnAddress = reinterpret_cast(_ReturnAddress()); + if (returnAddress != ExecutableBase + CoarseSleepReturnRva) { + // PlatformSleep is shared by a very large number of engine and + // worker-thread callers. Only this exact return address is the + // positive frame-pacer wait. Preserve the native zero-duration + // yield and every unrelated caller without QPC reads, counters, + // conversions, or cross-thread atomics. + OriginalPlatformSleep(seconds); + return; + } + + const uint64_t requestedNanoseconds = SecondsToNanoseconds(seconds); + + if (seconds > 0.0f && + std::isfinite(seconds) && seconds <= 1.0f && + CurrentCorrectionState.load(std::memory_order_acquire) == CorrectionState::Active) { + const bool ExtendGuard = GuardReductionActive.load(std::memory_order_acquire); + const float TimerSeconds = seconds + (ExtendGuard + ? static_cast(GuardExtensionMilliseconds / 1000.0) + : 0.0f); + const CorrectedSleepAttempt attempt = CorrectedSleep(seconds, TimerSeconds); + if (attempt.attempted) { + uint64_t totalActualNanoseconds = attempt.elapsedNanoseconds; + if (!attempt.completed && attempt.remainingSeconds > 0.0f) { + LARGE_INTEGER retryStarted{}; + LARGE_INTEGER retryCompleted{}; + QueryPerformanceCounter(&retryStarted); + OriginalPlatformSleep(attempt.remainingSeconds); + QueryPerformanceCounter(&retryCompleted); + totalActualNanoseconds += CounterDeltaToNanoseconds( + retryCompleted.QuadPart - retryStarted.QuadPart); + } + + RecordWait(CoarseWait, requestedNanoseconds, totalActualNanoseconds, true); + CurrentFrameCoarseCalls.fetch_add(1, std::memory_order_relaxed); + CurrentFrameCoarseRequestedNanoseconds.fetch_add( + requestedNanoseconds, std::memory_order_relaxed); + CurrentFrameCoarseActualNanoseconds.fetch_add( + totalActualNanoseconds, std::memory_order_relaxed); + if (ExtendGuard) { + RecordWait(GuardExtendedWait, SecondsToNanoseconds(TimerSeconds), + attempt.elapsedNanoseconds, true); + GuardReductionAppliedCalls.fetch_add(1, std::memory_order_relaxed); + GuardReductionAppliedNanoseconds.fetch_add( + SecondsToNanoseconds(GuardExtensionMilliseconds / 1000.0), + std::memory_order_relaxed); + } + return; + } + } + + LARGE_INTEGER started{}; + LARGE_INTEGER completed{}; + QueryPerformanceCounter(&started); + OriginalPlatformSleep(seconds); + QueryPerformanceCounter(&completed); + const uint64_t actualNanoseconds = CounterDeltaToNanoseconds( + completed.QuadPart - started.QuadPart); + + RecordWait(CoarseWait, requestedNanoseconds, actualNanoseconds, true); + CurrentFrameCoarseCalls.fetch_add(1, std::memory_order_relaxed); + CurrentFrameCoarseRequestedNanoseconds.fetch_add(requestedNanoseconds, std::memory_order_relaxed); + CurrentFrameCoarseActualNanoseconds.fetch_add(actualNanoseconds, std::memory_order_relaxed); + } + + void EvaluateEvidenceWindow() { + if (State.evidenceCount != EvidenceWindowSize) + return; + + ++State.snapshot.evaluatedWindows; + std::size_t coarseSamples = 0; + bool allRatesValid = true; + bool anyFixedFrameRate = false; + for (const EvidenceSample& sample : State.evidence) { + coarseSamples += sample.hasCoarseWait ? 1 : 0; + allRatesValid = allRatesValid && sample.ratesValid; + anyFixedFrameRate = anyFixedFrameRate || sample.fixedFrameRate; + } + + State.snapshot.rollingMedianCadenceHz = Percentile( + State.evidence, EvidenceWindowSize, 0.50, + [](const EvidenceSample& sample) { return sample.cadenceHz; }); + State.snapshot.rollingP95EngineWorkMilliseconds = Percentile( + State.evidence, EvidenceWindowSize, 0.95, + [](const EvidenceSample& sample) { return sample.engineWorkMilliseconds; }); + State.snapshot.rollingMedianCoarseOvershootMilliseconds = Percentile( + State.evidence, EvidenceWindowSize, 0.50, + [](const EvidenceSample& sample) { + return sample.hasCoarseWait ? sample.coarseOvershootMilliseconds : -1.0; + }); + State.snapshot.rollingP95FineSpinMilliseconds = Percentile( + State.evidence, EvidenceWindowSize, 0.95, + [](const EvidenceSample& sample) { + return sample.hasCoarseWait ? sample.fineSpinMilliseconds : -1.0; + }); + State.snapshot.rollingP95NativeOvershootMilliseconds = Percentile( + State.evidence, EvidenceWindowSize, 0.95, + [](const EvidenceSample& sample) { + return sample.nativeOvershootMilliseconds; + }); + + const CorrectionState correctionState = CurrentCorrectionState.load(std::memory_order_acquire); + if (correctionState == CorrectionState::Observing) { + const bool qualifies = allRatesValid && !anyFixedFrameRate && + coarseSamples >= MinimumCoarseSamplesPerWindow && + State.snapshot.rollingMedianCadenceHz < ActivationMaximumCadenceHz && + State.snapshot.rollingP95EngineWorkMilliseconds < + ActivationMaximumEngineWorkP95Milliseconds && + State.snapshot.rollingMedianCoarseOvershootMilliseconds >= + ActivationMinimumOvershootMedianMilliseconds; + if (qualifies) { + ++State.snapshot.qualifiedWindows; + if (CreateCorrectionTimerLocked() && + CurrentCorrectionState.load(std::memory_order_acquire) == CorrectionState::Observing) { + State.snapshot.state = CorrectionState::Active; + State.snapshot.correctionEverActivated = true; + State.snapshot.consecutiveUnimprovedWindows = 0; + CurrentCorrectionState.store(CorrectionState::Active, std::memory_order_release); + } + } + } + else if (correctionState == CorrectionState::Active) { + ++State.snapshot.activeWindows; + if (State.snapshot.rollingP95FineSpinMilliseconds > ExcessiveFineSpinP95Milliseconds) { + SetFallbackLocked(FallbackReason::ExcessiveFineSpin); + } + else if (State.snapshot.rollingP95EngineWorkMilliseconds < + ActivationMaximumEngineWorkP95Milliseconds && + State.snapshot.rollingMedianCadenceHz < ActiveMinimumAcceptedCadenceHz && + !GuardReductionActive.load(std::memory_order_acquire)) { + ++State.snapshot.consecutiveUnimprovedWindows; + if (State.snapshot.consecutiveUnimprovedWindows >= UnimprovedWindowLimit) + SetFallbackLocked(FallbackReason::UnimprovedCadence); + } + else { + State.snapshot.consecutiveUnimprovedWindows = 0; + } + + const bool guardEligibility = allRatesValid && !anyFixedFrameRate && + coarseSamples >= MinimumCoarseSamplesPerWindow && + State.snapshot.rollingMedianCadenceHz >= + GuardActivationMinimumCadenceHz && + State.snapshot.rollingMedianCadenceHz <= + GuardActivationMaximumCadenceHz && + State.snapshot.rollingP95EngineWorkMilliseconds < + ActivationMaximumEngineWorkP95Milliseconds && + State.snapshot.rollingP95FineSpinMilliseconds >= + GuardActivationMinimumFineSpinP95Milliseconds; + const bool correctionActive = CurrentCorrectionState.load( + std::memory_order_acquire) == CorrectionState::Active; + if (!State.snapshot.guardReductionPermanentlyDisabled && + (correctionActive || guardEligibility)) { + if (!GuardReductionActive.load(std::memory_order_acquire)) { + if (guardEligibility) { + State.snapshot.guardReductionBaselineFineSpinMilliseconds = + State.snapshot.rollingP95FineSpinMilliseconds; + State.snapshot.guardReductionActive = true; + State.snapshot.guardReductionEverActivated = true; + State.snapshot.guardReductionBadWindows = 0; + State.snapshot.guardReductionWindows = 0; + GuardReductionActive.store(true, std::memory_order_release); + } + } + else { + ++State.snapshot.guardReductionWindows; + const bool unloaded = + State.snapshot.rollingP95EngineWorkMilliseconds < + ActivationMaximumEngineWorkP95Milliseconds; + const bool overshootBad = + State.snapshot.rollingP95NativeOvershootMilliseconds > + GuardMaximumNativeOvershootP95Milliseconds; + const bool cadenceBad = unloaded && + (State.snapshot.rollingMedianCadenceHz < + GuardActivationMinimumCadenceHz || + State.snapshot.rollingMedianCadenceHz > + GuardActivationMaximumCadenceHz); + if (overshootBad || cadenceBad) + ++State.snapshot.guardReductionBadWindows; + else + State.snapshot.guardReductionBadWindows = 0; + + if (State.snapshot.guardReductionBadWindows >= GuardBadWindowLimit) { + DisableGuardReductionLocked(overshootBad + ? GuardReductionFallbackReason::NativeOvershoot + : GuardReductionFallbackReason::CadenceRegression); + } + else if (State.snapshot.guardReductionWindows >= 2 && + State.snapshot.guardReductionBaselineFineSpinMilliseconds - + State.snapshot.rollingP95FineSpinMilliseconds < + GuardMinimumFineSpinImprovementMilliseconds) { + DisableGuardReductionLocked( + GuardReductionFallbackReason::NoFineWaitImprovement); + } + } + } + } + + State.evidenceCount = 0; + } + } + + bool Install(uintptr_t executableBase, bool dedicatedServer) { + AcquireSRWLockExclusive(&StateLock); + if (State.snapshot.installAttempted) { + const bool alreadyInstalled = State.snapshot.hookEnabled; + ReleaseSRWLockExclusive(&StateLock); + return alreadyInstalled; + } + State.snapshot.installAttempted = true; + if (!dedicatedServer) { + State.snapshot.installFailure = InstallFailure::NotDedicatedServer; + State.snapshot.state = CorrectionState::Disabled; + ReleaseSRWLockExclusive(&StateLock); + return false; + } + ReleaseSRWLockExclusive(&StateLock); + + if (!executableBase || !IsExpectedExecutable(executableBase)) { + AcquireSRWLockExclusive(&StateLock); + State.snapshot.installFailure = executableBase + ? InstallFailure::ExecutableBuildMismatch + : InstallFailure::InvalidExecutable; + ReleaseSRWLockExclusive(&StateLock); + return false; + } + + AcquireSRWLockExclusive(&StateLock); + State.snapshot.executableBuildValid = true; + ReleaseSRWLockExclusive(&StateLock); + + if (!ValidateSignatures(executableBase)) { + AcquireSRWLockExclusive(&StateLock); + State.snapshot.installFailure = InstallFailure::SignatureMismatch; + ReleaseSRWLockExclusive(&StateLock); + return false; + } + + ExecutableBase = executableBase; + QueryPerformanceFrequency(&PerformanceFrequency); + HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll"); + if (kernel32) { + CreateWaitableTimerEx = reinterpret_cast( + GetProcAddress(kernel32, "CreateWaitableTimerExW")); + SetWaitableTimerEx = reinterpret_cast( + GetProcAddress(kernel32, "SetWaitableTimerEx")); + } + + AcquireSRWLockExclusive(&StateLock); + State.snapshot.signaturesValid = true; + State.snapshot.highResolutionApiAvailable = + CreateWaitableTimerEx != nullptr && SetWaitableTimerEx != nullptr; + ReleaseSRWLockExclusive(&StateLock); + + const uintptr_t target = executableBase + PlatformSleepRva; + MH_STATUS status = MH_CreateHook(reinterpret_cast(target), + reinterpret_cast(&PlatformSleepHook), + reinterpret_cast(&OriginalPlatformSleep)); + AcquireSRWLockExclusive(&StateLock); + State.snapshot.minHookStatus = static_cast(status); + if (status != MH_OK) { + State.snapshot.installFailure = InstallFailure::MinHookCreateFailed; + ReleaseSRWLockExclusive(&StateLock); + return false; + } + State.snapshot.hookCreated = true; + ReleaseSRWLockExclusive(&StateLock); + + status = MH_EnableHook(reinterpret_cast(target)); + AcquireSRWLockExclusive(&StateLock); + State.snapshot.minHookStatus = static_cast(status); + if (status != MH_OK) { + State.snapshot.installFailure = InstallFailure::MinHookEnableFailed; + ReleaseSRWLockExclusive(&StateLock); + MH_RemoveHook(reinterpret_cast(target)); + OriginalPlatformSleep = nullptr; + AcquireSRWLockExclusive(&StateLock); + State.snapshot.hookCreated = false; + ReleaseSRWLockExclusive(&StateLock); + return false; + } + State.snapshot.hookEnabled = true; + State.snapshot.state = CorrectionState::Observing; + ReleaseSRWLockExclusive(&StateLock); + CurrentCorrectionState.store(CorrectionState::Observing, std::memory_order_release); + return true; + } + + void ObserveFrame(const FrameObservation& observation) { + const CorrectionState correctionState = CurrentCorrectionState.load(std::memory_order_acquire); + if (correctionState == CorrectionState::Disabled) + return; + + const uint64_t coarseCalls = CurrentFrameCoarseCalls.exchange(0, std::memory_order_acq_rel); + const uint64_t coarseRequestedNanoseconds = + CurrentFrameCoarseRequestedNanoseconds.exchange(0, std::memory_order_acq_rel); + const uint64_t coarseActualNanoseconds = + CurrentFrameCoarseActualNanoseconds.exchange(0, std::memory_order_acq_rel); + + const double nativeWaitMilliseconds = *reinterpret_cast( + ExecutableBase + NativePacingWaitRva) * 1000.0; + const double nativeOvershootMilliseconds = *reinterpret_cast( + ExecutableBase + NativePacingOvershootRva) * 1000.0; + const double nativeDeltaMilliseconds = *reinterpret_cast( + ExecutableBase + NativeDeltaSecondsRva) * 1000.0; + const double coarseRequestedMilliseconds = + static_cast(coarseRequestedNanoseconds) / 1'000'000.0; + const double coarseActualMilliseconds = + static_cast(coarseActualNanoseconds) / 1'000'000.0; + + AcquireSRWLockExclusive(&StateLock); + ++State.snapshot.observedFrames; + State.snapshot.state = correctionState; + State.snapshot.cvarMaxFps = observation.cvarMaxFps; + State.snapshot.cachedMaxFps = observation.cachedMaxFps; + State.snapshot.virtualMaxFps = observation.virtualMaxFps; + State.snapshot.nativePacingWaitMilliseconds = nativeWaitMilliseconds; + State.snapshot.nativePacingOvershootMilliseconds = nativeOvershootMilliseconds; + State.snapshot.nativeDeltaMilliseconds = nativeDeltaMilliseconds; + constexpr double EwmaAlpha = 0.125; + State.overshootEwmaMilliseconds += EwmaAlpha * + (nativeOvershootMilliseconds - State.overshootEwmaMilliseconds); + const double TargetCadenceHz = observation.cachedMaxFps > 0.0 + ? observation.cachedMaxFps : observation.cvarMaxFps; + const double ObservedCadenceHz = nativeDeltaMilliseconds > 0.0 + ? 1000.0 / nativeDeltaMilliseconds : 0.0; + const double CadenceDrift = TargetCadenceHz > 0.0 + ? ObservedCadenceHz - TargetCadenceHz : 0.0; + State.cadenceDriftEwmaHz += EwmaAlpha * + (CadenceDrift - State.cadenceDriftEwmaHz); + State.snapshot.overshootEwmaMilliseconds = State.overshootEwmaMilliseconds; + State.snapshot.cadenceDriftEwmaHz = State.cadenceDriftEwmaHz; + State.snapshot.guardReductionCooldownWindows = + State.guardReductionCooldownWindows; + + AddHistogramSample(State.nativePacingWaitHistogram, nativeWaitMilliseconds); + AddHistogramSample(State.nativePacingOvershootHistogram, nativeOvershootMilliseconds); + AddHistogramSample(State.nativeDeltaHistogram, nativeDeltaMilliseconds); + AddHistogramSample(State.engineWorkHistogram, observation.engineWorkMilliseconds); + AddHistogramSample(State.frameGapHistogram, observation.frameGapMilliseconds); + + if (State.evidenceCount < EvidenceWindowSize) { + EvidenceSample& sample = State.evidence[State.evidenceCount++]; + sample.cadenceHz = observation.frameGapMilliseconds > 0.0 + ? 1000.0 / observation.frameGapMilliseconds + : 0.0; + sample.engineWorkMilliseconds = observation.engineWorkMilliseconds; + sample.coarseOvershootMilliseconds = coarseCalls > 0 + ? (std::max)(0.0, coarseActualMilliseconds - coarseRequestedMilliseconds) + : 0.0; + sample.fineSpinMilliseconds = coarseCalls > 0 + ? (std::max)(0.0, nativeWaitMilliseconds - coarseActualMilliseconds) + : 0.0; + sample.nativeOvershootMilliseconds = (std::max)( + 0.0, nativeOvershootMilliseconds); + sample.ratesValid = + std::isfinite(observation.cvarMaxFps) && + std::isfinite(observation.cachedMaxFps) && + std::isfinite(observation.virtualMaxFps) && + std::abs(observation.cvarMaxFps - ExpectedMaxFps) <= MaxFpsTolerance && + std::abs(observation.cachedMaxFps - ExpectedMaxFps) <= MaxFpsTolerance && + std::abs(observation.virtualMaxFps - ExpectedMaxFps) <= MaxFpsTolerance; + sample.fixedFrameRate = observation.fixedFrameRate; + sample.hasCoarseWait = coarseCalls > 0; + } + + EvaluateEvidenceWindow(); + State.snapshot.timerFailures = TimerFailures.load(std::memory_order_relaxed); + ReleaseSRWLockExclusive(&StateLock); + } + + Snapshot GetSnapshot() { + AcquireSRWLockShared(&StateLock); + Snapshot result = State.snapshot; + result.nativePacingWaitHistogram = CopyHistogram(State.nativePacingWaitHistogram); + result.nativePacingOvershootHistogram = CopyHistogram(State.nativePacingOvershootHistogram); + result.nativeDeltaHistogram = CopyHistogram(State.nativeDeltaHistogram); + result.engineWorkHistogram = CopyHistogram(State.engineWorkHistogram); + result.frameGapHistogram = CopyHistogram(State.frameGapHistogram); + ReleaseSRWLockShared(&StateLock); + + result.state = CurrentCorrectionState.load(std::memory_order_acquire); + result.guardReductionActive = GuardReductionActive.load(std::memory_order_acquire); + result.timerFailures = TimerFailures.load(std::memory_order_relaxed); + result.guardReductionAppliedCalls = + GuardReductionAppliedCalls.load(std::memory_order_relaxed); + result.guardReductionAppliedNanoseconds = + GuardReductionAppliedNanoseconds.load(std::memory_order_relaxed); + result.coarseWait = ReadWaitCounters(CoarseWait); + result.yieldWait = ReadWaitCounters(YieldWait); + result.otherWait = ReadWaitCounters(OtherWait); + result.correctedWait = ReadWaitCounters(CorrectedWait); + result.guardExtendedWait = ReadWaitCounters(GuardExtendedWait); + return result; + } + + const char* CorrectionStateName(CorrectionState state) { + switch (state) { + case CorrectionState::Disabled: return "disabled"; + case CorrectionState::Observing: return "observing"; + case CorrectionState::Active: return "active"; + case CorrectionState::Fallback: return "fallback"; + default: return "unknown"; + } + } + + const char* InstallFailureName(InstallFailure failure) { + switch (failure) { + case InstallFailure::None: return "none"; + case InstallFailure::NotDedicatedServer: return "not_dedicated_server"; + case InstallFailure::InvalidExecutable: return "invalid_executable"; + case InstallFailure::ExecutableBuildMismatch: return "executable_build_mismatch"; + case InstallFailure::SignatureMismatch: return "signature_mismatch"; + case InstallFailure::MinHookCreateFailed: return "minhook_create_failed"; + case InstallFailure::MinHookEnableFailed: return "minhook_enable_failed"; + default: return "unknown"; + } + } + + const char* FallbackReasonName(FallbackReason reason) { + switch (reason) { + case FallbackReason::None: return "none"; + case FallbackReason::TimerApiUnavailable: return "timer_api_unavailable"; + case FallbackReason::TimerCreateFailed: return "timer_create_failed"; + case FallbackReason::TimerSetFailed: return "timer_set_failed"; + case FallbackReason::TimerWaitTimedOut: return "timer_wait_timed_out"; + case FallbackReason::TimerWaitFailed: return "timer_wait_failed"; + case FallbackReason::UnimprovedCadence: return "unimproved_cadence"; + case FallbackReason::ExcessiveFineSpin: return "excessive_fine_spin"; + default: return "unknown"; + } + } + + const char* GuardReductionFallbackReasonName(GuardReductionFallbackReason reason) { + switch (reason) { + case GuardReductionFallbackReason::None: return "none"; + case GuardReductionFallbackReason::NativeOvershoot: return "native_overshoot"; + case GuardReductionFallbackReason::CadenceRegression: return "cadence_regression"; + case GuardReductionFallbackReason::NoFineWaitImprovement: return "no_fine_wait_improvement"; + default: return "unknown"; + } + } + + void Shutdown() { + CurrentCorrectionState.store(CorrectionState::Disabled, std::memory_order_release); + GuardReductionActive.store(false, std::memory_order_release); + bool hookEnabled = false; + AcquireSRWLockShared(&StateLock); + hookEnabled = State.snapshot.hookEnabled; + ReleaseSRWLockShared(&StateLock); + if (ExecutableBase && hookEnabled) { + const void* target = reinterpret_cast(ExecutableBase + PlatformSleepRva); + MH_DisableHook(const_cast(target)); + MH_RemoveHook(const_cast(target)); + } + if (HighResolutionTimer) { + CloseHandle(HighResolutionTimer); + HighResolutionTimer = nullptr; + } + + AcquireSRWLockExclusive(&StateLock); + State.snapshot.hookEnabled = false; + State.snapshot.state = CorrectionState::Disabled; + ReleaseSRWLockExclusive(&StateLock); + } +} diff --git a/UndauntedInternalServer/ServerPacing.h b/UndauntedInternalServer/ServerPacing.h new file mode 100644 index 0000000..0dfdea0 --- /dev/null +++ b/UndauntedInternalServer/ServerPacing.h @@ -0,0 +1,153 @@ +#pragma once + +#include +#include + +namespace ServerPacing { + constexpr std::size_t EvidenceWindowSize = 128; + constexpr std::size_t HistogramBucketCount = 16; + + // The final bucket contains values greater than the last finite boundary. + inline constexpr double MillisecondHistogramUpperBounds[HistogramBucketCount - 1] = { + 0.05, 0.10, 0.25, 0.50, 1.0, 2.0, 4.0, 8.0, + 12.0, 16.0, 24.0, 32.0, 40.0, 50.0, 100.0 + }; + + enum class CorrectionState : uint32_t { + Disabled, + Observing, + Active, + Fallback + }; + + enum class InstallFailure : uint32_t { + None, + NotDedicatedServer, + InvalidExecutable, + ExecutableBuildMismatch, + SignatureMismatch, + MinHookCreateFailed, + MinHookEnableFailed + }; + + enum class FallbackReason : uint32_t { + None, + TimerApiUnavailable, + TimerCreateFailed, + TimerSetFailed, + TimerWaitTimedOut, + TimerWaitFailed, + UnimprovedCadence, + ExcessiveFineSpin + }; + + enum class GuardReductionFallbackReason : uint32_t { + None, + NativeOvershoot, + CadenceRegression, + NoFineWaitImprovement + }; + + struct FrameObservation { + // These are deliberately supplied by the caller so this module does not + // depend on Unreal SDK types or perform console-variable lookups. + double cvarMaxFps = 0.0; + double cachedMaxFps = 0.0; + double virtualMaxFps = 0.0; + double engineWorkMilliseconds = 0.0; + double frameGapMilliseconds = 0.0; + bool fixedFrameRate = false; + }; + + struct WaitCounters { + uint64_t calls = 0; + uint64_t measuredCalls = 0; + uint64_t requestedNanoseconds = 0; + uint64_t actualNanoseconds = 0; + uint64_t maximumActualNanoseconds = 0; + }; + + struct HistogramSnapshot { + uint64_t buckets[HistogramBucketCount]{}; + uint64_t count = 0; + double totalMilliseconds = 0.0; + double minimumMilliseconds = 0.0; + double maximumMilliseconds = 0.0; + }; + + // Trivially-copyable snapshot suitable for the fixed-size profiling path. + struct Snapshot { + CorrectionState state = CorrectionState::Disabled; + InstallFailure installFailure = InstallFailure::None; + FallbackReason fallbackReason = FallbackReason::None; + int32_t minHookStatus = 0; + + bool installAttempted = false; + bool executableBuildValid = false; + bool signaturesValid = false; + bool hookCreated = false; + bool hookEnabled = false; + bool highResolutionApiAvailable = false; + bool timerCreated = false; + bool correctionEverActivated = false; + bool guardReductionActive = false; + bool guardReductionEverActivated = false; + bool guardReductionPermanentlyDisabled = false; + GuardReductionFallbackReason guardReductionFallbackReason = + GuardReductionFallbackReason::None; + + uint64_t observedFrames = 0; + uint64_t evaluatedWindows = 0; + uint64_t qualifiedWindows = 0; + uint64_t activeWindows = 0; + uint64_t consecutiveUnimprovedWindows = 0; + uint64_t timerFailures = 0; + uint64_t guardReductionWindows = 0; + uint64_t guardReductionBadWindows = 0; + uint64_t guardReductionAppliedCalls = 0; + uint64_t guardReductionAppliedNanoseconds = 0; + + double cvarMaxFps = 0.0; + double cachedMaxFps = 0.0; + double virtualMaxFps = 0.0; + double nativePacingWaitMilliseconds = 0.0; + double nativePacingOvershootMilliseconds = 0.0; + double nativeDeltaMilliseconds = 0.0; + double rollingMedianCadenceHz = 0.0; + double rollingP95EngineWorkMilliseconds = 0.0; + double rollingMedianCoarseOvershootMilliseconds = 0.0; + double rollingP95FineSpinMilliseconds = 0.0; + double rollingP95NativeOvershootMilliseconds = 0.0; + double guardReductionBaselineFineSpinMilliseconds = 0.0; + double overshootEwmaMilliseconds = 0.0; + double cadenceDriftEwmaHz = 0.0; + uint32_t guardReductionCooldownWindows = 0; + + WaitCounters coarseWait{}; + WaitCounters yieldWait{}; + WaitCounters otherWait{}; + WaitCounters correctedWait{}; + WaitCounters guardExtendedWait{}; + + HistogramSnapshot nativePacingWaitHistogram{}; + HistogramSnapshot nativePacingOvershootHistogram{}; + HistogramSnapshot nativeDeltaHistogram{}; + HistogramSnapshot engineWorkHistogram{}; + HistogramSnapshot frameGapHistogram{}; + }; + + // MinHook must already be initialized. This function only creates and + // enables the single, signature-checked dedicated-server detour. + bool Install(uintptr_t executableBase, bool dedicatedServer); + + // Call once after each original UGameEngine::Tick. Native wait/delta values + // are read from build-validated engine globals by this module. + void ObserveFrame(const FrameObservation& observation); + + Snapshot GetSnapshot(); + const char* CorrectionStateName(CorrectionState state); + const char* InstallFailureName(InstallFailure failure); + const char* FallbackReasonName(FallbackReason reason); + const char* GuardReductionFallbackReasonName(GuardReductionFallbackReason reason); + void Shutdown(); +} diff --git a/UndauntedInternalServer/ServerPerformance.cpp b/UndauntedInternalServer/ServerPerformance.cpp new file mode 100644 index 0000000..b35cd9e --- /dev/null +++ b/UndauntedInternalServer/ServerPerformance.cpp @@ -0,0 +1,1401 @@ +#include "ServerPerformance.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AssetOptimization.h" +#include "Networking.h" +#include "SDK.hpp" +#include "ServerPacing.h" + +using namespace SDK; + +namespace ServerPerformance { + namespace { + constexpr std::array BucketUpperBoundsMs{ + 1.0, 2.0, 4.0, 8.0, 16.0, 33.0, 50.0, 100.0, 250.0 + }; + constexpr std::array ReplicationBucketNames{ + "immediate", "archonCharacter", "archonBehemoth", "projectile", "pawnOrCharacter", "movementActor" + }; + constexpr std::array UrgentReplicationReasonNames{ + "stagger", "bleedout", "revive", "dodge" + }; + constexpr std::array CombatEventNames{ + "bleedoutStarted", "reviveCompleted", "reviveQuickItem", "dodgeRequested", + "dodgePerformed", "comboStarted", "comboNextMove", "comboStateChanged", + "clientTryHit", "serverAcceptHit", "serverRefuseHit", "processHitOnServer", + "combatTextQueued", "combatTextMulticast", "staggerConfirmed" + }; + constexpr uint32_t ConfiguredMaxFps = 30; + constexpr size_t MaximumPendingLifecycleRecords = 256; + constexpr size_t LifecycleEventNameCapacity = 64; + constexpr size_t LifecycleFieldsCapacity = 1024; + constexpr size_t MaximumThreadCpuEntries = 8; + constexpr size_t ThreadDescriptionCapacity = 64; + constexpr std::wstring_view DllHashArgumentPrefix = L"-undauntedDllSha256="; + + struct LifecycleRecord { + SYSTEMTIME Timestamp{}; + std::array EventName{}; + std::array FieldsJson{}; + }; + + struct ThreadCpuEntry { + DWORD ThreadId = 0; + uint64_t Cpu100Nanoseconds = 0; + std::array Description{}; + }; + + struct CpuAttribution { + DWORD GameThreadId = 0; + uint64_t ProcessCpu100Nanoseconds = 0; + uint64_t GameThreadCpu100Nanoseconds = 0; + std::array TopThreads{}; + size_t TopThreadCount = 0; + DWORD SamplingError = ERROR_SUCCESS; + }; + + struct Histogram { + std::array counts{}; + uint64_t samples = 0; + double total = 0.0; + double maximum = 0.0; + + void Add(double Value) { + if (!std::isfinite(Value) || Value < 0.0) return; + ++samples; + total += Value; + maximum = (std::max)(maximum, Value); + size_t Bucket = 0; + while (Bucket < BucketUpperBoundsMs.size() && Value > BucketUpperBoundsMs[Bucket]) ++Bucket; + ++counts[Bucket]; + } + + double Percentile(double Quantile) const { + if (samples == 0) return 0.0; + const uint64_t Target = (std::max)(uint64_t{ 1 }, + static_cast(samples * Quantile + 0.999999)); + uint64_t Seen = 0; + for (size_t Index = 0; Index < counts.size(); ++Index) { + Seen += counts[Index]; + if (Seen >= Target) + return Index < BucketUpperBoundsMs.size() ? BucketUpperBoundsMs[Index] : maximum; + } + return maximum; + } + + void Reset() { *this = {}; } + }; + + struct IntervalSnapshot { + SYSTEMTIME Timestamp{}; + double ElapsedSeconds = 0.0; + double ObservedMaxFps = 0.0; + Histogram EngineTickDuration{}; + Histogram FrameGap{}; + Histogram NetworkingDuration{}; + uint64_t LongFrames50Ms = 0; + uint64_t LongFrames100Ms = 0; + uint64_t LongFrames250Ms = 0; + uint64_t AbilityRpcMatched = 0; + uint64_t AbilityRpcActivated = 0; + uint64_t AbilityRpcRejected = 0; + uint64_t AbilityRpcOriginalFallback = 0; + uint64_t ProcessEventCalls = 0; + uint64_t ProcessEventNetServerCandidates = 0; + uint64_t ProcessEventNameLookups = 0; + uint64_t ProcessEventNameFallbacks = 0; + uint64_t ProcessEventFunctionMatches = 0; + uint64_t ProcessEventOriginalCalls = 0; + uint64_t DroppedLifecycleRecords = 0; + uint64_t DroppedIntervalSnapshots = 0; + uint64_t PreviousIntervalWriteFailures = 0; + DWORD PreviousIntervalWriteError = ERROR_SUCCESS; + uint64_t WriterSerializationCalls = 0; + uint64_t WriterSerializationMicroseconds = 0; + uint64_t WriterSerializationMaximumMicroseconds = 0; + uint64_t WriterWriteFlushCalls = 0; + uint64_t WriterWriteFlushMicroseconds = 0; + uint64_t WriterWriteFlushMaximumMicroseconds = 0; + int32_t NetServerMaxTickRate = 0; + int32_t MaxNetTickRate = 0; + int32_t MaxClientRate = 0; + uint32_t TrackedConnections = 0; + uint32_t RawConnections = 0; + CpuAttribution Cpu{}; + Networking::ProfilingCounters NetworkingCounters{}; + ServerPacing::Snapshot Pacing{}; + std::array LifecycleRecords{}; + size_t LifecycleRecordCount = 0; + }; + + bool ConfiguredEnabled = false; + bool Enabled = false; + bool Started = false; + uint32_t IntervalSeconds = 30; + std::string ServerId; + std::string MapPath; + std::string DllSha256; + std::filesystem::path ProfilePath; + std::filesystem::path ProfileOutputDirectory; + uint64_t MaximumProfileBytes = 64ull * 1024ull * 1024ull; + uint64_t TotalProfileBytesWritten = 0; + HANDLE ProfileFile = INVALID_HANDLE_VALUE; + HANDLE PendingSnapshotEvent = nullptr; + HANDLE StopEvent = nullptr; + HANDLE WriterThread = nullptr; + SRWLOCK PendingSnapshotLock = SRWLOCK_INIT; + IntervalSnapshot CaptureSnapshot{}; + // Keep the cross-thread handoff buffer on writable heap storage. With + // LTCG, MSVC placed the previously zero-initialized file-static buffer + // in .rdata even though PublishSnapshot assigns to it, causing the + // first 30-second snapshot to fault inside VCRUNTIME memcpy. + IntervalSnapshot* PendingSnapshot = nullptr; + IntervalSnapshot WriterSnapshot{}; + bool HasPendingSnapshot = false; + std::atomic IntervalBytesWritten{ 0 }; + std::atomic IntervalWriteFailures{ 0 }; + std::atomic LastIntervalWriteError{ ERROR_SUCCESS }; + std::atomic WriterSerializationCalls{ 0 }; + std::atomic WriterSerializationMicroseconds{ 0 }; + std::atomic WriterSerializationMaximumMicroseconds{ 0 }; + std::atomic WriterWriteFlushCalls{ 0 }; + std::atomic WriterWriteFlushMicroseconds{ 0 }; + std::atomic WriterWriteFlushMaximumMicroseconds{ 0 }; + uint64_t StartupBytesWritten = 0; + DWORD StartupWriteError = ERROR_SUCCESS; + DWORD WriterStartError = ERROR_SUCCESS; + bool StartupWriteSucceeded = false; + bool WriterStarted = false; + bool ExitHandlerRegistered = false; + uint64_t DroppedIntervalSnapshots = 0; + std::chrono::steady_clock::time_point IntervalStarted{}; + Histogram EngineTickDuration; + Histogram FrameGap; + Histogram NetworkingDuration; + uint64_t LongFrames50Ms = 0; + uint64_t LongFrames100Ms = 0; + uint64_t LongFrames250Ms = 0; + uint64_t AbilityRpcMatched = 0; + uint64_t AbilityRpcActivated = 0; + uint64_t AbilityRpcRejected = 0; + uint64_t AbilityRpcOriginalFallback = 0; + uint64_t ProcessEventCalls = 0; + uint64_t ProcessEventNetServerCandidates = 0; + uint64_t ProcessEventNameLookups = 0; + uint64_t ProcessEventNameFallbacks = 0; + uint64_t ProcessEventFunctionMatches = 0; + uint64_t ProcessEventOriginalCalls = 0; + std::array PendingLifecycleRecords{}; + size_t PendingLifecycleRecordCount = 0; + uint64_t DroppedLifecycleRecords = 0; + std::atomic GameThreadId{ 0 }; + uint64_t PreviousProcessCpu100Nanoseconds = 0; + std::unordered_map PreviousThreadCpu100Nanoseconds; + + std::string WideToUtf8(const std::wstring& Value); + + uint64_t FileTimeValue(const FILETIME& Value) { + ULARGE_INTEGER Combined{}; + Combined.LowPart = Value.dwLowDateTime; + Combined.HighPart = Value.dwHighDateTime; + return Combined.QuadPart; + } + + void CopyThreadDescription(HANDLE Thread, ThreadCpuEntry& Entry) { + using GetThreadDescriptionType = HRESULT(WINAPI*)(HANDLE, PWSTR*); + static const auto GetDescription = + reinterpret_cast(GetProcAddress( + GetModuleHandleW(L"Kernel32.dll"), "GetThreadDescription")); + if (!GetDescription) + return; + + PWSTR Description = nullptr; + if (FAILED(GetDescription(Thread, &Description)) || !Description) + return; + const std::string Utf8 = WideToUtf8(std::wstring(Description)); + LocalFree(Description); + const size_t Length = (std::min)( + Utf8.size(), Entry.Description.size() - 1); + std::memcpy(Entry.Description.data(), Utf8.data(), Length); + } + + void SampleThreadCpu(CpuAttribution& Output) { + Output = {}; + Output.GameThreadId = GameThreadId.load(std::memory_order_relaxed); + + FILETIME Created{}, Exited{}, Kernel{}, User{}; + if (!GetProcessTimes(GetCurrentProcess(), + &Created, &Exited, &Kernel, &User)) { + Output.SamplingError = GetLastError(); + return; + } + const uint64_t ProcessTotal = + FileTimeValue(Kernel) + FileTimeValue(User); + if (PreviousProcessCpu100Nanoseconds != 0) + Output.ProcessCpu100Nanoseconds = + ProcessTotal - PreviousProcessCpu100Nanoseconds; + PreviousProcessCpu100Nanoseconds = ProcessTotal; + + const HANDLE Snapshot = CreateToolhelp32Snapshot( + TH32CS_SNAPTHREAD, 0); + if (Snapshot == INVALID_HANDLE_VALUE) { + Output.SamplingError = GetLastError(); + return; + } + + struct ThreadDelta { + DWORD ThreadId = 0; + uint64_t Cpu100Nanoseconds = 0; + }; + std::vector Deltas; + std::unordered_map CurrentTotals; + THREADENTRY32 Entry{}; + Entry.dwSize = sizeof(Entry); + if (Thread32First(Snapshot, &Entry)) { + do { + if (Entry.th32OwnerProcessID != GetCurrentProcessId()) + continue; + const HANDLE Thread = OpenThread( + THREAD_QUERY_LIMITED_INFORMATION, FALSE, + Entry.th32ThreadID); + if (!Thread) + continue; + FILETIME ThreadCreated{}, ThreadExited{}, + ThreadKernel{}, ThreadUser{}; + if (GetThreadTimes(Thread, &ThreadCreated, &ThreadExited, + &ThreadKernel, &ThreadUser)) { + const uint64_t Total = + FileTimeValue(ThreadKernel) + + FileTimeValue(ThreadUser); + CurrentTotals.emplace(Entry.th32ThreadID, Total); + const auto Previous = + PreviousThreadCpu100Nanoseconds.find( + Entry.th32ThreadID); + const uint64_t Delta = + Previous != PreviousThreadCpu100Nanoseconds.end() && + Total >= Previous->second + ? Total - Previous->second + : 0; + Deltas.push_back({ Entry.th32ThreadID, Delta }); + if (Entry.th32ThreadID == Output.GameThreadId) + Output.GameThreadCpu100Nanoseconds = Delta; + } + CloseHandle(Thread); + } while (Thread32Next(Snapshot, &Entry)); + } + else { + Output.SamplingError = GetLastError(); + } + CloseHandle(Snapshot); + PreviousThreadCpu100Nanoseconds.swap(CurrentTotals); + + std::ranges::sort(Deltas, std::greater{}, + &ThreadDelta::Cpu100Nanoseconds); + Output.TopThreadCount = (std::min)( + Deltas.size(), Output.TopThreads.size()); + for (size_t Index = 0; Index < Output.TopThreadCount; ++Index) { + ThreadCpuEntry& Destination = Output.TopThreads[Index]; + Destination.ThreadId = Deltas[Index].ThreadId; + Destination.Cpu100Nanoseconds = + Deltas[Index].Cpu100Nanoseconds; + const HANDLE Thread = OpenThread( + THREAD_QUERY_LIMITED_INFORMATION, FALSE, + Destination.ThreadId); + if (Thread) { + CopyThreadDescription(Thread, Destination); + CloseHandle(Thread); + } + } + } + + std::string JsonEscape(const std::string& Value) { + std::string Result; + Result.reserve(Value.size()); + for (const unsigned char Character : Value) { + switch (Character) { + case '\\': Result += "\\\\"; break; + case '"': Result += "\\\""; break; + case '\n': Result += "\\n"; break; + case '\r': Result += "\\r"; break; + case '\t': Result += "\\t"; break; + default: if (Character >= 0x20) Result += static_cast(Character); break; + } + } + return Result; + } + + double JsonNumber(double Value) { + return std::isfinite(Value) ? Value : 0.0; + } + + uint64_t ElapsedMicroseconds(std::chrono::steady_clock::time_point StartedAt) { + const auto Elapsed = std::chrono::steady_clock::now() - StartedAt; + return static_cast((std::max)(int64_t{ 0 }, + std::chrono::duration_cast(Elapsed).count())); + } + + void AtomicMaximum(std::atomic& Destination, uint64_t Value) { + uint64_t Current = Destination.load(std::memory_order_relaxed); + while (Current < Value && !Destination.compare_exchange_weak( + Current, Value, std::memory_order_relaxed, std::memory_order_relaxed)) { + } + } + + std::string FormatUtcTimestamp(const SYSTEMTIME& Time, bool FilenameSafe) { + char Buffer[40]{}; + if (FilenameSafe) { + sprintf_s(Buffer, "%04u%02u%02uT%02u%02u%02u%03uZ", Time.wYear, Time.wMonth, Time.wDay, + Time.wHour, Time.wMinute, Time.wSecond, Time.wMilliseconds); + } + else { + sprintf_s(Buffer, "%04u-%02u-%02uT%02u:%02u:%02u.%03uZ", Time.wYear, Time.wMonth, Time.wDay, + Time.wHour, Time.wMinute, Time.wSecond, Time.wMilliseconds); + } + return Buffer; + } + + std::string UtcTimestamp(bool FilenameSafe) { + SYSTEMTIME Time{}; + GetSystemTime(&Time); + return FormatUtcTimestamp(Time, FilenameSafe); + } + + std::filesystem::path ExecutableDirectory() { + std::wstring Buffer(32768, L'\0'); + const DWORD Length = GetModuleFileNameW(nullptr, Buffer.data(), static_cast(Buffer.size())); + if (Length == 0 || Length >= Buffer.size()) return std::filesystem::current_path(); + Buffer.resize(Length); + return std::filesystem::path(Buffer).parent_path(); + } + + std::string WideToUtf8(const std::wstring& Value) { + if (Value.empty()) return {}; + const int Required = WideCharToMultiByte(CP_UTF8, 0, Value.data(), static_cast(Value.size()), + nullptr, 0, nullptr, nullptr); + if (Required <= 0) return {}; + std::string Result(static_cast(Required), '\0'); + WideCharToMultiByte(CP_UTF8, 0, Value.data(), static_cast(Value.size()), + Result.data(), Required, nullptr, nullptr); + return Result; + } + + bool IsSha256(const std::wstring_view Value) { + if (Value.size() != 64) return false; + return std::all_of(Value.begin(), Value.end(), [](wchar_t Character) { + return (Character >= L'0' && Character <= L'9') + || (Character >= L'a' && Character <= L'f') + || (Character >= L'A' && Character <= L'F'); + }); + } + + std::string ReadDllSha256Argument() { + int ArgumentCount = 0; + wchar_t** Arguments = CommandLineToArgvW(GetCommandLineW(), &ArgumentCount); + if (!Arguments) return {}; + + std::string Result; + for (int Index = 0; Index < ArgumentCount; ++Index) { + const std::wstring_view Argument(Arguments[Index]); + if (!Argument.starts_with(DllHashArgumentPrefix)) continue; + const std::wstring_view Value = Argument.substr(DllHashArgumentPrefix.size()); + if (!IsSha256(Value)) break; + std::wstring Normalized(Value); + std::transform(Normalized.begin(), Normalized.end(), Normalized.begin(), [](wchar_t Character) { + return Character >= L'A' && Character <= L'F' ? Character + (L'a' - L'A') : Character; + }); + Result = WideToUtf8(Normalized); + break; + } + + LocalFree(Arguments); + return Result; + } + + void WriteHistogram(std::ostream& Stream, const Histogram& Value) { + Stream << "{\"samples\":" << Value.samples + << ",\"mean\":" << (Value.samples ? Value.total / Value.samples : 0.0) + << ",\"p50Upper\":" << Value.Percentile(0.50) + << ",\"p95Upper\":" << Value.Percentile(0.95) + << ",\"p99Upper\":" << Value.Percentile(0.99) + << ",\"max\":" << Value.maximum << ",\"buckets\":["; + for (size_t Index = 0; Index < Value.counts.size(); ++Index) { + if (Index) Stream << ','; + Stream << Value.counts[Index]; + } + Stream << "]}"; + } + + void WriteWaitCounters(std::ostream& Stream, const ServerPacing::WaitCounters& Counters) { + Stream << "{\"calls\":" << Counters.calls + << ",\"measuredCalls\":" << Counters.measuredCalls + << ",\"requestedMs\":" << static_cast(Counters.requestedNanoseconds) / 1'000'000.0 + << ",\"actualMs\":" << static_cast(Counters.actualNanoseconds) / 1'000'000.0 + << ",\"maximumActualMs\":" << static_cast(Counters.maximumActualNanoseconds) / 1'000'000.0 + << '}'; + } + + void WritePacingHistogram(std::ostream& Stream, const ServerPacing::HistogramSnapshot& Histogram) { + Stream << "{\"samples\":" << Histogram.count + << ",\"mean\":" << (Histogram.count ? Histogram.totalMilliseconds / Histogram.count : 0.0) + << ",\"min\":" << Histogram.minimumMilliseconds + << ",\"max\":" << Histogram.maximumMilliseconds + << ",\"buckets\":["; + for (size_t Index = 0; Index < ServerPacing::HistogramBucketCount; ++Index) { + if (Index) Stream << ','; + Stream << Histogram.buckets[Index]; + } + Stream << "]}"; + } + + void WritePacing(std::ostream& Stream, const ServerPacing::Snapshot& Pacing) { + Stream << "{\"state\":\"" << ServerPacing::CorrectionStateName(Pacing.state) + << "\",\"installFailure\":\"" << ServerPacing::InstallFailureName(Pacing.installFailure) + << "\",\"fallbackReason\":\"" << ServerPacing::FallbackReasonName(Pacing.fallbackReason) + << "\",\"minHookStatus\":" << Pacing.minHookStatus + << ",\"installAttempted\":" << (Pacing.installAttempted ? "true" : "false") + << ",\"executableBuildValid\":" << (Pacing.executableBuildValid ? "true" : "false") + << ",\"signaturesValid\":" << (Pacing.signaturesValid ? "true" : "false") + << ",\"hookCreated\":" << (Pacing.hookCreated ? "true" : "false") + << ",\"hookEnabled\":" << (Pacing.hookEnabled ? "true" : "false") + << ",\"highResolutionApiAvailable\":" << (Pacing.highResolutionApiAvailable ? "true" : "false") + << ",\"timerCreated\":" << (Pacing.timerCreated ? "true" : "false") + << ",\"correctionEverActivated\":" << (Pacing.correctionEverActivated ? "true" : "false") + << ",\"guardReductionActive\":" << (Pacing.guardReductionActive ? "true" : "false") + << ",\"guardReductionEverActivated\":" << (Pacing.guardReductionEverActivated ? "true" : "false") + << ",\"guardReductionPermanentlyDisabled\":" << (Pacing.guardReductionPermanentlyDisabled ? "true" : "false") + << ",\"guardReductionFallbackReason\":\"" + << ServerPacing::GuardReductionFallbackReasonName( + Pacing.guardReductionFallbackReason) << '"' + << ",\"observedFrames\":" << Pacing.observedFrames + << ",\"evaluatedWindows\":" << Pacing.evaluatedWindows + << ",\"qualifiedWindows\":" << Pacing.qualifiedWindows + << ",\"activeWindows\":" << Pacing.activeWindows + << ",\"consecutiveUnimprovedWindows\":" << Pacing.consecutiveUnimprovedWindows + << ",\"timerFailures\":" << Pacing.timerFailures + << ",\"guardReductionWindows\":" << Pacing.guardReductionWindows + << ",\"guardReductionBadWindows\":" << Pacing.guardReductionBadWindows + << ",\"guardReductionAppliedCalls\":" << Pacing.guardReductionAppliedCalls + << ",\"guardReductionAppliedNanoseconds\":" + << Pacing.guardReductionAppliedNanoseconds + << ",\"rates\":{\"cvarMaxFps\":" << JsonNumber(Pacing.cvarMaxFps) + << ",\"cachedMaxFps\":" << JsonNumber(Pacing.cachedMaxFps) + << ",\"virtualMaxFps\":" << JsonNumber(Pacing.virtualMaxFps) + << ",\"rollingMedianCadenceHz\":" << JsonNumber(Pacing.rollingMedianCadenceHz) << '}' + << ",\"evidence\":{\"nativePacingWaitMs\":" << JsonNumber(Pacing.nativePacingWaitMilliseconds) + << ",\"nativePacingOvershootMs\":" << JsonNumber(Pacing.nativePacingOvershootMilliseconds) + << ",\"nativeDeltaMs\":" << JsonNumber(Pacing.nativeDeltaMilliseconds) + << ",\"rollingP95EngineWorkMs\":" << JsonNumber(Pacing.rollingP95EngineWorkMilliseconds) + << ",\"rollingMedianCoarseOvershootMs\":" << JsonNumber(Pacing.rollingMedianCoarseOvershootMilliseconds) + << ",\"rollingP95FineSpinMs\":" << JsonNumber(Pacing.rollingP95FineSpinMilliseconds) + << ",\"rollingP95NativeOvershootMs\":" << JsonNumber(Pacing.rollingP95NativeOvershootMilliseconds) + << ",\"guardReductionBaselineFineSpinMs\":" + << JsonNumber(Pacing.guardReductionBaselineFineSpinMilliseconds) << '}' + << ",\"waits\":{\"coarse\":"; + WriteWaitCounters(Stream, Pacing.coarseWait); + Stream << ",\"yield\":"; + WriteWaitCounters(Stream, Pacing.yieldWait); + Stream << ",\"other\":"; + WriteWaitCounters(Stream, Pacing.otherWait); + Stream << ",\"corrected\":"; + WriteWaitCounters(Stream, Pacing.correctedWait); + Stream << ",\"guardExtended\":"; + WriteWaitCounters(Stream, Pacing.guardExtendedWait); + Stream << "},\"histograms\":{\"nativePacingWaitMs\":"; + WritePacingHistogram(Stream, Pacing.nativePacingWaitHistogram); + Stream << ",\"nativePacingOvershootMs\":"; + WritePacingHistogram(Stream, Pacing.nativePacingOvershootHistogram); + Stream << ",\"nativeDeltaMs\":"; + WritePacingHistogram(Stream, Pacing.nativeDeltaHistogram); + Stream << ",\"engineWorkMs\":"; + WritePacingHistogram(Stream, Pacing.engineWorkHistogram); + Stream << ",\"frameGapMs\":"; + WritePacingHistogram(Stream, Pacing.frameGapHistogram); + Stream << "}}"; + } + + void WriteActorPropertyDistribution( + std::ostream& Stream, + const Networking::ProfilingCounters& Counters + ) { + Stream << "{\"samples\":" << Counters.ActorPropertySamples + << ",\"frequencyBandLabels\":[\"invalid\",\"le0\",\"le2\",\"le5\",\"le10\"," + << "\"le15\",\"le30\",\"le60\",\"le100\",\"gt100\"]" + << ",\"netUpdateFrequency\":["; + for (size_t Index = 0; Index < Counters.NetUpdateFrequencyBands.size(); ++Index) { + if (Index) Stream << ','; + Stream << Counters.NetUpdateFrequencyBands[Index]; + } + Stream << "],\"minNetUpdateFrequency\":["; + for (size_t Index = 0; Index < Counters.MinNetUpdateFrequencyBands.size(); ++Index) { + if (Index) Stream << ','; + Stream << Counters.MinNetUpdateFrequencyBands[Index]; + } + Stream << "],\"dormancyBandLabels\":[\"never\",\"awake\",\"dormantAll\"," + << "\"dormantPartial\",\"initial\",\"max\",\"invalid\"],\"dormancy\":["; + for (size_t Index = 0; Index < Counters.NetDormancyBands.size(); ++Index) { + if (Index) Stream << ','; + Stream << Counters.NetDormancyBands[Index]; + } + Stream << "],\"roleLabels\":[\"none\",\"simulatedProxy\",\"autonomousProxy\"," + << "\"authority\",\"maxOrInvalid\"],\"roleMatrix\":["; + for (size_t Index = 0; Index < Counters.NetRoleMatrix.size(); ++Index) { + if (Index) Stream << ','; + Stream << Counters.NetRoleMatrix[Index]; + } + Stream << "],\"owned\":" << Counters.OwnedActorSamples + << ",\"replicatedMovement\":" << Counters.ReplicatedMovementSamples + << ",\"critical\":" << Counters.CriticalActorSamples + << ",\"immediate\":" << Counters.ImmediateActorSamples + << ",\"netTemporary\":" << Counters.NetTemporarySamples + << ",\"netStartup\":" << Counters.NetStartupSamples + << ",\"netLoadOnClient\":" << Counters.NetLoadOnClientSamples + << ",\"onlyRelevantToOwner\":" + << Counters.OnlyRelevantToOwnerSamples + << ",\"alwaysRelevant\":" << Counters.AlwaysRelevantSamples + << ",\"ownerRelevancy\":" << Counters.OwnerRelevancySamples + << ",\"tearOff\":" << Counters.TearOffSamples + << ",\"excluded\":{\"notReplicated\":" + << Counters.ExcludedNotReplicated + << ",\"remoteRoleNone\":" << Counters.ExcludedRemoteRoleNone + << ",\"localRoleNone\":" << Counters.ExcludedLocalRoleNone + << ",\"unexpectedLocalRole\":" << Counters.UnexpectedLocalRole + << ",\"destroying\":" << Counters.ExcludedDestroying + << ",\"wrongWorld\":" << Counters.ExcludedWrongWorld << '}' + << ",\"channelStateSamples\":" << Counters.ChannelStateSamples + << ",\"channelOpenAcknowledged\":" << Counters.ChannelOpenAcknowledgedSamples + << ",\"channelNotOpenAcknowledged\":" + << Counters.ChannelNotOpenAcknowledgedSamples << '}'; + } + + void ResetInterval() { + EngineTickDuration.Reset(); + FrameGap.Reset(); + NetworkingDuration.Reset(); + LongFrames50Ms = 0; + LongFrames100Ms = 0; + LongFrames250Ms = 0; + IntervalStarted = std::chrono::steady_clock::now(); + } + + bool WriteAndFlush(const std::string& Buffer, uint64_t& BytesWritten, DWORD& Error) { + BytesWritten = 0; + Error = ERROR_SUCCESS; + if (ProfileFile == INVALID_HANDLE_VALUE || Buffer.empty()) { + Error = ERROR_INVALID_HANDLE; + return false; + } + if (Buffer.size() > MaximumProfileBytes + || TotalProfileBytesWritten > MaximumProfileBytes - Buffer.size()) { + Error = ERROR_FILE_TOO_LARGE; + return false; + } + + size_t Offset = 0; + while (Offset < Buffer.size()) { + const DWORD Requested = static_cast((std::min)( + Buffer.size() - Offset, static_cast(MAXDWORD))); + DWORD Written = 0; + if (!WriteFile(ProfileFile, Buffer.data() + Offset, Requested, &Written, nullptr) || Written == 0) { + Error = GetLastError(); + return false; + } + Offset += Written; + BytesWritten += Written; + } + + if (!FlushFileBuffers(ProfileFile)) { + Error = GetLastError(); + return false; + } + TotalProfileBytesWritten += BytesWritten; + return true; + } + + void WriteLifecycleRecords(std::ostream& Stream, const IntervalSnapshot& Snapshot) { + for (size_t Index = 0; Index < Snapshot.LifecycleRecordCount; ++Index) { + const LifecycleRecord& Record = Snapshot.LifecycleRecords[Index]; + Stream << "{\"type\":\"lifecycle\",\"timestamp\":\"" + << FormatUtcTimestamp(Record.Timestamp, false) + << "\",\"pid\":" << GetCurrentProcessId() + << ",\"serverId\":\"" << JsonEscape(ServerId) + << "\",\"dllSha256\":\"" << JsonEscape(DllSha256) + << "\",\"event\":\"" << JsonEscape(Record.EventName.data()) << "\""; + if (Record.FieldsJson[0]) Stream << ',' << Record.FieldsJson.data(); + Stream << "}\n"; + } + } + + void WriteInterval(std::ostream& Output, const IntervalSnapshot& Snapshot) { + const Networking::ProfilingCounters& NetworkingCounters = Snapshot.NetworkingCounters; + const double EngineHz = Snapshot.ElapsedSeconds > 0.0 + ? Snapshot.EngineTickDuration.samples / Snapshot.ElapsedSeconds : 0.0; + const double ActorSchedulerHz = Snapshot.ElapsedSeconds > 0.0 + ? NetworkingCounters.SchedulerTickCalls / Snapshot.ElapsedSeconds : 0.0; + const double TransportDispatchHz = Snapshot.ElapsedSeconds > 0.0 + ? NetworkingCounters.NativeDispatchCalls / Snapshot.ElapsedSeconds : 0.0; + const double TransportFlushHz = Snapshot.ElapsedSeconds > 0.0 + ? NetworkingCounters.NativeFlushCalls / Snapshot.ElapsedSeconds : 0.0; + const double AverageConnections = NetworkingCounters.ConnectionSamples + ? static_cast(NetworkingCounters.ConnectionTotal) / NetworkingCounters.ConnectionSamples : 0.0; + const double AverageLoadedWorldLatency = NetworkingCounters.LoadedWorldLatencySamples + ? static_cast(NetworkingCounters.LoadedWorldLatencyMilliseconds) + / NetworkingCounters.LoadedWorldLatencySamples : 0.0; + const double AverageBootstrapLatency = NetworkingCounters.BootstrapLatencySamples + ? static_cast(NetworkingCounters.BootstrapLatencyMilliseconds) + / NetworkingCounters.BootstrapLatencySamples : 0.0; + const auto AverageMicroseconds = [](uint64_t Total, uint64_t Samples) { + return Samples ? static_cast(Total) / Samples : 0.0; + }; + + Output << std::fixed << std::setprecision(3) + << "{\"type\":\"interval\",\"profileSchemaVersion\":" + << Networking::ProfileSchemaVersion + << ",\"timestamp\":\"" << FormatUtcTimestamp(Snapshot.Timestamp, false) + << "\",\"pid\":" << GetCurrentProcessId() + << ",\"serverId\":\"" << JsonEscape(ServerId) + << "\",\"dllSha256\":\"" << JsonEscape(DllSha256) + << "\",\"configuredMaxFps\":" << ConfiguredMaxFps + << ",\"observedMaxFps\":" << JsonNumber(Snapshot.ObservedMaxFps) + << ",\"observedEngineHz\":" << EngineHz + << ",\"observedActorSchedulerHz\":" << ActorSchedulerHz + << ",\"observedTransportDispatchHz\":" << TransportDispatchHz + << ",\"observedTransportFlushHz\":" << TransportFlushHz + << ",\"engineTickMs\":"; + WriteHistogram(Output, Snapshot.EngineTickDuration); + Output << ",\"frameGapMs\":"; + WriteHistogram(Output, Snapshot.FrameGap); + Output << ",\"networkingMs\":"; + WriteHistogram(Output, Snapshot.NetworkingDuration); + Output << ",\"pacing\":"; + WritePacing(Output, Snapshot.Pacing); + Output << ",\"longFrames\":{\"over50Ms\":" << Snapshot.LongFrames50Ms + << ",\"over100Ms\":" << Snapshot.LongFrames100Ms + << ",\"over250Ms\":" << Snapshot.LongFrames250Ms << "}" + << ",\"replication\":{\"attempts\":" << NetworkingCounters.ReplicationAttempts + << ",\"successes\":" << NetworkingCounters.ReplicationSuccesses + << ",\"ordinaryAttempts\":" + << (NetworkingCounters.ReplicationAttempts - NetworkingCounters.BootstrapReplicationAttempts) + << ",\"ordinarySuccesses\":" + << (NetworkingCounters.ReplicationSuccesses - NetworkingCounters.BootstrapReplicationSuccesses) + << ",\"bootstrapAttempts\":" << NetworkingCounters.BootstrapReplicationAttempts + << ",\"bootstrapSuccesses\":" << NetworkingCounters.BootstrapReplicationSuccesses + << ",\"preReplicationCalls\":" + << NetworkingCounters.PreReplicationCalls + << ",\"preReplicationUnavailable\":" + << NetworkingCounters.PreReplicationUnavailable + << ",\"preReplicationInvalidatedActors\":" + << NetworkingCounters.PreReplicationInvalidatedActors + << ",\"actorChannelsCreated\":" << NetworkingCounters.ActorChannelsCreated + << ",\"actorChannelsReused\":" << NetworkingCounters.ActorChannelsReused + << ",\"buckets\":{"; + for (size_t Index = 0; Index < ReplicationBucketNames.size(); ++Index) { + if (Index) Output << ','; + Output << '\"' << ReplicationBucketNames[Index] << "\":{\"attempts\":" + << NetworkingCounters.ReplicationAttemptsByBucket[Index] + << ",\"successes\":" << NetworkingCounters.ReplicationSuccessesByBucket[Index] << '}'; + } + Output << "},\"urgentDamage\":{\"rpcMatches\":" + << NetworkingCounters.UrgentDamageRpcMatches + << ",\"interruptCorrelations\":" + << NetworkingCounters.UrgentDamageInterruptCorrelations + << ",\"invalidWeakHandles\":" << NetworkingCounters.UrgentDamageWeakHandlesInvalid + << ",\"directBehemothTargets\":" << NetworkingCounters.UrgentDamageDirectBehemothTargets + << ",\"ownerChainTargets\":" << NetworkingCounters.UrgentDamageOwnerChainTargets + << ",\"targetsWithoutBehemoth\":" << NetworkingCounters.UrgentDamageTargetsWithoutBehemoth + << ",\"queued\":" << NetworkingCounters.UrgentDamageTargetsQueued + << ",\"deduplicated\":" << NetworkingCounters.UrgentDamageTargetsDeduplicated + << ",\"dropped\":" << NetworkingCounters.UrgentDamageTargetsDropped + << ",\"invalidated\":" << NetworkingCounters.UrgentDamageTargetsInvalidated + << ",\"expired\":" << NetworkingCounters.UrgentDamageTargetsExpired + << ",\"setupRetries\":" << NetworkingCounters.UrgentDamageSetupRetries + << ",\"setupRetryExhausted\":" + << NetworkingCounters.UrgentDamageSetupRetryExhausted + << ",\"replicationAttempts\":" << NetworkingCounters.UrgentDamageReplicationAttempts + << ",\"replicationSuccesses\":" << NetworkingCounters.UrgentDamageReplicationSuccesses + << ",\"clientInterruptNotifications\":" + << NetworkingCounters.BehemothInterruptClientNotifications + << ",\"averageReceiveToPassMs\":" << AverageMicroseconds( + NetworkingCounters.UrgentDamageLatencyMilliseconds * 1000, + NetworkingCounters.UrgentDamageLatencySamples) / 1000.0 << "}" + << ",\"urgentByReason\":{"; + for (size_t Index = 0; Index < UrgentReplicationReasonNames.size(); ++Index) { + if (Index) Output << ','; + Output << '\"' << UrgentReplicationReasonNames[Index] << "\":{" + << "\"queued\":" << NetworkingCounters.UrgentQueuedByReason[Index] + << ",\"deduplicated\":" << NetworkingCounters.UrgentDeduplicatedByReason[Index] + << ",\"dropped\":" << NetworkingCounters.UrgentDroppedByReason[Index] + << ",\"invalidated\":" << NetworkingCounters.UrgentInvalidatedByReason[Index] + << ",\"expired\":" << NetworkingCounters.UrgentExpiredByReason[Index] + << ",\"attempts\":" << NetworkingCounters.UrgentAttemptsByReason[Index] + << ",\"successes\":" << NetworkingCounters.UrgentSuccessesByReason[Index] + << ",\"latencySamples\":" << NetworkingCounters.UrgentLatencySamplesByReason[Index] + << ",\"averageLatencyMs\":" << AverageMicroseconds( + NetworkingCounters.UrgentLatencyMillisecondsByReason[Index] * 1000, + NetworkingCounters.UrgentLatencySamplesByReason[Index]) / 1000.0 + << '}'; + } + Output << "},\"criticalPrepass\":{" + << "\"candidates\":" << NetworkingCounters.CriticalPrepassCandidates + << ",\"attempts\":" << NetworkingCounters.CriticalPrepassAttempts + << ",\"successes\":" << NetworkingCounters.CriticalPrepassSuccesses + << ",\"duplicateSkips\":" << NetworkingCounters.CriticalPrepassDuplicateSkips + << ",\"setupFallbacks\":" << NetworkingCounters.CriticalPrepassSetupFallbacks + << ",\"ownedFrequencyLimited\":" + << NetworkingCounters.CriticalOwnedFrequencyLimited << "}" + << ",\"immediateActorPassStreakScope\":\"actor_engine_pass_across_connections\"" + << ",\"actorClasses\":["; + bool WroteImmediateClass = false; + for (const Networking::ImmediateClassCounters& ClassCounters : NetworkingCounters.ImmediateClasses) { + if (ClassCounters.Attempts == 0 && + ClassCounters.UntrackableOwnerCalls == 0 && + ClassCounters.OwnerSensitiveDeferrals == 0 && + ClassCounters.IrrelevantSkips == 0) continue; + if (WroteImmediateClass) Output << ','; + WroteImmediateClass = true; + Output << "{\"class\":\"" << JsonEscape(ClassCounters.ClassName.data()) + << "\",\"attempts\":" << ClassCounters.Attempts + << ",\"successes\":" << ClassCounters.Successes + << ",\"actorPasses\":" << ClassCounters.ActorPasses + << ",\"actorPassAttempts\":" << ClassCounters.ActorPassAttempts + << ",\"actorPassSuccesses\":" << ClassCounters.ActorPassSuccesses + << ",\"actorPassesWithAnyData\":" << ClassCounters.ActorPassesWithAnyData + << ",\"actorPassesWithNoData\":" << ClassCounters.ActorPassesWithNoData + << ",\"untrackableOwnerCalls\":" << ClassCounters.UntrackableOwnerCalls + << ",\"ownerSensitiveDeferrals\":" << ClassCounters.OwnerSensitiveDeferrals + << ",\"irrelevantSkips\":" << ClassCounters.IrrelevantSkips + << ",\"immediateNoDataPassStreaks\":["; + for (size_t Band = 0; Band < ClassCounters.NoDataPassStreaks.size(); ++Band) { + if (Band) Output << ','; + Output << ClassCounters.NoDataPassStreaks[Band]; + } + Output << "]}"; + } + Output << "],\"schedulerState\":{\"insertions\":" + << NetworkingCounters.SchedulerStateInsertions + << ",\"capacityDrops\":" + << NetworkingCounters.SchedulerStateCapacityDrops + << ",\"actorIdentityResets\":" + << NetworkingCounters.SchedulerActorIdentityResets + << ",\"ownerChanges\":" + << NetworkingCounters.SchedulerOwnerChanges + << ",\"prunes\":" << NetworkingCounters.SchedulerStatePrunes + << ",\"current\":" << NetworkingCounters.CurrentSchedulerStates + << ",\"maximum\":" << NetworkingCounters.MaximumSchedulerStates + << "}}" + << ",\"combatEvents\":{"; + for (size_t Index = 0; Index < CombatEventNames.size(); ++Index) { + if (Index) Output << ','; + Output << '\"' << CombatEventNames[Index] << "\":{\"calls\":" + << NetworkingCounters.CombatEventCalls[Index] + << ",\"successes\":" << NetworkingCounters.CombatEventSuccesses[Index] << '}'; + } + Output << "},\"combatText\":{" + << "\"queuedEntries\":" << NetworkingCounters.CombatTextEntries + << ",\"queuedEstimatedBytes\":" << NetworkingCounters.CombatTextEstimatedBytes + << ",\"multicastEntries\":" << NetworkingCounters.CombatTextMulticastEntries + << ",\"multicastEstimatedBytes\":" + << NetworkingCounters.CombatTextMulticastEstimatedBytes << "}" + << ",\"actorProperties\":"; + WriteActorPropertyDistribution(Output, NetworkingCounters); + Output << ",\"lastCompletedNetworkingPhase\":" << NetworkingCounters.LastCompletedPhase + << ",\"hotPathTimingUs\":{\"cacheRebuildTotal\":" + << NetworkingCounters.CacheRebuildMicroseconds + << ",\"cacheRebuildSamples\":" << NetworkingCounters.CacheRebuildTimingSamples + << ",\"cacheRebuildMean\":" << AverageMicroseconds( + NetworkingCounters.CacheRebuildMicroseconds, NetworkingCounters.CacheRebuildTimingSamples) + << ",\"candidateSelectionTotal\":" << NetworkingCounters.CandidateSelectionMicroseconds + << ",\"candidateSelectionSamples\":" << NetworkingCounters.CandidateSelectionTimingSamples + << ",\"candidateSelectionMean\":" << AverageMicroseconds( + NetworkingCounters.CandidateSelectionMicroseconds, NetworkingCounters.CandidateSelectionTimingSamples) + << ",\"channelScanTotal\":" << NetworkingCounters.ChannelScanMicroseconds + << ",\"channelScanSamples\":" << NetworkingCounters.ChannelScanTimingSamples + << ",\"channelScanMean\":" << AverageMicroseconds( + NetworkingCounters.ChannelScanMicroseconds, NetworkingCounters.ChannelScanTimingSamples) + << ",\"replicationTotal\":" << NetworkingCounters.ReplicationMicroseconds + << ",\"replicationSamples\":" << NetworkingCounters.ReplicationTimingSamples + << ",\"replicationMean\":" << AverageMicroseconds( + NetworkingCounters.ReplicationMicroseconds, NetworkingCounters.ReplicationTimingSamples) << "}" + << ",\"considerCache\":{\"rebuilds\":" << NetworkingCounters.CacheRebuilds + << ",\"currentCandidates\":" << NetworkingCounters.CurrentCandidates + << ",\"maximumCandidates\":" << NetworkingCounters.MaximumCandidates + << ",\"reasonLabels\":[\"forced\",\"worldChanged\",\"worldIdentity\"," + << "\"maximumAge\",\"levelCount\",\"actorCount\"],\"byReason\":["; + for (size_t Index = 0; + Index < NetworkingCounters.CacheRebuildsByReason.size(); ++Index) { + if (Index) Output << ','; + Output << NetworkingCounters.CacheRebuildsByReason[Index]; + } + const uint64_t NaturalCacheRebuilds = + NetworkingCounters.CacheRebuildsByReason[3]; + Output << "],\"naturalRebuilds\":" << NaturalCacheRebuilds + << ",\"forcedRebuilds\":" + << (NetworkingCounters.CacheRebuilds - NaturalCacheRebuilds) << "}" + << ",\"connectionBootstrap\":{\"loadedWorldAccepted\":" << NetworkingCounters.LoadedWorldAccepted + << ",\"loadedWorldMismatched\":" << NetworkingCounters.LoadedWorldMismatched + << ",\"possessionAcknowledged\":" << NetworkingCounters.PossessionAcknowledged + << ",\"starts\":" << NetworkingCounters.BootstrapStarts + << ",\"restarts\":" << NetworkingCounters.BootstrapRestarts + << ",\"completed\":" << NetworkingCounters.BootstrapCompleted + << ",\"deadlines\":" << NetworkingCounters.BootstrapDeadlines + << ",\"criticalActorsDiscovered\":" << NetworkingCounters.CriticalActorsDiscovered + << ",\"criticalChannelsAcknowledged\":" << NetworkingCounters.CriticalChannelsAcknowledged + << ",\"averageLoadedWorldLatencyMs\":" << AverageLoadedWorldLatency + << ",\"averageCompletionLatencyMs\":" << AverageBootstrapLatency + << ",\"droppedLifecycleEvents\":" + << (NetworkingCounters.DroppedLifecycleEvents + Snapshot.DroppedLifecycleRecords) + << ",\"phases\":{\"bootstrapping\":" << NetworkingCounters.BootstrapPhaseConnections[0] + << ",\"active\":" << NetworkingCounters.BootstrapPhaseConnections[1] << "}}" + << ",\"abilityRpc\":{\"matched\":" << Snapshot.AbilityRpcMatched + << ",\"activated\":" << Snapshot.AbilityRpcActivated + << ",\"rejected\":" << Snapshot.AbilityRpcRejected + << ",\"originalFallback\":" << Snapshot.AbilityRpcOriginalFallback << "}" + << ",\"processEvent\":{\"calls\":" << Snapshot.ProcessEventCalls + << ",\"netServerCandidates\":" << Snapshot.ProcessEventNetServerCandidates + << ",\"nameLookups\":" << Snapshot.ProcessEventNameLookups + << ",\"nameFallbacks\":" << Snapshot.ProcessEventNameFallbacks + << ",\"functionMatches\":" << Snapshot.ProcessEventFunctionMatches + << ",\"originalCalls\":" << Snapshot.ProcessEventOriginalCalls << "}" + << ",\"connections\":{\"average\":" << AverageConnections + << ",\"maximum\":" << NetworkingCounters.MaximumConnections + << ",\"zeroConnectionTicks\":" << NetworkingCounters.ZeroConnectionTicks + << ",\"trackedCurrent\":" << Snapshot.TrackedConnections + << ",\"rawCurrent\":" << Snapshot.RawConnections << "}" + << ",\"liveActorPolicy\":{\"eligible\":" + << NetworkingCounters.LivePolicyEligible + << ",\"due\":" << NetworkingCounters.LivePolicyDue + << ",\"notDue\":" << NetworkingCounters.LivePolicyNotDue + << ",\"relevant\":" << NetworkingCounters.LivePolicyRelevant + << ",\"notRelevant\":" << NetworkingCounters.LivePolicyNotRelevant + << ",\"dormantDeferrals\":" + << NetworkingCounters.LivePolicyDormantDeferrals + << ",\"driverMismatches\":" + << NetworkingCounters.LivePolicyDriverMismatches + << ",\"temporaryRetirements\":" + << NetworkingCounters.LivePolicyTemporaryRetirements + << ",\"tearOffRetirements\":" + << NetworkingCounters.LivePolicyTearOffRetirements + << ",\"identityFallbacks\":" + << NetworkingCounters.LivePolicyIdentityFallbacks + << ",\"actorIdentityFallbacks\":" + << NetworkingCounters.LivePolicyActorIdentityFallbacks + << ",\"connectionIdentityFallbacks\":" + << NetworkingCounters.LivePolicyConnectionIdentityFallbacks + << ",\"stateCapacityFallbacks\":" + << NetworkingCounters.LivePolicyStateCapacityFallbacks + << ",\"untrackableOwners\":" + << NetworkingCounters.LivePolicyUntrackableOwners + << ",\"ownerSensitiveDeferrals\":" + << NetworkingCounters.LivePolicyOwnerSensitiveDeferrals + << ",\"nativeRelevancyCalls\":" + << NetworkingCounters.LivePolicyNativeRelevancyCalls + << ",\"nativeRelevancyFallbacks\":" + << NetworkingCounters.LivePolicyNativeRelevancyFallbacks + << ",\"relevancyAudits\":" + << NetworkingCounters.LivePolicyRelevancyAudits + << ",\"relevancyRecheckSamples\":" + << NetworkingCounters.LivePolicyRelevancyRecheckSamples + << ",\"averageRelevancyRecheckMs\":" << AverageMicroseconds( + NetworkingCounters.LivePolicyRelevancyRecheckMilliseconds * 1000, + NetworkingCounters.LivePolicyRelevancyRecheckSamples) / 1000.0 + << ",\"irrelevantSkips\":" + << NetworkingCounters.LivePolicyIrrelevantSkips + << ",\"levelInitializationUnavailable\":" + << NetworkingCounters.LivePolicyLevelInitializationUnavailable + << ",\"prioritySorts\":" + << NetworkingCounters.LivePolicyPrioritySorts + << ",\"priorityCandidates\":" + << NetworkingCounters.LivePolicyPriorityCandidates + << ",\"priorityBandLabels\":[\"le1\",\"le10\",\"le100\"," + << "\"le1000\",\"gt1000\"],\"priorityBands\":["; + for (size_t Index = 0; + Index < NetworkingCounters.LivePolicyPriorityBands.size(); ++Index) { + if (Index) Output << ','; + Output << NetworkingCounters.LivePolicyPriorityBands[Index]; + } + Output << "]" + << ",\"movementPrepassAttempts\":" + << NetworkingCounters.LivePolicyMovementPrepassAttempts + << ",\"movementPrepassSuccesses\":" + << NetworkingCounters.LivePolicyMovementPrepassSuccesses + << ",\"criticalRejected\":" + << NetworkingCounters.LivePolicyCriticalRejected + << ",\"duplicateSkips\":" + << NetworkingCounters.LivePolicyDuplicateSkips << "}" + << ",\"nativeNetworking\":{\"dispatchCalls\":" + << NetworkingCounters.NativeDispatchCalls + << ",\"dispatchOwnershipValidBefore\":" + << NetworkingCounters.NativeDispatchOwnershipValidBefore + << ",\"dispatchOwnershipValidAfter\":" + << NetworkingCounters.NativeDispatchOwnershipValidAfter + << ",\"dispatchOwnershipLostInside\":" + << NetworkingCounters.NativeDispatchOwnershipLostInside + << ",\"flushCalls\":" << NetworkingCounters.NativeFlushCalls + << ",\"flushOwnershipValidBefore\":" + << NetworkingCounters.NativeFlushOwnershipValidBefore + << ",\"flushOwnershipValidAfter\":" + << NetworkingCounters.NativeFlushOwnershipValidAfter + << ",\"flushOwnershipLostInside\":" + << NetworkingCounters.NativeFlushOwnershipLostInside + << ",\"ownershipMissingAfterEngineTick\":" + << NetworkingCounters.NativeOwnershipMissingAfterEngineTick + << ",\"reciprocalRepairs\":" + << NetworkingCounters.NativeReciprocalRepairs + << ",\"blockedDispatches\":" + << NetworkingCounters.NativeDispatchBlocked + << ",\"conflicts\":" << NetworkingCounters.NativeDispatchConflicts + << ",\"postLogins\":" << NetworkingCounters.NativePostLogins + << "}" + << ",\"profileWriter\":{\"droppedSnapshots\":" << Snapshot.DroppedIntervalSnapshots + << ",\"previousWriteFailures\":" << Snapshot.PreviousIntervalWriteFailures + << ",\"previousWriteError\":" << Snapshot.PreviousIntervalWriteError + << ",\"serializationCalls\":" << Snapshot.WriterSerializationCalls + << ",\"serializationTotalUs\":" << Snapshot.WriterSerializationMicroseconds + << ",\"serializationMaximumUs\":" << Snapshot.WriterSerializationMaximumMicroseconds + << ",\"serializationMeanUs\":" << AverageMicroseconds( + Snapshot.WriterSerializationMicroseconds, Snapshot.WriterSerializationCalls) + << ",\"writeFlushCalls\":" << Snapshot.WriterWriteFlushCalls + << ",\"writeFlushTotalUs\":" << Snapshot.WriterWriteFlushMicroseconds + << ",\"writeFlushMaximumUs\":" << Snapshot.WriterWriteFlushMaximumMicroseconds + << ",\"writeFlushMeanUs\":" << AverageMicroseconds( + Snapshot.WriterWriteFlushMicroseconds, Snapshot.WriterWriteFlushCalls) << "}"; + + const double CpuInterval100Nanoseconds = + Snapshot.ElapsedSeconds * 10000000.0; + const auto CpuPercent = [CpuInterval100Nanoseconds]( + uint64_t Cpu100Nanoseconds) { + return CpuInterval100Nanoseconds > 0.0 + ? static_cast(Cpu100Nanoseconds) / + CpuInterval100Nanoseconds * 100.0 + : 0.0; + }; + Output << ",\"cpuAttribution\":{\"samplingError\":" + << Snapshot.Cpu.SamplingError + << ",\"gameThreadId\":" << Snapshot.Cpu.GameThreadId + << ",\"processCpuMs\":" + << static_cast( + Snapshot.Cpu.ProcessCpu100Nanoseconds) / 10000.0 + << ",\"processPercentOfOneCore\":" + << CpuPercent(Snapshot.Cpu.ProcessCpu100Nanoseconds) + << ",\"gameThreadCpuMs\":" + << static_cast( + Snapshot.Cpu.GameThreadCpu100Nanoseconds) / 10000.0 + << ",\"gameThreadPercentOfOneCore\":" + << CpuPercent(Snapshot.Cpu.GameThreadCpu100Nanoseconds) + << ",\"topThreads\":["; + for (size_t Index = 0; + Index < Snapshot.Cpu.TopThreadCount; ++Index) { + if (Index) Output << ','; + const ThreadCpuEntry& Thread = + Snapshot.Cpu.TopThreads[Index]; + Output << "{\"id\":" << Thread.ThreadId + << ",\"cpuMs\":" + << static_cast(Thread.Cpu100Nanoseconds) / + 10000.0 + << ",\"percentOfOneCore\":" + << CpuPercent(Thread.Cpu100Nanoseconds) + << ",\"description\":\"" + << JsonEscape(Thread.Description.data()) << "\"}"; + } + Output << "]}" + << ",\"netDriver\":{\"netServerMaxTickRate\":" << Snapshot.NetServerMaxTickRate + << ",\"maxNetTickRate\":" << Snapshot.MaxNetTickRate + << ",\"maxClientRate\":" << Snapshot.MaxClientRate << "}}\n"; + } + + unsigned __stdcall WriterMain(void*) { + for (;;) { + const HANDLE Events[] = { StopEvent, PendingSnapshotEvent }; + const DWORD Result = WaitForMultipleObjects(2, Events, FALSE, INFINITE); + if (Result != WAIT_OBJECT_0 && Result != WAIT_OBJECT_0 + 1) continue; + const bool Stopping = Result == WAIT_OBJECT_0; + + AcquireSRWLockExclusive(&PendingSnapshotLock); + if (!HasPendingSnapshot) { + ReleaseSRWLockExclusive(&PendingSnapshotLock); + if (Stopping) break; + continue; + } + WriterSnapshot = *PendingSnapshot; + HasPendingSnapshot = false; + ReleaseSRWLockExclusive(&PendingSnapshotLock); + + SampleThreadCpu(WriterSnapshot.Cpu); + const auto SerializationStartedAt = std::chrono::steady_clock::now(); + std::ostringstream Output; + WriteLifecycleRecords(Output, WriterSnapshot); + WriteInterval(Output, WriterSnapshot); + const std::string Buffer = Output.str(); + const uint64_t SerializationMicroseconds = ElapsedMicroseconds(SerializationStartedAt); + WriterSerializationCalls.fetch_add(1, std::memory_order_relaxed); + WriterSerializationMicroseconds.fetch_add(SerializationMicroseconds, std::memory_order_relaxed); + AtomicMaximum(WriterSerializationMaximumMicroseconds, SerializationMicroseconds); + + uint64_t BytesWritten = 0; + DWORD Error = ERROR_SUCCESS; + const auto WriteStartedAt = std::chrono::steady_clock::now(); + if (WriteAndFlush(Buffer, BytesWritten, Error)) { + IntervalBytesWritten.fetch_add(BytesWritten, std::memory_order_relaxed); + LastIntervalWriteError.store(ERROR_SUCCESS, std::memory_order_relaxed); + } + else { + IntervalWriteFailures.fetch_add(1, std::memory_order_relaxed); + LastIntervalWriteError.store(Error, std::memory_order_relaxed); + } + const uint64_t WriteMicroseconds = ElapsedMicroseconds(WriteStartedAt); + WriterWriteFlushCalls.fetch_add(1, std::memory_order_relaxed); + WriterWriteFlushMicroseconds.fetch_add(WriteMicroseconds, std::memory_order_relaxed); + AtomicMaximum(WriterWriteFlushMaximumMicroseconds, WriteMicroseconds); + if (Stopping) break; + } + return 0; + } + + bool PublishSnapshot(const IntervalSnapshot& Snapshot) { + if (!TryAcquireSRWLockExclusive(&PendingSnapshotLock)) return false; + if (HasPendingSnapshot) { + ReleaseSRWLockExclusive(&PendingSnapshotLock); + return false; + } + *PendingSnapshot = Snapshot; + HasPendingSnapshot = true; + ReleaseSRWLockExclusive(&PendingSnapshotLock); + SetEvent(PendingSnapshotEvent); + return true; + } + } + + void Configure(bool IsEnabled, uint32_t NewIntervalSeconds, const std::string& NewServerId, + const std::string& NewMapPath, const std::wstring& NewOutputDirectory, + uint64_t NewMaximumFileBytes) { + ConfiguredEnabled = IsEnabled; + ProfileOutputDirectory = NewOutputDirectory; + Enabled = IsEnabled && !ProfileOutputDirectory.empty(); + TotalProfileBytesWritten = 0; + IntervalSeconds = (std::clamp)(NewIntervalSeconds, 10u, 3600u); + MaximumProfileBytes = (std::clamp)( + NewMaximumFileBytes, 1024ull * 1024ull, 1024ull * 1024ull * 1024ull); + ServerId = NewServerId; + MapPath = NewMapPath; + DllSha256 = ReadDllSha256Argument(); + } + + bool Start(const AssetOptimization::Metrics& Optimization) { + if (!Enabled || Started) return Started; + + std::error_code DirectoryError; + std::filesystem::create_directories(ProfileOutputDirectory, DirectoryError); + if (DirectoryError) { + StartupWriteError = DirectoryError.value(); + Enabled = false; + return false; + } + + if (!PendingSnapshot) { + PendingSnapshot = new (std::nothrow) IntervalSnapshot{}; + if (!PendingSnapshot) { + WriterStartError = ERROR_NOT_ENOUGH_MEMORY; + Enabled = false; + return false; + } + } + + ProfilePath = ProfileOutputDirectory + / ("gameserver-perf-" + UtcTimestamp(true) + "-" + std::to_string(GetCurrentProcessId()) + ".jsonl"); + ProfileFile = CreateFileW(ProfilePath.c_str(), FILE_APPEND_DATA, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_ALWAYS, + FILE_ATTRIBUTE_NORMAL, nullptr); + if (ProfileFile == INVALID_HANDLE_VALUE) { + StartupWriteError = GetLastError(); + Enabled = false; + return false; + } + + std::ostringstream Startup; + Startup << "{\"type\":\"startup\",\"profileSchemaVersion\":" + << Networking::ProfileSchemaVersion + << ",\"timestamp\":\"" << UtcTimestamp(false) + << "\",\"pid\":" << GetCurrentProcessId() + << ",\"serverId\":\"" << JsonEscape(ServerId) + << "\",\"map\":\"" << JsonEscape(MapPath) + << "\",\"dllSha256\":\"" << JsonEscape(DllSha256) + << "\",\"optimization\":" << AssetOptimization::MetricsJson(Optimization) + << ",\"pacing\":"; + WritePacing(Startup, ServerPacing::GetSnapshot()); + Startup << "}\n"; + const std::string StartupBuffer = Startup.str(); + StartupWriteSucceeded = WriteAndFlush(StartupBuffer, StartupBytesWritten, StartupWriteError); + + LARGE_INTEGER FileSize{}; + if (StartupWriteSucceeded && (!GetFileSizeEx(ProfileFile, &FileSize) || FileSize.QuadPart <= 0)) { + StartupWriteSucceeded = false; + StartupWriteError = GetLastError(); + if (StartupWriteError == ERROR_SUCCESS) StartupWriteError = ERROR_WRITE_FAULT; + } + if (!StartupWriteSucceeded) { + CloseHandle(ProfileFile); + ProfileFile = INVALID_HANDLE_VALUE; + Enabled = false; + return false; + } + + PendingSnapshotEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr); + if (!PendingSnapshotEvent) { + WriterStartError = GetLastError(); + CloseHandle(ProfileFile); + ProfileFile = INVALID_HANDLE_VALUE; + Enabled = false; + return false; + } + + StopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (!StopEvent) { + WriterStartError = GetLastError(); + CloseHandle(PendingSnapshotEvent); + PendingSnapshotEvent = nullptr; + CloseHandle(ProfileFile); + ProfileFile = INVALID_HANDLE_VALUE; + Enabled = false; + return false; + } + + const uintptr_t Thread = _beginthreadex(nullptr, 0, WriterMain, nullptr, 0, nullptr); + if (Thread == 0) { + WriterStartError = ERROR_NOT_ENOUGH_MEMORY; + CloseHandle(StopEvent); + StopEvent = nullptr; + CloseHandle(PendingSnapshotEvent); + PendingSnapshotEvent = nullptr; + CloseHandle(ProfileFile); + ProfileFile = INVALID_HANDLE_VALUE; + Enabled = false; + return false; + } + WriterThread = reinterpret_cast(Thread); + + WriterStarted = true; + Started = true; + if (!ExitHandlerRegistered) { + std::atexit(ServerPerformance::Stop); + ExitHandlerRegistered = true; + } + ResetInterval(); + return true; + } + + void Stop() { + Started = false; + if (StopEvent && WriterThread) { + SetEvent(StopEvent); + if (WaitForSingleObject(WriterThread, 10'000) != WAIT_OBJECT_0) + return; + CloseHandle(WriterThread); + WriterThread = nullptr; + } + if (ProfileFile != INVALID_HANDLE_VALUE) { + FlushFileBuffers(ProfileFile); + CloseHandle(ProfileFile); + ProfileFile = INVALID_HANDLE_VALUE; + } + if (PendingSnapshotEvent) { + CloseHandle(PendingSnapshotEvent); + PendingSnapshotEvent = nullptr; + } + if (StopEvent) { + CloseHandle(StopEvent); + StopEvent = nullptr; + } + delete PendingSnapshot; + PendingSnapshot = nullptr; + HasPendingSnapshot = false; + WriterStarted = false; + Enabled = false; + } + + std::string StatusJson() { + std::ostringstream Output; + Output << "{\"enabled\":" << (ConfiguredEnabled ? "true" : "false") + << ",\"started\":" << (Started ? "true" : "false") + << ",\"startupWriteSucceeded\":" << (StartupWriteSucceeded ? "true" : "false") + << ",\"writerStarted\":" << (WriterStarted ? "true" : "false") + << ",\"maximumBytes\":" << MaximumProfileBytes + << ",\"bytesWritten\":" << TotalProfileBytesWritten + << ",\"path\":\"" << JsonEscape(WideToUtf8(ProfilePath.wstring())) + << "\",\"startupBytes\":" << StartupBytesWritten + << ",\"startupError\":" << StartupWriteError + << ",\"writerError\":" << WriterStartError + << ",\"intervalBytes\":" << IntervalBytesWritten.load(std::memory_order_relaxed) + << ",\"intervalWriteFailures\":" << IntervalWriteFailures.load(std::memory_order_relaxed) + << ",\"lastIntervalError\":" << LastIntervalWriteError.load(std::memory_order_relaxed) + << ",\"writerSerializationMaximumUs\":" + << WriterSerializationMaximumMicroseconds.load(std::memory_order_relaxed) + << ",\"writerWriteFlushMaximumUs\":" + << WriterWriteFlushMaximumMicroseconds.load(std::memory_order_relaxed) + << ",\"dllSha256\":\"" << JsonEscape(DllSha256) << "\"}"; + return Output.str(); + } + + void RecordEngineTick(float DeltaSeconds, double OriginalTickMilliseconds) { + if (!Started) return; + DWORD ExpectedThreadId = 0; + GameThreadId.compare_exchange_strong( + ExpectedThreadId, GetCurrentThreadId(), + std::memory_order_relaxed); + const double GapMilliseconds = static_cast(DeltaSeconds) * 1000.0; + EngineTickDuration.Add(OriginalTickMilliseconds); + FrameGap.Add(GapMilliseconds); + if (GapMilliseconds > 50.0) ++LongFrames50Ms; + if (GapMilliseconds > 100.0) ++LongFrames100Ms; + if (GapMilliseconds > 250.0) ++LongFrames250Ms; + } + + void RecordNetworking(double Milliseconds) { + if (Started) NetworkingDuration.Add(Milliseconds); + } + + void RecordProcessEvent(bool NetServerCandidate, bool OriginalCall) { + if (!Started) return; + ++ProcessEventCalls; + if (NetServerCandidate) ++ProcessEventNetServerCandidates; + if (OriginalCall) ++ProcessEventOriginalCalls; + } + + void RecordProcessEventNameLookup(bool UsedFallback) { + if (!Started) return; + ++ProcessEventNameLookups; + if (UsedFallback) ++ProcessEventNameFallbacks; + } + + void RecordProcessEventFunctionMatch() { + if (Started) ++ProcessEventFunctionMatches; + } + + void RecordAbilityRpc(bool Activated, bool UsedOriginalFallback) { + if (!Started) return; + ++AbilityRpcMatched; + if (Activated) ++AbilityRpcActivated; + else ++AbilityRpcRejected; + if (UsedOriginalFallback) ++AbilityRpcOriginalFallback; + } + + std::string JsonString(const std::string& Value) { + return "\"" + JsonEscape(Value) + "\""; + } + + void RecordLifecycleEvent(const std::string& EventName, const std::string& FieldsJson) { + if (!Started) return; + if (PendingLifecycleRecordCount >= PendingLifecycleRecords.size() + || EventName.size() >= LifecycleEventNameCapacity + || FieldsJson.size() >= LifecycleFieldsCapacity) { + ++DroppedLifecycleRecords; + return; + } + + LifecycleRecord& Record = PendingLifecycleRecords[PendingLifecycleRecordCount++]; + Record = {}; + GetSystemTime(&Record.Timestamp); + std::memcpy(Record.EventName.data(), EventName.data(), EventName.size()); + if (!FieldsJson.empty()) + std::memcpy(Record.FieldsJson.data(), FieldsJson.data(), FieldsJson.size()); + } + + void Tick() { + if (!Started) return; + const auto Now = std::chrono::steady_clock::now(); + const double ElapsedSeconds = std::chrono::duration(Now - IntervalStarted).count(); + if (ElapsedSeconds < IntervalSeconds) return; + + IntervalSnapshot& Snapshot = CaptureSnapshot; + Snapshot = {}; + GetSystemTime(&Snapshot.Timestamp); + Snapshot.ElapsedSeconds = ElapsedSeconds; + Snapshot.ObservedMaxFps = UKismetSystemLibrary::GetConsoleVariableFloatValue(L"t.MaxFPS"); + Snapshot.EngineTickDuration = EngineTickDuration; + Snapshot.FrameGap = FrameGap; + Snapshot.NetworkingDuration = NetworkingDuration; + Snapshot.LongFrames50Ms = LongFrames50Ms; + Snapshot.LongFrames100Ms = LongFrames100Ms; + Snapshot.LongFrames250Ms = LongFrames250Ms; + Snapshot.AbilityRpcMatched = AbilityRpcMatched; + Snapshot.AbilityRpcActivated = AbilityRpcActivated; + Snapshot.AbilityRpcRejected = AbilityRpcRejected; + Snapshot.AbilityRpcOriginalFallback = AbilityRpcOriginalFallback; + Snapshot.ProcessEventCalls = ProcessEventCalls; + Snapshot.ProcessEventNetServerCandidates = ProcessEventNetServerCandidates; + Snapshot.ProcessEventNameLookups = ProcessEventNameLookups; + Snapshot.ProcessEventNameFallbacks = ProcessEventNameFallbacks; + Snapshot.ProcessEventFunctionMatches = ProcessEventFunctionMatches; + Snapshot.ProcessEventOriginalCalls = ProcessEventOriginalCalls; + Snapshot.DroppedLifecycleRecords = DroppedLifecycleRecords; + Snapshot.DroppedIntervalSnapshots = DroppedIntervalSnapshots; + Snapshot.PreviousIntervalWriteFailures = IntervalWriteFailures.load(std::memory_order_relaxed); + Snapshot.PreviousIntervalWriteError = LastIntervalWriteError.load(std::memory_order_relaxed); + Snapshot.WriterSerializationCalls = WriterSerializationCalls.load(std::memory_order_relaxed); + Snapshot.WriterSerializationMicroseconds = WriterSerializationMicroseconds.load(std::memory_order_relaxed); + Snapshot.WriterSerializationMaximumMicroseconds = + WriterSerializationMaximumMicroseconds.load(std::memory_order_relaxed); + Snapshot.WriterWriteFlushCalls = WriterWriteFlushCalls.load(std::memory_order_relaxed); + Snapshot.WriterWriteFlushMicroseconds = WriterWriteFlushMicroseconds.load(std::memory_order_relaxed); + Snapshot.WriterWriteFlushMaximumMicroseconds = + WriterWriteFlushMaximumMicroseconds.load(std::memory_order_relaxed); + Snapshot.NetworkingCounters = Networking::TakeProfilingCounters(); + Snapshot.Pacing = ServerPacing::GetSnapshot(); + Snapshot.LifecycleRecordCount = PendingLifecycleRecordCount; + std::copy_n(PendingLifecycleRecords.begin(), PendingLifecycleRecordCount, + Snapshot.LifecycleRecords.begin()); + + UNetDriver* Driver = Networking::NetDriver; + Snapshot.TrackedConnections = static_cast(Networking::GetLiveConnections().size()); + Snapshot.RawConnections = Driver ? static_cast((std::max)(0, Driver->ClientConnections.Num())) : 0; + Snapshot.NetServerMaxTickRate = Driver ? Driver->NetServerMaxTickRate : 0; + Snapshot.MaxNetTickRate = Driver ? Driver->MaxNetTickRate : 0; + Snapshot.MaxClientRate = Driver ? Driver->MaxClientRate : 0; + + PendingLifecycleRecordCount = 0; + AbilityRpcMatched = 0; + AbilityRpcActivated = 0; + AbilityRpcRejected = 0; + AbilityRpcOriginalFallback = 0; + ProcessEventCalls = 0; + ProcessEventNetServerCandidates = 0; + ProcessEventNameLookups = 0; + ProcessEventNameFallbacks = 0; + ProcessEventFunctionMatches = 0; + ProcessEventOriginalCalls = 0; + DroppedLifecycleRecords = 0; + ResetInterval(); + + if (PublishSnapshot(Snapshot)) { + DroppedIntervalSnapshots = 0; + } + else { + ++DroppedIntervalSnapshots; + // The networking counters and lifecycle ring were already consumed into + // this snapshot. Carry every lost record forward if the single writer + // slot was occupied so the next successful interval reports the loss. + DroppedLifecycleRecords += Snapshot.LifecycleRecordCount + + Snapshot.DroppedLifecycleRecords + + Snapshot.NetworkingCounters.DroppedLifecycleEvents; + } + } +} diff --git a/UndauntedInternalServer/ServerPerformance.h b/UndauntedInternalServer/ServerPerformance.h new file mode 100644 index 0000000..f705752 --- /dev/null +++ b/UndauntedInternalServer/ServerPerformance.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +namespace AssetOptimization { struct Metrics; } + +namespace ServerPerformance { + void Configure(bool enabled, uint32_t intervalSeconds, const std::string& serverId, + const std::string& mapPath, const std::wstring& outputDirectory, + uint64_t maximumFileBytes); + bool Start(const AssetOptimization::Metrics& optimization); + void Stop(); + std::string StatusJson(); + void RecordEngineTick(float deltaSeconds, double originalTickMilliseconds); + void RecordNetworking(double milliseconds); + void RecordProcessEvent(bool netServerCandidate, bool originalCall); + void RecordProcessEventNameLookup(bool usedFallback); + void RecordProcessEventFunctionMatch(); + void RecordAbilityRpc(bool activated, bool usedOriginalFallback); + std::string JsonString(const std::string& value); + void RecordLifecycleEvent(const std::string& eventName, const std::string& fieldsJson = ""); + void Tick(); +} diff --git a/UndauntedInternalServer/UndauntedInternalServer.vcxproj b/UndauntedInternalServer/UndauntedInternalServer.vcxproj index 171d916..88f84d4 100644 --- a/UndauntedInternalServer/UndauntedInternalServer.vcxproj +++ b/UndauntedInternalServer/UndauntedInternalServer.vcxproj @@ -121,34 +121,57 @@ Level3 + Full + Speed + AnySuitable true true + true + true + false + Fast true NDEBUG;UNDAUNTEDINTERNALSERVER_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) true NotUsing pch.h stdcpplatest + /FS /Gw /Zc:inline %(AdditionalOptions) Windows true + $(OutDir)$(TargetName)-build.pdb + UseLinkTimeCodeGeneration + true + true false + + + + + + + + - + + + + @@ -163,4 +186,4 @@ - \ No newline at end of file + diff --git a/UndauntedInternalServer/UndauntedInternalServer.vcxproj.filters b/UndauntedInternalServer/UndauntedInternalServer.vcxproj.filters index fa8b3ef..d6b7f7b 100644 --- a/UndauntedInternalServer/UndauntedInternalServer.vcxproj.filters +++ b/UndauntedInternalServer/UndauntedInternalServer.vcxproj.filters @@ -1,4 +1,4 @@ - + @@ -19,8 +19,20 @@ {f7171c16-3dd7-48b5-89e7-59c77c468888} + + {9a090ef4-0728-4a45-9c7c-7044347405c6} + + + {8c3f11b2-4e2a-4d57-b05e-5c1881a6e9c8} + + + {c8bd8110-1948-4a08-a1f2-8d14d2b9d30e} + + + Header Files + Header Files @@ -30,8 +42,28 @@ Header Files + + Header Files + + + Header Files + + + Hooks + + Networking + Networking + Networking\Private + Networking\Private + Networking\Private + Networking\Private + Networking\Private + Networking + + Source Files + Source Files @@ -59,9 +91,17 @@ SDK - + Networking + Networking + Source Files + + Source Files + + + Hooks + SDK @@ -84,4 +124,4 @@ SDK - \ No newline at end of file + diff --git a/UndauntedInternalServer/dllmain.cpp b/UndauntedInternalServer/dllmain.cpp index b4bbe81..1267291 100644 --- a/UndauntedInternalServer/dllmain.cpp +++ b/UndauntedInternalServer/dllmain.cpp @@ -1,28 +1,54 @@ #include #include +#include +#include +#include +#include #include #include -#include +#include #include #include #include +#include #include +#include + +#pragma comment(lib, "winhttp.lib") #include "framework.h" #include "SDK.hpp" #include "MinHook/MinHook.h" #include "constants.h" +#include "AssetOptimization.h" #include "Networking.h" +#include "Networking/NetworkLifecycle.h" +#include "ServerPerformance.h" +#include "ServerPacing.h" +#include "Hooks/NativeNameCleanup.h" #include "SDK/GameplayAbilities_parameters.hpp" #include "SDK/Archon_parameters.hpp" -#include "SDK/lantern_equipped_ab_parameters.hpp" +#include "SDK/Engine_parameters.hpp" using namespace SDK; +namespace { + char ModuleLifetimeAnchor = 0; + + void PinInjectedModule() { + HMODULE PinnedModule = nullptr; + if (!GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN, + reinterpret_cast(&ModuleLifetimeAnchor), &PinnedModule)) { + OutputDebugStringA("[Undaunted] WARNING: failed to pin injected module; dynamic unload is unsafe\n"); + } + } +} + namespace Globals { - static bool AmServer = false; - static uintptr_t BaseAddress = 0x0; + bool AmServer = false; + uintptr_t BaseAddress = 0x0; bool Listening = false; bool DoListen = false; const wchar_t* ServerAPIKey = nullptr; @@ -32,12 +58,114 @@ namespace Globals { const wchar_t* ExpectedPlayerString = nullptr; int Port = 0; const wchar_t* MyIpAndPort = nullptr; + std::wstring GameserverId; + std::wstring ReadyCallbackUrl; + std::wstring ReadyCallbackToken; + std::wstring LifecycleCallbackUrl; std::wstring MetagameAddress; + std::wstring ServerAPIKeyValue; + std::wstring MapPathValue; + std::wstring BehemothPathValue; + std::wstring MatchmakerHuntIdValue; + std::wstring ExpectedPlayerStringValue; + std::wstring MyIpAndPortValue; + + bool EnableLogging = false; + AssetOptimization::Mode AssetStrippingMode = AssetOptimization::Mode::Aggressive; + bool StripInactiveMapPackages = true; + bool AssetStrippingLogDetails = false; + uint32_t AssetGcWaitSeconds = 15; + bool ProfilingEnabled = false; + uint32_t ProfileIntervalSeconds = 30; + std::wstring ProfileOutputDirectory; + uint32_t ProfileMaximumBytes = 64u * 1024u * 1024u; + uint32_t ConsiderCacheMaxAgeMilliseconds = 250; +} - bool EnableLogging = true; +static std::wstring GetNamedArgument(wchar_t** Args, int NumArgs, const std::wstring& Prefix, + const std::wstring& Fallback) { + for (int Index = 1; Index < NumArgs; ++Index) { + const std::wstring Argument = Args[Index]; + if (Argument.starts_with(Prefix)) return Argument.substr(Prefix.length()); + } + return Fallback; +} + +static bool GetNamedBooleanArgument(wchar_t** Args, int NumArgs, const std::wstring& Prefix, bool Fallback) { + for (int Index = 1; Index < NumArgs; ++Index) { + const std::wstring Argument = Args[Index]; + if (!Argument.starts_with(Prefix)) continue; + const std::wstring Value = Argument.substr(Prefix.length()); + if (Value == L"true" || Value == L"1") return true; + if (Value == L"false" || Value == L"0") return false; + return Fallback; + } + return Fallback; +} + +static uint32_t GetNamedUnsignedArgument(wchar_t** Args, int NumArgs, const std::wstring& Prefix, + uint32_t Fallback, uint32_t Minimum, uint32_t Maximum) { + const std::wstring Value = GetNamedArgument(Args, NumArgs, Prefix, L""); + if (Value.empty()) return Fallback; + try { + const unsigned long Parsed = std::stoul(Value); + if (Parsed < Minimum || Parsed > Maximum) return Fallback; + return static_cast(Parsed); + } + catch (...) { + return Fallback; + } +} + +namespace ClientHookConfig { + constexpr bool kEnableNativeNameCleanup = true; } std::map EndpointMap = {}; + +// TODO: ConfigCacheIni::GetString called for every INI setting during startup. +bool WideContains(const wchar_t* Value, const wchar_t* Needle) { + return Value != nullptr && Needle != nullptr && wcsstr(Value, Needle) != nullptr; +} + +bool WideContainsInsensitive(const wchar_t* Value, const wchar_t* Needle) { + if (Value == nullptr || Needle == nullptr) + return false; + + const size_t NeedleLength = wcslen(Needle); + if (NeedleLength == 0) + return true; + + for (const wchar_t* Candidate = Value; *Candidate != L'\0'; ++Candidate) { + size_t Index = 0; + while (Index < NeedleLength && Candidate[Index] != L'\0' && + towlower(Candidate[Index]) == towlower(Needle[Index])) { + ++Index; + } + + if (Index == NeedleLength) + return true; + } + + return false; +} + +bool MayBeMappedEndpointKey(const wchar_t* Key) { + return WideContains(Key, L"Endpoint") || + (Key != nullptr && wcscmp(Key, L"StoreReconcileUrl") == 0); +} + +std::wstring FindClientMetagameAddress(wchar_t** Args, int NumArgs) { + for (int Index = 1; Index < NumArgs; ++Index) { + if (!Args[Index] || Args[Index][0] == L'\0' || Args[Index][0] == L'-') + continue; + + return Args[Index]; + } + + return L""; +} + void EvalEndpointMap() { static bool DidEvalEndpointMap = false; @@ -52,6 +180,8 @@ void EvalEndpointMap() { {L"AuthTagsEndpoint", L"http://" + Globals::MetagameAddress + L"/tags"}, {L"AccountInfoEndpoint", L"http://" + Globals::MetagameAddress + L"/accountinfo"}, {L"DauntlessSessionTokenEndpoint", L"http://" + Globals::MetagameAddress + L"/gamesession/{linkedaccountservice}"}, + // TODO: CreatePhoenixAccountEndpoint likely responsible for /account127.0.0.1 + // but why? {L"CreatePhoenixAccountEndpoint", L"http://" + Globals::MetagameAddress + L"/account"}, {L"LinkAccountEndpoint", L"http://" + Globals::MetagameAddress + L"/account/link"}, {L"IsAccountLinkedEndpoint", L"http://" + Globals::MetagameAddress + L"/account/link/{service}/{accountid}"}, @@ -161,7 +291,7 @@ void EvalEndpointMap() { {L"EntitlementsEndpoint", L"http://" + Globals::MetagameAddress + L"/entitlementsv2"}, {L"GrantEntitlementEndpoint", L"http://" + Globals::MetagameAddress + L"/entitlementv2/{accountid}"}, {L"RevokeEntitlementEndpoint", L"http://" + Globals::MetagameAddress + L"/entitlement/{accountid}/{entitlement}"}, - {L"ServiceSessionEndpoint", L"http://" + Globals::MetagameAddress + L"/ws/{accountid}"}, + {L"ServiceSessionEndpoint", L"ws://" + Globals::MetagameAddress + L"/xmpp"}, {L"QueryUserPresenceEndpoint", L"http://" + Globals::MetagameAddress + L"/present/{accountid}"}, {L"MatchmakingEndpoint", L"http://" + Globals::MetagameAddress}, {L"TrackingEndpoint", L"http://" + Globals::MetagameAddress}, @@ -222,8 +352,149 @@ __declspec(dllexport) const char* DummyLinkFunc() { return "mrow :3"; } +static std::string WideToUtf8(const std::wstring& Value) { + if (Value.empty()) + return ""; + + int Size = WideCharToMultiByte(CP_UTF8, 0, Value.c_str(), -1, nullptr, 0, nullptr, nullptr); + + if (Size <= 0) + return ""; + + std::string Result(Size, '\0'); + WideCharToMultiByte(CP_UTF8, 0, Value.c_str(), -1, Result.data(), Size, nullptr, nullptr); + Result.pop_back(); + + return Result; +} + +static std::string ServerPacingReadyJson() { + const ServerPacing::Snapshot Snapshot = ServerPacing::GetSnapshot(); + const auto Boolean = [](bool Value) { return Value ? "true" : "false"; }; + const auto Number = [](double Value) { + return std::to_string(std::isfinite(Value) ? Value : 0.0); + }; + return std::string("{\"state\":") + + ServerPerformance::JsonString(ServerPacing::CorrectionStateName(Snapshot.state)) + + ",\"installFailure\":" + + ServerPerformance::JsonString(ServerPacing::InstallFailureName(Snapshot.installFailure)) + + ",\"fallbackReason\":" + + ServerPerformance::JsonString(ServerPacing::FallbackReasonName(Snapshot.fallbackReason)) + + ",\"executableBuildValid\":" + Boolean(Snapshot.executableBuildValid) + + ",\"signaturesValid\":" + Boolean(Snapshot.signaturesValid) + + ",\"hookEnabled\":" + Boolean(Snapshot.hookEnabled) + + ",\"highResolutionApiAvailable\":" + Boolean(Snapshot.highResolutionApiAvailable) + + ",\"timerCreated\":" + Boolean(Snapshot.timerCreated) + + ",\"correctionEverActivated\":" + Boolean(Snapshot.correctionEverActivated) + + ",\"observedFrames\":" + std::to_string(Snapshot.observedFrames) + + ",\"cvarMaxFps\":" + Number(Snapshot.cvarMaxFps) + + ",\"cachedMaxFps\":" + Number(Snapshot.cachedMaxFps) + + ",\"virtualMaxFps\":" + Number(Snapshot.virtualMaxFps) + + ",\"rollingMedianCadenceHz\":" + Number(Snapshot.rollingMedianCadenceHz) + + ",\"rollingMedianCoarseOvershootMs\":" + + Number(Snapshot.rollingMedianCoarseOvershootMilliseconds) + "}"; +} + +static bool PostJsonCallback(const std::wstring& Url, const std::wstring& TokenHeader, + const std::wstring& Token, const std::string& Body) { + URL_COMPONENTS UrlComponents = {}; + wchar_t HostName[256] = {}; + wchar_t UrlPath[1024] = {}; + wchar_t ExtraInfo[1024] = {}; + + UrlComponents.dwStructSize = sizeof(UrlComponents); + UrlComponents.lpszHostName = HostName; + UrlComponents.dwHostNameLength = ARRAYSIZE(HostName); + UrlComponents.lpszUrlPath = UrlPath; + UrlComponents.dwUrlPathLength = ARRAYSIZE(UrlPath); + UrlComponents.lpszExtraInfo = ExtraInfo; + UrlComponents.dwExtraInfoLength = ARRAYSIZE(ExtraInfo); + + if (!WinHttpCrackUrl(Url.c_str(), 0, 0, &UrlComponents)) + return false; + + DWORD RequestFlags = UrlComponents.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0; + + HINTERNET Session = WinHttpOpen(L"UndauntedInternalServer/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); + + if (!Session) + return false; + WinHttpSetTimeouts(Session, 2000, 2000, 2000, 2000); + + HINTERNET Connect = WinHttpConnect(Session, HostName, UrlComponents.nPort, 0); + + if (!Connect) { + WinHttpCloseHandle(Session); + return false; + } + + std::wstring RequestPath = std::wstring(UrlPath) + std::wstring(ExtraInfo); + + HINTERNET Request = WinHttpOpenRequest(Connect, L"POST", RequestPath.c_str(), nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, RequestFlags); + + if (!Request) { + WinHttpCloseHandle(Connect); + WinHttpCloseHandle(Session); + return false; + } + + const std::wstring Headers = L"Content-Type: application/json\r\n" + + TokenHeader + L": " + Token + L"\r\n"; + + BOOL Sent = WinHttpSendRequest(Request, Headers.c_str(), (DWORD)-1L, (LPVOID)Body.data(), (DWORD)Body.size(), (DWORD)Body.size(), 0); + BOOL Received = Sent ? WinHttpReceiveResponse(Request, nullptr) : FALSE; + DWORD StatusCode = 0; + DWORD StatusCodeSize = sizeof(StatusCode); + + if (Received) { + WinHttpQueryHeaders(Request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &StatusCode, &StatusCodeSize, WINHTTP_NO_HEADER_INDEX); + } + + WinHttpCloseHandle(Request); + WinHttpCloseHandle(Connect); + WinHttpCloseHandle(Session); + + return Received && StatusCode >= 200 && StatusCode < 300; +} + +static bool SendReadyCallback(const AssetOptimization::Metrics& Metrics, bool Ready = true, + const std::string& Error = "") { + if (Globals::GameserverId.empty() || Globals::ReadyCallbackUrl.empty() || Globals::ReadyCallbackToken.empty()) + return true; + + const std::string Body = "{\"id\":\"" + WideToUtf8(Globals::GameserverId) + + "\",\"port\":" + std::to_string(Globals::Port) + + ",\"pid\":" + std::to_string(GetCurrentProcessId()) + + ",\"ready\":" + std::string(Ready ? "true" : "false") + + (Error.empty() ? "" : ",\"error\":" + ServerPerformance::JsonString(Error)) + + ",\"optimization\":" + AssetOptimization::MetricsJson(Metrics) + + ",\"profiling\":" + ServerPerformance::StatusJson() + + ",\"pacing\":" + ServerPacingReadyJson() + "}"; + return PostJsonCallback(Globals::ReadyCallbackUrl, + L"x-undaunted-ready-token", Globals::ReadyCallbackToken, Body); +} + +static bool StartLifecycleHeartbeat() { + Networking::Lifecycle::LifecycleConfig Config{}; + Config.callbackUrl = Globals::LifecycleCallbackUrl; + Config.callbackToken = Globals::ReadyCallbackToken; + Config.serverId = Globals::GameserverId; + Config.port = static_cast((std::max)(0, Globals::Port)); + Config.callback = PostJsonCallback; + return Networking::Lifecycle::Start(Config); +} + +static void ObserveLifecycleConnections(uint32_t Tracked, uint32_t Raw, SDK::UWorld* World) { + Networking::Lifecycle::ObserveConnections(Tracked, Raw, World); +} + void MainThread() { - while (!UWorld::GetWorld()) { + // UI and profiling workers contain callbacks into this DLL. Until the + // injector has an explicit out-of-loader-lock shutdown/join protocol, + // pinning is safer than allowing FreeLibrary to unmap live callback code. + PinInjectedModule(); + + while (!SDK::UWorld::GetWorld()) { if (Globals::AmServer) { Sleep(1000); } @@ -232,19 +503,21 @@ void MainThread() { } } - Sleep(3 * 1000); - if (!Globals::AmServer) { + Sleep(3 * 1000); + UEngine* Engine = UEngine::GetEngine(); UInputSettings::GetDefaultObj()->ConsoleKeys[0].KeyName = UKismetStringLibrary::Conv_StringToName(L"F2"); - UObject* NewObject = UGameplayStatics::SpawnObject(Engine->ConsoleClass, Engine->GameViewport); + if (Engine && Engine->GameViewport && Engine->ConsoleClass) { + UObject* NewObject = UGameplayStatics::SpawnObject(Engine->ConsoleClass, Engine->GameViewport); - Engine->GameViewport->ViewportConsole = static_cast(NewObject); + Engine->GameViewport->ViewportConsole = static_cast(NewObject); - if (Globals::EnableLogging) - std::cout << "Spawned UConsole!" << std::endl; + if (Globals::EnableLogging) + std::cout << "Spawned UConsole!" << std::endl; + } } else { if (Globals::EnableLogging) @@ -298,6 +571,10 @@ FString* GetGameDefaultMap(FString* a1) { void* OrigGetCommandLine = nullptr; const wchar_t* GetCommandLineHook() { + if (!Globals::AmServer) { + return reinterpret_cast(OrigGetCommandLine)(); + } + return L"Dauntless-Win64-Shipping.exe -server -unattended -nullrhi -nosound -EpicPortal -RepDriverDisable"; } @@ -319,64 +596,491 @@ bool EnableWatchdog = true; void* OrigGameEngineTick = nullptr; -void GameEngineTickHook(UGameEngine* GameEngine, float DeltaTime, char CanRender) { - reinterpret_cast(OrigGameEngineTick)(GameEngine, DeltaTime, CanRender); +static float ListenReadinessElapsed = 0.0f; +static int ListenReadinessStableTicks = 0; +static bool ListenReadinessFallbackLogged = false; - if (GetAsyncKeyState(VK_F7)) { - for (int i = 0; i < SDK::UObject::GObjects->Num(); i++) - { - SDK::UObject* Obj = SDK::UObject::GObjects->GetByIndex(i); +static bool IsServerReadyToListen() { + SDK::UWorld* World = SDK::UWorld::GetWorld(); + UEngine* Engine = UEngine::GetEngine(); - if (!Obj) - continue; + if (!World || !Engine || !World->NetworkNotify) + return false; - if (Obj->IsA(SDK::UArenaMapHuntsFeature::StaticClass())) - { - UArenaMapHuntsFeature* Quest = (UArenaMapHuntsFeature*)Obj; - - std::cout << Quest->bEnabled << std::endl; - } + if (World->Levels.Num() <= 0) + return false; + + for (ULevel* Level : World->Levels) { + if (Level) + return true; + } + + return false; +} + +static bool ShouldAttemptListen(float DeltaTime) { + ListenReadinessElapsed += DeltaTime; + + if (IsServerReadyToListen()) { + ListenReadinessStableTicks++; + + if (ListenReadinessStableTicks >= 2) + return true; + } + else { + ListenReadinessStableTicks = 0; + } + + if (ListenReadinessElapsed >= 10.0f) { + if (!ListenReadinessFallbackLogged && Globals::EnableLogging) { + ListenReadinessFallbackLogged = true; + std::cout << "Server readiness fallback elapsed; attempting listen" << std::endl; } - while (GetAsyncKeyState(VK_F7)) { + return true; + } + + return false; +} + +static bool ShouldTickStaminaForServer() { + static bool Initialized = false; + static bool ShouldTick = true; + + if (!Initialized) { + const std::wstring MapPath = Globals::MapPath != nullptr ? std::wstring(Globals::MapPath) : L""; + ShouldTick = !MapPath.contains(L"/Maps/ramsgate/"); + Initialized = true; + } + + return ShouldTick; +} + +namespace DedicatedServerFrameCap { + constexpr float MaximumFps = 30.0f; + constexpr uintptr_t RegistrationRva = 0x07121D0; + constexpr uintptr_t ConsoleVariablePointerRva = 0x601AC28; + constexpr uintptr_t ConsoleVariableDataPointerRva = 0x601AC30; + constexpr uintptr_t FloatConsoleVariableVTableRva = 0x4A04F88; + constexpr uintptr_t ConsoleVariableSetRva = 0x1C5A9E0; + constexpr uintptr_t ConsoleVariableSetVTableOffset = 0x80; + constexpr uintptr_t GameEngineGetMaxTickRateVTableOffset = 0x2B0; + constexpr uintptr_t GameEngineGetMaxTickRateRva = 0x3722590; + constexpr uintptr_t EngineFrameRateFlagsOffset = 0x7B8; + constexpr uint8_t UseFixedFrameRateMask = 1u << 6; + constexpr uint32_t SetByCode = 0x08000000; + constexpr uint32_t ExpectedExecutableTimestamp = 0x5F9A37D4; + constexpr uint32_t ExpectedExecutableImageSize = 0x066CB000; + constexpr uint64_t BootstrapMinimumMilliseconds = 3000; + constexpr uint64_t BootstrapMaximumMilliseconds = 5000; + + constexpr uint8_t ExpectedRegistrationSignature[] = { + 0x48, 0x83, 0xEC, 0x38, 0x48, 0x8B, 0x0D, 0x25, + 0x83, 0x6E, 0x05, 0x48, 0x85, 0xC9, 0x75, 0x0C + }; + constexpr uint8_t ExpectedSetterSignature[] = { + 0x48, 0x89, 0x6C, 0x24, 0x10, 0x48, 0x89, 0x74, + 0x24, 0x18, 0x57, 0x48, 0x83, 0xEC, 0x20, 0x48, + 0x8B, 0xEA, 0x41, 0x8B, 0xF0, 0x41, 0x8B, 0xD0, + 0x48, 0x8B, 0xF9, 0xE8, 0x30, 0xF1, 0xFE, 0xFF + }; + + enum class State { + Pending, + AwaitingSecondReadback, + Verified, + Failed + }; + + struct Status { + State state = State::Pending; + bool signatureValid = false; + bool resolved = false; + bool applied = false; + double observed = 0.0; + std::string error; + }; + + static Status Current{}; + static bool IsReadableMemory(const void* Address, size_t Size) { + if (!Address || Size == 0) + return false; + + MEMORY_BASIC_INFORMATION Information{}; + if (VirtualQuery(Address, &Information, sizeof(Information)) != sizeof(Information) || + Information.State != MEM_COMMIT || (Information.Protect & PAGE_GUARD) != 0 || + (Information.Protect & 0xFF) == PAGE_NOACCESS) { + return false; } + + const uintptr_t Start = reinterpret_cast(Address); + const uintptr_t RegionEnd = reinterpret_cast(Information.BaseAddress) + Information.RegionSize; + return Start <= RegionEnd && Size <= RegionEnd - Start; } - if (Globals::Listening) { - Networking::TickNetworking(); + template + static bool MatchesSignature(uintptr_t Address, const uint8_t (&Expected)[Size]) { + const auto* Bytes = reinterpret_cast(Address); + return IsReadableMemory(Bytes, Size) && std::equal(Expected, Expected + Size, Bytes); } - if (Globals::DoListen) { - Globals::DoListen = false; - Networking::Listen(UEngine::GetEngine(), Globals::Port); + static double ReadObservedMaxFps() { + return static_cast(UKismetSystemLibrary::GetConsoleVariableFloatValue(FString(L"t.MaxFPS"))); + } - Globals::Listening = true; + static double ReadCachedMaxFps() { + float** DataStorage = reinterpret_cast(Globals::BaseAddress + ConsoleVariableDataPointerRva); + if (!IsReadableMemory(DataStorage, sizeof(*DataStorage)) || !*DataStorage || + !IsReadableMemory(*DataStorage, sizeof(**DataStorage))) { + return 0.0; + } + return static_cast(**DataStorage); } - if (Globals::Listening && Networking::NetDriver) { - bool HasConnection = false; + static double ReadVirtualMaxFps(UGameEngine* GameEngine, float DeltaTime) { + if (!GameEngine || !IsReadableMemory(GameEngine, sizeof(void*))) + return 0.0; - for (UNetConnection* Connection : Networking::NetDriver->ClientConnections) { - if (!Connection->OwningActor || *(uint32_t*)((uintptr_t)Connection + 0x134) != 3) - continue; + const uintptr_t VTable = *reinterpret_cast(GameEngine); + if (!IsReadableMemory(reinterpret_cast( + VTable + GameEngineGetMaxTickRateVTableOffset), sizeof(uintptr_t))) { + return 0.0; + } - HasConnection = true; + const uintptr_t Function = *reinterpret_cast( + VTable + GameEngineGetMaxTickRateVTableOffset); + // UArchonGameEngine may override or thunk this virtual. The previous + // exact-address check rejected the live slot and kept pacing correction + // disabled. The executable build is already signature checked, so + // accept only executable code inside this exact image. + if (Function < Globals::BaseAddress || + Function >= Globals::BaseAddress + ExpectedExecutableImageSize) { + return 0.0; } - if (EnableWatchdog) { - if (!HasConnection) { - TotalNoPlayersTime += DeltaTime; + MEMORY_BASIC_INFORMATION Information{}; + if (VirtualQuery(reinterpret_cast(Function), &Information, + sizeof(Information)) != sizeof(Information) || + Information.State != MEM_COMMIT || (Information.Protect & PAGE_GUARD) != 0) { + return 0.0; + } + const DWORD Protection = Information.Protect & 0xFF; + if (Protection != PAGE_EXECUTE && Protection != PAGE_EXECUTE_READ && + Protection != PAGE_EXECUTE_READWRITE && Protection != PAGE_EXECUTE_WRITECOPY) { + return 0.0; + } - if (TotalNoPlayersTime >= 50.0f) { - exit(0); - } + using GetMaxTickRateFn = float(*)(UGameEngine*, float, bool); + return static_cast(reinterpret_cast(Function)( + // Match the native frame-pacer call. Passing false asks a different + // policy question and can hide the exact rate Unreal is enforcing. + GameEngine, DeltaTime, true)); + } + + static bool UsesFixedFrameRate(UGameEngine* GameEngine) { + if (!GameEngine) + return false; + const auto* Flags = reinterpret_cast( + reinterpret_cast(GameEngine) + EngineFrameRateFlagsOffset); + return IsReadableMemory(Flags, sizeof(*Flags)) && + (*Flags & UseFixedFrameRateMask) != 0; + } + + static void Fail(const std::string& Reason) { + Current.state = State::Failed; + Current.error = Reason; + OutputDebugStringA(("[Undaunted] ERROR: " + Reason + "\n").c_str()); + } + + static bool Tick() { + if (Current.state == State::Verified) + return true; + if (Current.state == State::Failed) + return false; + + if (Current.state == State::AwaitingSecondReadback) { + Current.observed = ReadObservedMaxFps(); + if (std::abs(Current.observed - MaximumFps) > 0.5) { + Fail("t.MaxFPS changed before the second gameserver cap readback"); + return false; } + Current.state = State::Verified; + return true; + } + + const uintptr_t BaseAddress = Globals::BaseAddress; + const auto* DosHeader = reinterpret_cast(BaseAddress); + if (!IsReadableMemory(DosHeader, sizeof(*DosHeader)) || DosHeader->e_magic != IMAGE_DOS_SIGNATURE || + DosHeader->e_lfanew <= 0 || DosHeader->e_lfanew > 0x1000) { + Fail("gameserver executable DOS header validation failed"); + return false; + } + const auto* NtHeaders = reinterpret_cast(BaseAddress + DosHeader->e_lfanew); + if (!IsReadableMemory(NtHeaders, sizeof(*NtHeaders)) || NtHeaders->Signature != IMAGE_NT_SIGNATURE || + NtHeaders->FileHeader.TimeDateStamp != ExpectedExecutableTimestamp || + NtHeaders->OptionalHeader.SizeOfImage != ExpectedExecutableImageSize) { + Fail("gameserver executable build validation failed"); + return false; + } + const uintptr_t RegistrationAddress = BaseAddress + RegistrationRva; + const uintptr_t SetterAddress = BaseAddress + ConsoleVariableSetRva; + Current.signatureValid = MatchesSignature(RegistrationAddress, ExpectedRegistrationSignature) && + MatchesSignature(SetterAddress, ExpectedSetterSignature); + if (!Current.signatureValid) { + Fail("native t.MaxFPS signature verification failed"); + return false; + } + + void** ConsoleVariableStorage = reinterpret_cast(BaseAddress + ConsoleVariablePointerRva); + void** DataStorage = reinterpret_cast(BaseAddress + ConsoleVariableDataPointerRva); + if (!IsReadableMemory(ConsoleVariableStorage, sizeof(void*)) || + !IsReadableMemory(DataStorage, sizeof(void*)) || !*ConsoleVariableStorage || !*DataStorage) { + Fail("native t.MaxFPS storage was not initialized"); + return false; + } + + void* ConsoleVariable = *ConsoleVariableStorage; + if (!IsReadableMemory(ConsoleVariable, sizeof(void*))) { + Fail("native t.MaxFPS console variable was unreadable"); + return false; + } + + const uintptr_t VTable = *reinterpret_cast(ConsoleVariable); + if (VTable != BaseAddress + FloatConsoleVariableVTableRva || + !IsReadableMemory(reinterpret_cast(VTable + ConsoleVariableSetVTableOffset), sizeof(void*))) { + Fail("native t.MaxFPS console variable vtable did not match this build"); + return false; + } + + const uintptr_t ResolvedSetter = *reinterpret_cast(VTable + ConsoleVariableSetVTableOffset); + if (ResolvedSetter != SetterAddress) { + Fail("native t.MaxFPS setter did not match this build"); + return false; + } + + Current.resolved = true; + using SetConsoleVariableFn = void(*)(void*, const wchar_t*, uint32_t); + reinterpret_cast(ResolvedSetter)(ConsoleVariable, L"30", SetByCode); + Current.applied = true; + Current.observed = ReadObservedMaxFps(); + if (std::abs(Current.observed - MaximumFps) > 0.5) { + Fail("native t.MaxFPS setter did not produce a 30 FPS readback"); + return false; + } + + Current.state = State::AwaitingSecondReadback; + return false; + } + + static bool Failed() { + return Current.state == State::Failed; + } + + static bool Verified() { + return Current.state == State::Verified; + } + + static const Status& GetStatus() { + return Current; + } +} + +static bool PreReadyOptimizationCompleted = false; +static bool StartupRejected = false; +static std::chrono::steady_clock::time_point ListenStartedAt{}; +static AssetOptimization::Metrics OptimizationMetrics{}; +static uint64_t PreReadyNetworkingPasses = 0; + +static double ReadObservedMaxFps() { + return DedicatedServerFrameCap::ReadObservedMaxFps(); +} + +static void PopulateStartupMetrics(AssetOptimization::Metrics& Metrics) { + const DedicatedServerFrameCap::Status& Cap = DedicatedServerFrameCap::GetStatus(); + Metrics.configuredMaxFps = static_cast(DedicatedServerFrameCap::MaximumFps); + Metrics.observedMaxFps = ReadObservedMaxFps(); + Metrics.capSignatureValid = Cap.signatureValid; + Metrics.capResolved = Cap.resolved; + Metrics.capApplied = Cap.applied; + Metrics.capVerified = DedicatedServerFrameCap::Verified() && + std::abs(Metrics.observedMaxFps - DedicatedServerFrameCap::MaximumFps) <= 0.5; + Metrics.preReadyNetworkingPasses = PreReadyNetworkingPasses; + Metrics.profilingEnabled = Globals::ProfilingEnabled; + Metrics.bootstrapMinimumMilliseconds = DedicatedServerFrameCap::BootstrapMinimumMilliseconds; + Metrics.bootstrapMaximumMilliseconds = DedicatedServerFrameCap::BootstrapMaximumMilliseconds; + Metrics.considerCacheMaxAgeMilliseconds = Globals::ConsiderCacheMaxAgeMilliseconds; + if (Networking::NetDriver) { + Metrics.netServerMaxTickRate = static_cast((std::max)(0, Networking::NetDriver->NetServerMaxTickRate)); + Metrics.maxNetTickRate = static_cast((std::max)(0, Networking::NetDriver->MaxNetTickRate)); + } +} + +static void RejectServerStartup(AssetOptimization::Metrics Metrics, const std::string& Reason) { + if (StartupRejected) + return; + + StartupRejected = true; + PreReadyOptimizationCompleted = true; + Globals::DoListen = false; + Metrics.failed = true; + if (!Reason.empty()) { + if (!Metrics.error.empty()) Metrics.error += "; "; + Metrics.error += Reason; + } + PopulateStartupMetrics(Metrics); + Metrics.capVerified = false; + OptimizationMetrics = std::move(Metrics); + ServerPerformance::Start(OptimizationMetrics); + SendReadyCallback(OptimizationMetrics, false, Reason); +} + +static void FinishPreReadyOptimization(AssetOptimization::Metrics Metrics) { + if (PreReadyOptimizationCompleted) + return; + + PopulateStartupMetrics(Metrics); + + if (!Metrics.capVerified) { + RejectServerStartup(std::move(Metrics), "gameserver t.MaxFPS was not verified at 30 before readiness"); + return; + } + if (PreReadyNetworkingPasses == 0) { + if (!Metrics.error.empty()) Metrics.error += "; "; + Metrics.error += "readiness reached without a pre-ready networking pass"; + OutputDebugStringA("[Undaunted] ERROR: readiness reached without a pre-ready networking pass\n"); + } + + OptimizationMetrics = std::move(Metrics); + PreReadyOptimizationCompleted = true; + ServerPerformance::Start(OptimizationMetrics); + if (SendReadyCallback(OptimizationMetrics)) { + if (!StartLifecycleHeartbeat()) { + OutputDebugStringA( + "[Undaunted] WARNING: lifecycle heartbeat did not start; idle shutdown will remain disabled\n"); + } + } +} + +static void TickPreReadyOptimization() { + if (PreReadyOptimizationCompleted || PreReadyNetworkingPasses == 0) + return; + + if (Globals::AssetStrippingMode == AssetOptimization::Mode::Off) { + FinishPreReadyOptimization(AssetOptimization::MakeSkipped( + Globals::AssetStrippingMode, AssetOptimization::SafetyGate::NotRequired, "")); + return; + } + + const bool HasTrackedConnections = !Networking::GetLiveConnections().empty(); + const bool HasRawConnections = Networking::NetDriver && Networking::NetDriver->ClientConnections.Num() > 0; + if (HasTrackedConnections || HasRawConnections) { + FinishPreReadyOptimization(AssetOptimization::MakeSkipped( + Globals::AssetStrippingMode, AssetOptimization::SafetyGate::ConnectionsPresent, + "asset cleanup skipped because a connection appeared before readiness")); + return; + } + + if (AssetOptimization::IsInitialLoadComplete()) { + FinishPreReadyOptimization(AssetOptimization::Run( + Globals::AssetStrippingMode, + Globals::StripInactiveMapPackages, + Globals::AssetStrippingLogDetails, + WideToUtf8(Globals::MapPathValue))); + return; + } + + const double WaitedSeconds = std::chrono::duration( + std::chrono::steady_clock::now() - ListenStartedAt).count(); + if (WaitedSeconds >= Globals::AssetGcWaitSeconds) { + FinishPreReadyOptimization(AssetOptimization::MakeSkipped( + Globals::AssetStrippingMode, AssetOptimization::SafetyGate::Timeout, + "initial-load safety flag did not clear before the cleanup deadline")); + } +} + +void GameEngineTickHook(UGameEngine* GameEngine, float DeltaTime, char CanRender) { + const auto OriginalTickStarted = std::chrono::steady_clock::now(); + reinterpret_cast(OrigGameEngineTick)(GameEngine, DeltaTime, CanRender); + const double OriginalTickMilliseconds = std::chrono::duration( + std::chrono::steady_clock::now() - OriginalTickStarted).count(); + + SDK::UWorld* World = SDK::UWorld::GetWorld(); + + if (Globals::AmServer && !DedicatedServerFrameCap::Tick()) { + if (DedicatedServerFrameCap::Failed()) { + AssetOptimization::Metrics Metrics = AssetOptimization::MakeSkipped( + Globals::AssetStrippingMode, AssetOptimization::SafetyGate::Failed, + ""); + RejectServerStartup(std::move(Metrics), DedicatedServerFrameCap::GetStatus().error); } + return; + } + + if (StartupRejected) + return; + + // Do not feed the evidence window the guaranteed pre-cap first frame. The + // cap path above has now passed both native readbacks, so every recorded + // sample represents the invariant the pacing correction is evaluating. + if (Globals::AmServer) { + const DedicatedServerFrameCap::Status& CapStatus = DedicatedServerFrameCap::GetStatus(); + const double CachedMaxFps = DedicatedServerFrameCap::ReadCachedMaxFps(); + ServerPacing::ObserveFrame({ + CapStatus.observed, + CachedMaxFps, + CapStatus.resolved && CachedMaxFps > 0.0 + ? DedicatedServerFrameCap::ReadVirtualMaxFps(GameEngine, DeltaTime) + : 0.0, + OriginalTickMilliseconds, + static_cast(DeltaTime) * 1000.0, + DedicatedServerFrameCap::UsesFixedFrameRate(GameEngine) + }); + } + + if (Globals::DoListen && ShouldAttemptListen(DeltaTime)) { + Globals::DoListen = false; + Globals::Listening = Networking::Listen(UEngine::GetEngine(), World, Globals::Port); - for (UNetConnection* Conn : Networking::NetDriver->ClientConnections) { - if (Conn->PlayerController && Conn->PlayerController->Pawn && Conn->PlayerController->Pawn->IsA(ABP_PlayerCharacter_C::StaticClass())) { - ((ABP_PlayerCharacter_C*)Conn->PlayerController->Pawn)->TickStamina(ECityExecFilter::Both, ERemoteExecFilter::All); + if (Globals::Listening) + ListenStartedAt = std::chrono::steady_clock::now(); + } + + if (Globals::Listening && Networking::NetDriver) { + const auto NetworkingStarted = std::chrono::steady_clock::now(); + Networking::TickNetworking(World); + const double NetworkingMilliseconds = std::chrono::duration( + std::chrono::steady_clock::now() - NetworkingStarted).count(); + if (!PreReadyOptimizationCompleted) + ++PreReadyNetworkingPasses; + + TickPreReadyOptimization(); + if (!PreReadyOptimizationCompleted) + return; + + ServerPerformance::RecordEngineTick(DeltaTime, OriginalTickMilliseconds); + ServerPerformance::RecordNetworking(NetworkingMilliseconds); + ServerPerformance::Tick(); + + const auto& LiveConnections = Networking::GetLiveConnections(); + ObserveLifecycleConnections( + static_cast(LiveConnections.size()), + Networking::NetDriver != nullptr + ? static_cast((std::max)(0, + Networking::NetDriver->ClientConnections.Num())) + : 0, + World); + if (!LiveConnections.empty()) + TotalNoPlayersTime = 0.0f; + + if (ShouldTickStaminaForServer()) { + for (UNetConnection* Conn : LiveConnections) { + if (Conn->PlayerController && Conn->PlayerController->Pawn) { + APawn* Pawn = Conn->PlayerController->Pawn; + if (!Pawn->IsA(ABP_PlayerCharacter_C::StaticClass())) + continue; + static_cast(Pawn)->TickStamina(ECityExecFilter::Both, ERemoteExecFilter::All); + } } } } @@ -385,8 +1089,8 @@ void GameEngineTickHook(UGameEngine* GameEngine, float DeltaTime, char CanRender void* OrigFixupNetworkNotify = nullptr; void* FixupNetworkNotifyHook(void* a1) { - if(UWorld::GetWorld()) - *(void**)((uintptr_t)a1 + 0x208) = &UWorld::GetWorld()->NetworkNotify; + if(SDK::UWorld::GetWorld()) + *(void**)((uintptr_t)a1 + 0x208) = &SDK::UWorld::GetWorld()->NetworkNotify; return reinterpret_cast(OrigFixupNetworkNotify)(a1); } @@ -415,10 +1119,6 @@ enum EFunctionCallspace : uint32_t void* OrigGetActorCallspace = nullptr; EFunctionCallspace GetActorCallspace(AActor* Actor, UFunction* Function, void* Stack) { - if (Function->GetFullName().contains("Ammo")) { - std::cout << Actor->GetFullName() << " - " << Function->GetFullName() << std::endl; - } - return reinterpret_cast(OrigGetActorCallspace)(Actor, Function, Stack); } @@ -435,7 +1135,7 @@ bool HasFinishedLoadingHook(UObject* a1) { if (!Ret) { if (Globals::EnableLogging) - std::cout << "[FORCEREADY] " << a1->GetFullName() << std::endl; + std::cout << "[FORCEREADY] " << a1->GetFullName() << std::endl; return true; } @@ -504,9 +1204,6 @@ APlayerStart* GetStartSpotHook(void* a1, void* a2, void* a3) { } bool ServerTryActivateAbilityInternal(UAbilitySystemComponent* Component, FGameplayAbilitySpecHandle& AbilityHandle, bool InputPressed, FPredictionKey& PredictionKey, FGameplayEventData* TriggerEventData) { - if (Globals::EnableLogging) - std::cout << "Activated ability!" << std::endl; - if(InputPressed) Component->ServerSetInputPressed(AbilityHandle); @@ -528,120 +1225,243 @@ bool MakeDoDamageHook(void* a1, void* a2, void* a3) { return true; } -#include - -void* OrigProcessEventClient = nullptr; - -void ProcessEventClientHook(UObject* Object, UFunction* Function, void* Parms) { - if (GetAsyncKeyState(VK_F7)) { - for (int i = 0; i < SDK::UObject::GObjects->Num(); i++) - { - SDK::UObject* Obj = SDK::UObject::GObjects->GetByIndex(i); - - if (!Obj) - continue; - - if (Obj->IsA(SDK::UArenaMapHuntsFeature::StaticClass())) - { - UArenaMapHuntsFeature* Quest = (UArenaMapHuntsFeature*)Obj; - - std::cout << Quest->bEnabled << std::endl; - } - } - - while (GetAsyncKeyState(VK_F7)) { - - } - } - - reinterpret_cast(OrigProcessEventClient)(Object, Function, Parms); -} - static int NumTimesOnAirshipUpdated = 0; bool DidDoTravelReset = false; void* OrigProcessEvent = nullptr; +struct PendingInterruptDamageContext { + PendingInterruptDamageContext* Previous = nullptr; + int32_t TargetIndex = -1; + int32_t TargetSerial = 0; + bool InterruptObserved = false; +}; + +thread_local PendingInterruptDamageContext* ActiveInterruptDamageContext = nullptr; + void ProcessEventHook(UObject* Object, UFunction* Function, void* Parms) { static UFunction* ServerTryActivateAbilityWithEventData = nullptr; static UFunction* ServerTryActivateAbility = nullptr; - static UFunction* OnAirshipUpdated = nullptr; - static UFunction* OnPostMitDealtAnyDamage = nullptr; - - if (Function == ServerTryActivateAbilityWithEventData || (!ServerTryActivateAbilityWithEventData && Function->GetFullName().contains("ServerTryActivateAbilityWithEventData"))) { - ServerTryActivateAbilityWithEventData = Function; - - Params::AbilitySystemComponent_ServerTryActivateAbilityWithEventData* ActivateAbilityParams = (Params::AbilitySystemComponent_ServerTryActivateAbilityWithEventData*)Parms; + static UFunction* ServerNotifyLoadedWorld = nullptr; + static UFunction* ServerAcknowledgePossession = nullptr; + static UFunction* ServerTryDoDamage = nullptr; + static UFunction* ClientOnBehemothInterrupted = nullptr; + static bool AttemptedInterruptClientFunctionResolution = false; + + const auto CallOriginal = [&]() { + reinterpret_cast(OrigProcessEvent)(Object, Function, Parms); + }; - ServerTryActivateAbilityInternal((UAbilitySystemComponent*)Object, ActivateAbilityParams->AbilityToActivate, ActivateAbilityParams->InputPressed, ActivateAbilityParams->PredictionKey, &ActivateAbilityParams->TriggerEventData); + if (!Object || !Function) { + ServerPerformance::RecordProcessEvent(false, true); + CallOriginal(); + return; } - else if (Function == ServerTryActivateAbility || (!ServerTryActivateAbility && Function->GetFullName().contains("ServerTryActivateAbility"))) { - ServerTryActivateAbility = Function; - Params::AbilitySystemComponent_ServerTryActivateAbility* ActivateAbilityParams = (Params::AbilitySystemComponent_ServerTryActivateAbility*)Parms; - - ServerTryActivateAbilityInternal((UAbilitySystemComponent*)Object, ActivateAbilityParams->AbilityToActivate, ActivateAbilityParams->InputPressed, ActivateAbilityParams->PredictionKey, nullptr); + // Gameplay interception only concerns server RPCs. The one passive client + // notification below uses an exact UFunction pointer resolved once from its + // already-loaded class, avoiding name work on the global ProcessEvent path. + constexpr uint32_t NetServerFunctionFlag = static_cast(EFunctionFlags::NetServer); + const bool IsNetServerFunction = (Function->FunctionFlags & NetServerFunctionFlag) != 0; + if (!IsNetServerFunction) { + constexpr uint32_t NetClientFunctionFlag = static_cast(EFunctionFlags::NetClient); + if (Globals::AmServer && + (Function->FunctionFlags & NetClientFunctionFlag) != 0 && + Object->IsA(Aplayer_state_bp_C::StaticClass())) { + if (!AttemptedInterruptClientFunctionResolution) { + AttemptedInterruptClientFunctionResolution = true; + ClientOnBehemothInterrupted = Aplayer_state_bp_C::StaticClass()->GetFunction( + "player_state_bp_C", "ClientOnBehemothInterrupted"); + } + if (Function == ClientOnBehemothInterrupted) { + Networking::NotifyBehemothInterruptClientRpc(); + if (ActiveInterruptDamageContext) + ActiveInterruptDamageContext->InterruptObserved = true; + } + } + ServerPerformance::RecordProcessEvent(false, true); + CallOriginal(); + return; } - /* - if (Function == OnPostMitDealtAnyDamage || (!OnPostMitDealtAnyDamage && Function->GetFullName().contains("OnPostMitDealtAnyDamage"))) { - OnPostMitDealtAnyDamage = Function; - - //OnPostMitDamage - Params::lantern_equipped_ab_C_OnPostMitDealtAnyDamage* LanternParms = (Params::lantern_equipped_ab_C_OnPostMitDealtAnyDamage*)Parms; - - UGameplayAbility* Obj = (UGameplayAbility*)Object; + const bool IsAbilityRpc = Object->IsA(UAbilitySystemComponent::StaticClass()); + const bool IsPlayerControllerRpc = Globals::AmServer && Object->IsA(APlayerController::StaticClass()); + const bool IsDamageRpc = Globals::AmServer && Object->IsA(UDamageComponent::StaticClass()); + if (!IsAbilityRpc && !IsPlayerControllerRpc && !IsDamageRpc) { + ServerPerformance::RecordProcessEvent(true, true); + CallOriginal(); + return; + } - ABP_PlayerCharacter_C* PlayerCharacter = (ABP_PlayerCharacter_C*)Obj->GetAvatarActorFromActorInfo(); + std::string FunctionName; + auto MatchesFunction = [&](UFunction*& CachedFunction, const char* ExpectedName) { + if (Function == CachedFunction) { + ServerPerformance::RecordProcessEventFunctionMatch(); + return true; + } + if (CachedFunction) + return false; + if (FunctionName.empty()) { + FunctionName = Function->GetName(); + ServerPerformance::RecordProcessEventNameLookup(true); + } + if (FunctionName != ExpectedName) + return false; + CachedFunction = Function; + ServerPerformance::RecordProcessEventFunctionMatch(); + return true; + }; - if (PlayerCharacter) { - AArchonWeapon* Weapon = PlayerCharacter->GetWeapon(); + if (IsAbilityRpc && MatchesFunction(ServerTryActivateAbilityWithEventData, + "ServerTryActivateAbilityWithEventData")) { + Params::AbilitySystemComponent_ServerTryActivateAbilityWithEventData* ActivateAbilityParams = (Params::AbilitySystemComponent_ServerTryActivateAbilityWithEventData*)Parms; - if (Weapon && Weapon->IsA(ABP_EB_Weapon_C::StaticClass())) { - UFunction* PostMitFunc = Weapon->Class->GetFunction("BP_EB_Weapon_C", "OnPostMitDamage"); + if (ActivateAbilityParams && ServerTryActivateAbilityInternal((UAbilitySystemComponent*)Object, ActivateAbilityParams->AbilityToActivate, ActivateAbilityParams->InputPressed, ActivateAbilityParams->PredictionKey, &ActivateAbilityParams->TriggerEventData)) { + ServerPerformance::RecordAbilityRpc(true, false); + ServerPerformance::RecordProcessEvent(true, false); + return; + } + ServerPerformance::RecordAbilityRpc(false, true); + } + else if (IsAbilityRpc && MatchesFunction(ServerTryActivateAbility, "ServerTryActivateAbility")) { + Params::AbilitySystemComponent_ServerTryActivateAbility* ActivateAbilityParams = (Params::AbilitySystemComponent_ServerTryActivateAbility*)Parms; - if (PostMitFunc) { - //std::cout << "Proc'ing Post-mit callback on compat weapon!" << std::endl; + if (ActivateAbilityParams && ServerTryActivateAbilityInternal((UAbilitySystemComponent*)Object, ActivateAbilityParams->AbilityToActivate, ActivateAbilityParams->InputPressed, ActivateAbilityParams->PredictionKey, nullptr)) { + ServerPerformance::RecordAbilityRpc(true, false); + ServerPerformance::RecordProcessEvent(true, false); + return; + } + ServerPerformance::RecordAbilityRpc(false, true); + } - void* NewLanternParms = malloc(sizeof(Params::lantern_equipped_ab_C_OnPostMitDealtAnyDamage)); + const bool IsLoadedWorldNotification = IsPlayerControllerRpc && MatchesFunction( + ServerNotifyLoadedWorld, "ServerNotifyLoadedWorld"); + const bool IsPossessionAcknowledgement = IsPlayerControllerRpc && MatchesFunction( + ServerAcknowledgePossession, "ServerAcknowledgePossession"); + const bool IsDamageRpcMatch = IsDamageRpc && MatchesFunction( + ServerTryDoDamage, "ServerTryDoDamage"); + if (IsDamageRpcMatch) + Networking::NotifyDamageRpcObserved(); + const bool IsDamageNotification = IsDamageRpcMatch && Parms; + int32_t DamageTargetIndex = -1; + int32_t DamageTargetSerial = 0; + if (IsDamageNotification) { + const auto* DamageParams = static_cast(Parms); + // Copy the weak identity while the incoming parameter block is valid. + // Networking deliberately resolves index+serial itself after native + // validation/execution; generated FWeakObjectPtr::Get ignores serials. + DamageTargetIndex = DamageParams->Payload.Hit.Actor.ObjectIndex; + DamageTargetSerial = DamageParams->Payload.Hit.Actor.ObjectSerialNumber; + } - memcpy_s(NewLanternParms, sizeof(Params::lantern_equipped_ab_C_OnPostMitDealtAnyDamage), LanternParms, sizeof(Params::lantern_equipped_ab_C_OnPostMitDealtAnyDamage)); + ServerPerformance::RecordProcessEvent(true, true); + PendingInterruptDamageContext DamageContext{ + ActiveInterruptDamageContext, + DamageTargetIndex, + DamageTargetSerial, + false + }; + if (IsDamageNotification) + ActiveInterruptDamageContext = &DamageContext; + CallOriginal(); + if (IsDamageNotification) { + ActiveInterruptDamageContext = DamageContext.Previous; + if (DamageContext.InterruptObserved) { + Networking::NotifyUrgentDamageTarget( + DamageContext.TargetIndex, DamageContext.TargetSerial); + } + } - reinterpret_cast(OrigProcessEvent)(Weapon, PostMitFunc, NewLanternParms); + if (!IsPlayerControllerRpc) + return; - free(NewLanternParms); - } - } - } + APlayerController* PlayerController = static_cast(Object); + if (IsLoadedWorldNotification && Parms) { + const auto* LoadedWorldParams = static_cast(Parms); + Networking::NotifyClientLoadedWorld(PlayerController, LoadedWorldParams->WorldPackageName); + } + else if (IsPossessionAcknowledgement && Parms) { + const auto* PossessionParams = static_cast(Parms); + Networking::NotifyClientAcknowledgedPawn(PlayerController, PossessionParams->P); } - */ - reinterpret_cast(OrigProcessEvent)(Object, Function, Parms); +} +// TODO: Temporary workaround for CreatePhoenixAccountEndpoint and GET 127.0.0.1// +bool IsMalformedLocalEndpoint(const std::wstring& Value) { + const size_t SchemeEnd = Value.find(L"://"); + const size_t PathStart = SchemeEnd == std::wstring::npos ? 0 : Value.find(L'/', SchemeEnd + 3); + if (PathStart != std::wstring::npos && Value.compare(PathStart, 2, L"//") == 0) + return true; + return Value.find(L"/account127.0.0.1") != std::wstring::npos || + Value.find(L"/accountlocalhost") != std::wstring::npos; } void* OrigConfigCacheIniGetString = nullptr; bool ConfigCacheInitGetStringHook(void* a1, const wchar_t* Section, const wchar_t* Key, FString* Value, FString* Filename) { - EvalEndpointMap(); - - if (EndpointMap.contains(Key)) { - *Value = FString(EndpointMap.at(Key).c_str()); + if (Globals::MetagameAddress.empty()) + return reinterpret_cast(OrigConfigCacheIniGetString)(a1, Section, Key, Value, Filename); + if (WideContainsInsensitive(Section, L"StompServiceMcp") && + Key != nullptr && WideContainsInsensitive(Key, L"ServiceSessionEndpoint")) { + *Value = FString((L"ws://" + Globals::MetagameAddress + L"/ws/{accountid}").c_str()); return true; } - if (std::wstring(Section).contains(L"Mcp")) { - if (std::wstring(Key).contains(L"protocol") || std::wstring(Key).contains(L"Protocol")) { - *Value = FString(L"http"); + if (MayBeMappedEndpointKey(Key)) { + EvalEndpointMap(); + const std::wstring KeyName(Key); + const auto Endpoint = EndpointMap.find(KeyName); + + if (Endpoint != EndpointMap.end()) { + const std::wstring& MappedValue = Endpoint->second; + + if (Globals::EnableLogging) { + FString RawValue; + const bool HadRawValue = reinterpret_cast(OrigConfigCacheIniGetString)(a1, Section, Key, &RawValue, Filename); + const std::string RawUtf8 = HadRawValue ? RawValue.ToString() : std::string(); + const std::wstring RawValueWide(RawUtf8.begin(), RawUtf8.end()); + if (IsMalformedLocalEndpoint(RawValueWide) || IsMalformedLocalEndpoint(MappedValue)) { + std::wcout << L"Malformed endpoint config section=" << (Section ? Section : L"") + << L" key=" << (Key ? Key : L"") << L" raw=" << RawValueWide + << L" mapped=" << MappedValue << std::endl; + } + } + + *Value = FString(MappedValue.c_str()); + + return true; + } + } + + if (WideContainsInsensitive(Section, L"Mcp") && Key != nullptr) { + const bool IsStompSection = WideContainsInsensitive(Section, L"StompServiceMcp"); + const bool IsXmppSection = WideContainsInsensitive(Section, L"Xmpp"); + if (IsStompSection && (WideContainsInsensitive(Key, L"path") || + WideContainsInsensitive(Key, L"url") || + WideContainsInsensitive(Key, L"endpoint"))) { + *Value = FString(L"/stomp"); return true; } - - if (std::wstring(Key).contains(L"Domain") || std::wstring(Key).contains(L"RedirectUrl")) { - *Value = FString(Globals::MetagameAddress.c_str()); + + if (WideContainsInsensitive(Key, L"protocol") || WideContainsInsensitive(Key, L"scheme")) { + *Value = FString((IsStompSection || IsXmppSection) ? L"ws" : L"http"); return true; } + + if (WideContainsInsensitive(Key, L"Domain")) { + if (IsXmppSection) { + constexpr const wchar_t* XmppIdentityDomain = L"prod.ol.epicgames.com"; + *Value = FString(XmppIdentityDomain); + return true; + } + if (IsStompSection) { + *Value = FString(Globals::MetagameAddress.c_str()); + return true; + } + } } return reinterpret_cast(OrigConfigCacheIniGetString)(a1, Section, Key, Value, Filename); @@ -660,10 +1480,6 @@ bool GetEscalationSeason(UHuntCatalog* a1, FString* HuntID, FHunt_UnlockInfo* Un void* OrigGetTrackProgress = nullptr; __int64 GetTrackProgress(void* a1, FName* a2, void* a3) { - if (a2) { - std::cout << a2->ToString() << std::endl; - } - return 9999999; } @@ -676,14 +1492,20 @@ void InitClientHooks() { MH_EnableHook((void*)(Globals::BaseAddress + 0x1528000)); - MH_CreateHook((void*)(Globals::BaseAddress + 0x1D09D50), ConfigCacheInitGetStringHook, &OrigConfigCacheIniGetString); + if (!Globals::MetagameAddress.empty()) { + MH_CreateHook((void*)(Globals::BaseAddress + 0x1D09D50), ConfigCacheInitGetStringHook, &OrigConfigCacheIniGetString); - MH_EnableHook((void*)(Globals::BaseAddress + 0x1D09D50)); + MH_EnableHook((void*)(Globals::BaseAddress + 0x1D09D50)); + } MH_CreateHook((void*)(Globals::BaseAddress + 0x14F2A30), GetEscalationSeason, &OrigGetEscalationSeason); MH_EnableHook((void*)(Globals::BaseAddress + 0x14F2A30)); + MH_CreateHook((void*)(Globals::BaseAddress + 0x3307100), GameEngineTickHook, &OrigGameEngineTick); + + MH_EnableHook((void*)(Globals::BaseAddress + 0x3307100)); + // //1469E00 @@ -697,9 +1519,9 @@ void InitClientHooks() { //MH_EnableHook((void*)(Globals::BaseAddress + 0x347E110)); - //MH_CreateHook((void*)(Globals::BaseAddress + 0x1F61820), ProcessEventClientHook, &OrigProcessEventClient); - - //MH_EnableHook((void*)(Globals::BaseAddress + 0x1F61820)); + if constexpr (ClientHookConfig::kEnableNativeNameCleanup) { + NativeNameCleanup::Init(); + } // MH_CreateHook((void*)(Globals::BaseAddress + 0x3077710), GetActorCallspace, &OrigGetActorCallspace); @@ -721,6 +1543,10 @@ int NetModeHook(void* a1) { //char __fastcall UArchonStaminaComponent_TryConsume void InitServerHooks() { MH_Initialize(); + // This hook is build/signature checked and remains pass-through unless the + // native 30 FPS pacer is demonstrably losing time in its coarse Sleep call. + ServerPacing::Install(Globals::BaseAddress, Globals::AmServer); + MH_CreateHook((void*)(Globals::BaseAddress + 0x25A37C0), GetGameDefaultMap, &OrigGetDefaultMap); MH_EnableHook((void*)(Globals::BaseAddress + 0x25A37C0)); @@ -913,20 +1739,70 @@ void Init() { wchar_t** Args = CommandLineToArgvW(GetCommandLineW(), &NumArgs); if (NumArgs > 8) { - Globals::ServerAPIKey = Args[1]; + Globals::ServerAPIKeyValue = Args[1]; Globals::Port = std::stoi(std::wstring(Args[2])); - Globals::MapPath = Args[3]; - Globals::BehemothPath = Args[4]; - Globals::MatchmakerHuntId = Args[5]; - Globals::ExpectedPlayerString = Args[6]; - Globals::MyIpAndPort = Args[7]; + Globals::MapPathValue = Args[3]; + Globals::BehemothPathValue = Args[4]; + Globals::MatchmakerHuntIdValue = Args[5]; + Globals::ExpectedPlayerStringValue = Args[6]; + Globals::MyIpAndPortValue = Args[7]; + Globals::ServerAPIKey = Globals::ServerAPIKeyValue.c_str(); + Globals::MapPath = Globals::MapPathValue.c_str(); + Globals::BehemothPath = Globals::BehemothPathValue.c_str(); + Globals::MatchmakerHuntId = Globals::MatchmakerHuntIdValue.c_str(); + Globals::ExpectedPlayerString = Globals::ExpectedPlayerStringValue.c_str(); + Globals::MyIpAndPort = Globals::MyIpAndPortValue.c_str(); + if (NumArgs > 11 && Args[8][0] != L'-') { + Globals::GameserverId = Args[8]; + Globals::ReadyCallbackUrl = Args[9]; + Globals::ReadyCallbackToken = Args[10]; + } + + Globals::EnableLogging = GetNamedBooleanArgument( + Args, NumArgs, L"-undauntedConsoleLog=", false); + Globals::AssetStrippingMode = AssetOptimization::ParseMode(GetNamedArgument( + Args, NumArgs, L"-undauntedAssetStrippingMode=", L"aggressive")); + Globals::StripInactiveMapPackages = GetNamedBooleanArgument( + Args, NumArgs, L"-undauntedStripInactiveMapPackages=", true); + Globals::AssetStrippingLogDetails = GetNamedBooleanArgument( + Args, NumArgs, L"-undauntedAssetStrippingLogDetails=", false); + Globals::AssetGcWaitSeconds = GetNamedUnsignedArgument( + Args, NumArgs, L"-undauntedAssetGcWaitSeconds=", 15, 1, 15); + Globals::ProfilingEnabled = GetNamedBooleanArgument( + Args, NumArgs, L"-undauntedProfiling=", false); + Globals::ProfileIntervalSeconds = GetNamedUnsignedArgument( + Args, NumArgs, L"-undauntedProfileIntervalSeconds=", 30, 10, 3600); + Globals::ProfileOutputDirectory = GetNamedArgument( + Args, NumArgs, L"-undauntedProfileOutputDirectory=", L""); + Globals::ProfileMaximumBytes = GetNamedUnsignedArgument( + Args, NumArgs, L"-undauntedProfileMaxBytes=", + 64u * 1024u * 1024u, 1024u * 1024u, 1024u * 1024u * 1024u); + Globals::ConsiderCacheMaxAgeMilliseconds = GetNamedUnsignedArgument( + Args, NumArgs, L"-undauntedConsiderCacheMaxAgeMs=", 250, 50, 5000); + Globals::LifecycleCallbackUrl = GetNamedArgument( + Args, NumArgs, L"-undauntedLifecycleCallbackUrl=", L""); + Networking::Configure(Globals::ConsiderCacheMaxAgeMilliseconds); + ServerPerformance::Configure( + Globals::ProfilingEnabled, + Globals::ProfileIntervalSeconds, + WideToUtf8(Globals::GameserverId), + WideToUtf8(Globals::MapPathValue), + Globals::ProfileOutputDirectory, + Globals::ProfileMaximumBytes); + Networking::ConfigureLifecycleEventSink( + Globals::ProfilingEnabled && !Globals::ProfileOutputDirectory.empty() + ? ServerPerformance::RecordLifecycleEvent + : nullptr); if (Globals::Port >= 8776) { EnableWatchdog = false; - Globals::EnableLogging = true; } + + LocalFree(Args); } else { + if (Args != nullptr) + LocalFree(Args); MessageBoxA(nullptr, "INVALID GAMESERVER ARGS", "INVALID GAMESERVER ARGS", 0); exit(0); return; @@ -967,15 +1843,17 @@ void Init() { wchar_t** Args = CommandLineToArgvW(GetCommandLineW(), &NumArgs); - if (NumArgs > 2) { - Globals::MetagameAddress = Args[1]; - } + Globals::MetagameAddress = FindClientMetagameAddress(Args, NumArgs); + if (Args != nullptr) + LocalFree(Args); InitClientHooks(); } DWORD threadId; - CreateThread(nullptr, 0x1000, (LPTHREAD_START_ROUTINE)MainThread, nullptr, 0, &threadId); + HANDLE Thread = CreateThread(nullptr, 0x1000, (LPTHREAD_START_ROUTINE)MainThread, nullptr, 0, &threadId); + if (Thread != nullptr) + CloseHandle(Thread); } BOOL APIENTRY DllMain( HMODULE hModule, diff --git a/UndauntedMetagame/.env.example b/UndauntedMetagame/.env.example index e17299c..fff2869 100644 --- a/UndauntedMetagame/.env.example +++ b/UndauntedMetagame/.env.example @@ -7,4 +7,16 @@ TARGET_CHANGELIST= QOS_TARGET_URL= MATCHMAKING_MODE= DEPLOYSERVER_URL= -REGISTRATION_MODE= \ No newline at end of file +REGISTRATION_MODE= +MATCHMAKING_QUEUE_WAIT_SECONDS=3 +MATCHMAKING_STATUS_PERIOD_MILLIS=1000 +DEPLOYSERVER_TOUCH_TIMEOUT_MS=1000 +MATCHMAKING_RECONNECT_WINDOW_SECONDS=120 +MATCHMAKING_SESSION_TTL_SECONDS=300 +DEPLOYSERVER_STATUS_TIMEOUT_MS=1000 +AUTH_ACCESS_TOKEN_TTL_SECONDS=86400 +AUTH_REFRESH_TOKEN_TTL_SECONDS=2592000 +# CONST (cached timing-safe comparison) or DIRECT (indexed database lookup) +KEY_LOOKUP=CONST +# Keep revoked refresh-token records for this long; expired records are removed. +AUTH_REFRESH_TOKEN_RETENTION_SECONDS=86400 diff --git a/UndauntedMetagame/build.cjs b/UndauntedMetagame/build.cjs new file mode 100644 index 0000000..d223f2f --- /dev/null +++ b/UndauntedMetagame/build.cjs @@ -0,0 +1,18 @@ +const path = require("path"); +const { build } = require("esbuild"); + +build({ + absWorkingDir: __dirname, + entryPoints: ["./src/server.ts"], + bundle: true, + platform: "node", + target: "es2022", + format: "cjs", + packages: "external", + minify: true, + legalComments: "none", + outfile: "./dist/server.js", +}).catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/UndauntedMetagame/package-lock.json b/UndauntedMetagame/package-lock.json deleted file mode 100644 index af76834..0000000 --- a/UndauntedMetagame/package-lock.json +++ /dev/null @@ -1,3420 +0,0 @@ -{ - "name": "undauntedmetagame", - "version": "0.0.3", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "undauntedmetagame", - "version": "0.0.3", - "license": "AGPL-3.0-only", - "dependencies": { - "better-sqlite3": "^12.11.1", - "dotenv": "^17.4.2", - "drizzle-orm": "0.45.2", - "express": "5.2.1", - "jsonwebtoken": "^9.0.3", - "pino": "^10.3.1", - "pino-pretty": "^13.1.3" - }, - "devDependencies": { - "@types/express": "5.0.6", - "@types/jsonwebtoken": "^9.0.10", - "@types/node": "26.0.0", - "drizzle-kit": "0.31.10", - "tsx": "4.22.4", - "typescript": "6.0.3" - } - }, - "node_modules/@drizzle-team/brocli": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", - "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@esbuild-kit/core-utils": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", - "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", - "deprecated": "Merged into tsx: https://tsx.is", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.18.20", - "source-map-support": "^0.5.21" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "node_modules/@esbuild-kit/esm-loader": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", - "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", - "deprecated": "Merged into tsx: https://tsx.is", - "dev": true, - "license": "MIT", - "dependencies": { - "@esbuild-kit/core-utils": "^3.3.2", - "get-tsconfig": "^4.7.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@pinojs/redact": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", - "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", - "license": "MIT" - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", - "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "^2" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", - "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/jsonwebtoken": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", - "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/ms": "*", - "@types/node": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", - "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~8.3.0" - } - }, - "node_modules/@types/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/better-sqlite3": { - "version": "12.11.1", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", - "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", - "hasInstallScript": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - }, - "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^2.0.0", - "debug": "^4.4.3", - "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", - "on-finished": "^2.4.1", - "qs": "^6.15.2", - "raw-body": "^3.0.2", - "type-is": "^2.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "license": "MIT" - }, - "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/dateformat": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", - "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/dotenv": { - "version": "17.4.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", - "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/drizzle-kit": { - "version": "0.31.10", - "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz", - "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@drizzle-team/brocli": "^0.10.2", - "@esbuild-kit/esm-loader": "^2.5.5", - "esbuild": "^0.25.4", - "tsx": "^4.21.0" - }, - "bin": { - "drizzle-kit": "bin.cjs" - } - }, - "node_modules/drizzle-orm": { - "version": "0.45.2", - "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", - "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", - "license": "Apache-2.0", - "peerDependencies": { - "@aws-sdk/client-rds-data": ">=3", - "@cloudflare/workers-types": ">=4", - "@electric-sql/pglite": ">=0.2.0", - "@libsql/client": ">=0.10.0", - "@libsql/client-wasm": ">=0.10.0", - "@neondatabase/serverless": ">=0.10.0", - "@op-engineering/op-sqlite": ">=2", - "@opentelemetry/api": "^1.4.1", - "@planetscale/database": ">=1.13", - "@prisma/client": "*", - "@tidbcloud/serverless": "*", - "@types/better-sqlite3": "*", - "@types/pg": "*", - "@types/sql.js": "*", - "@upstash/redis": ">=1.34.7", - "@vercel/postgres": ">=0.8.0", - "@xata.io/client": "*", - "better-sqlite3": ">=7", - "bun-types": "*", - "expo-sqlite": ">=14.0.0", - "gel": ">=2", - "knex": "*", - "kysely": "*", - "mysql2": ">=2", - "pg": ">=8", - "postgres": ">=3", - "sql.js": ">=1", - "sqlite3": ">=5" - }, - "peerDependenciesMeta": { - "@aws-sdk/client-rds-data": { - "optional": true - }, - "@cloudflare/workers-types": { - "optional": true - }, - "@electric-sql/pglite": { - "optional": true - }, - "@libsql/client": { - "optional": true - }, - "@libsql/client-wasm": { - "optional": true - }, - "@neondatabase/serverless": { - "optional": true - }, - "@op-engineering/op-sqlite": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@prisma/client": { - "optional": true - }, - "@tidbcloud/serverless": { - "optional": true - }, - "@types/better-sqlite3": { - "optional": true - }, - "@types/pg": { - "optional": true - }, - "@types/sql.js": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@vercel/postgres": { - "optional": true - }, - "@xata.io/client": { - "optional": true - }, - "better-sqlite3": { - "optional": true - }, - "bun-types": { - "optional": true - }, - "expo-sqlite": { - "optional": true - }, - "gel": { - "optional": true - }, - "knex": { - "optional": true - }, - "kysely": { - "optional": true - }, - "mysql2": { - "optional": true - }, - "pg": { - "optional": true - }, - "postgres": { - "optional": true - }, - "prisma": { - "optional": true - }, - "sql.js": { - "optional": true - }, - "sqlite3": { - "optional": true - } - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/fast-copy": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.3.tgz", - "integrity": "sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw==", - "license": "MIT" - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "license": "MIT" - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-tsconfig": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", - "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/help-me": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", - "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/jsonwebtoken": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", - "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", - "license": "MIT", - "dependencies": { - "jws": "^4.0.1", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", - "license": "MIT" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "license": "MIT" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", - "license": "MIT" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "license": "MIT" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "license": "MIT" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "license": "MIT" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-abi": { - "version": "3.92.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", - "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pino": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", - "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", - "license": "MIT", - "dependencies": { - "@pinojs/redact": "^0.4.0", - "atomic-sleep": "^1.0.0", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^3.0.0", - "pino-std-serializers": "^7.0.0", - "process-warning": "^5.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^4.0.1", - "thread-stream": "^4.0.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", - "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", - "license": "MIT", - "dependencies": { - "split2": "^4.0.0" - } - }, - "node_modules/pino-pretty": { - "version": "13.1.3", - "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", - "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", - "license": "MIT", - "dependencies": { - "colorette": "^2.0.7", - "dateformat": "^4.6.3", - "fast-copy": "^4.0.0", - "fast-safe-stringify": "^2.1.1", - "help-me": "^5.0.0", - "joycon": "^3.1.1", - "minimist": "^1.2.6", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^3.0.0", - "pump": "^3.0.0", - "secure-json-parse": "^4.0.0", - "sonic-boom": "^4.0.1", - "strip-json-comments": "^5.0.2" - }, - "bin": { - "pino-pretty": "bin.js" - } - }, - "node_modules/pino-std-serializers": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", - "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", - "license": "MIT" - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/process-warning": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", - "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "license": "MIT" - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/secure-json-parse": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", - "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/sonic-boom": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", - "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-json-comments": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", - "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/thread-stream": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", - "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", - "license": "MIT", - "dependencies": { - "real-require": "^1.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/thread-stream/node_modules/real-require": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", - "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", - "license": "MIT" - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tsx": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", - "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.28.0" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.1", - "@esbuild/android-arm": "0.28.1", - "@esbuild/android-arm64": "0.28.1", - "@esbuild/android-x64": "0.28.1", - "@esbuild/darwin-arm64": "0.28.1", - "@esbuild/darwin-x64": "0.28.1", - "@esbuild/freebsd-arm64": "0.28.1", - "@esbuild/freebsd-x64": "0.28.1", - "@esbuild/linux-arm": "0.28.1", - "@esbuild/linux-arm64": "0.28.1", - "@esbuild/linux-ia32": "0.28.1", - "@esbuild/linux-loong64": "0.28.1", - "@esbuild/linux-mips64el": "0.28.1", - "@esbuild/linux-ppc64": "0.28.1", - "@esbuild/linux-riscv64": "0.28.1", - "@esbuild/linux-s390x": "0.28.1", - "@esbuild/linux-x64": "0.28.1", - "@esbuild/netbsd-arm64": "0.28.1", - "@esbuild/netbsd-x64": "0.28.1", - "@esbuild/openbsd-arm64": "0.28.1", - "@esbuild/openbsd-x64": "0.28.1", - "@esbuild/openharmony-arm64": "0.28.1", - "@esbuild/sunos-x64": "0.28.1", - "@esbuild/win32-arm64": "0.28.1", - "@esbuild/win32-ia32": "0.28.1", - "@esbuild/win32-x64": "0.28.1" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", - "license": "MIT", - "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - } - } -} diff --git a/UndauntedMetagame/package.json b/UndauntedMetagame/package.json index 1660a5a..87e5d1a 100644 --- a/UndauntedMetagame/package.json +++ b/UndauntedMetagame/package.json @@ -6,7 +6,17 @@ "author": "gwog :3", "scripts": { "dev": "tsx watch --env-file=.env src/server.ts", - "build": "tsc", + "build": "node build.cjs", + "test:xmpp": "tsx src/xmpp/selftest.ts", + "test:stomp": "tsx src/stomp/selftest.ts", + "test:friends": "tsx src/selftests/friends.ts", + "test:party": "tsx src/selftests/party.ts", + "test:loadout": "tsx src/selftests/loadout.ts", + "test:mastery": "tsx src/selftests/mastery.ts", + "test:store": "tsx src/selftests/store.ts", + "test:huntpass": "tsx src/selftests/huntpass.ts", + "test:inventory": "tsx src/selftests/inventory.ts", + "test": "tsx src/selftests/run.ts", "start": "node --env-file=.env dist/server.js", "db:generate": "drizzle-kit generate" }, @@ -17,13 +27,17 @@ "express": "5.2.1", "jsonwebtoken": "^9.0.3", "pino": "^10.3.1", - "pino-pretty": "^13.1.3" + "pino-pretty": "^13.1.3", + "saxes": "^6.0.0", + "ws": "^8.21.0" }, "devDependencies": { "@types/express": "5.0.6", "@types/jsonwebtoken": "^9.0.10", "@types/node": "26.0.0", + "@types/ws": "^8.18.1", "drizzle-kit": "0.31.10", + "esbuild": "0.28.1", "tsx": "4.22.4", "typescript": "6.0.3" } diff --git a/UndauntedMetagame/src/controllers/apikeys.ts b/UndauntedMetagame/src/controllers/apikeys.ts index bd2408e..53a8973 100644 --- a/UndauntedMetagame/src/controllers/apikeys.ts +++ b/UndauntedMetagame/src/controllers/apikeys.ts @@ -2,6 +2,7 @@ import {GetDb} from "../db" import { gameserverapikeys, gameserverapikeystoregister } from "../db/schema"; import crypto from "crypto"; import { logger } from "../logger"; +import { eq } from "drizzle-orm"; function HashGameserverAPIKey(GameserverAPIKeyToHash: string){ return crypto.createHash("sha256").update(GameserverAPIKeyToHash, "utf8").digest("hex"); @@ -22,23 +23,10 @@ export async function DrainAndRegisterAPIKeys(){ } export async function IsValidGameserverAPIKey(GameserverAPIKey: string){ - const AllAPIKeyHashes = await GetDb().query.gameserverapikeys.findMany(); + const IncomingGameserverAPIKeyHash = HashGameserverAPIKey(GameserverAPIKey); + const APIKey = await GetDb().query.gameserverapikeys.findFirst({ + where: eq(gameserverapikeys.keyHash, IncomingGameserverAPIKeyHash) + }); - const IncomingGameserverAPIKeyHashBuffer = Buffer.from(HashGameserverAPIKey(GameserverAPIKey), "hex"); - - let Match: boolean = false; - - for(const CmpAPIKeyHash of AllAPIKeyHashes){ - const CmpAPIKeyHashBuffer = Buffer.from(CmpAPIKeyHash.keyHash!, "hex"); - - if(CmpAPIKeyHashBuffer.length !== IncomingGameserverAPIKeyHashBuffer.length){ - continue; - } - - if(crypto.timingSafeEqual(IncomingGameserverAPIKeyHashBuffer, CmpAPIKeyHashBuffer)){ - Match = true; - } - } - - return Match; -} \ No newline at end of file + return APIKey != undefined; +} diff --git a/UndauntedMetagame/src/controllers/auth.ts b/UndauntedMetagame/src/controllers/auth.ts index bc63d6e..fba5dc3 100644 --- a/UndauntedMetagame/src/controllers/auth.ts +++ b/UndauntedMetagame/src/controllers/auth.ts @@ -1,16 +1,72 @@ import jwt, {JwtPayload} from "jsonwebtoken"; import crypto from "crypto"; +import { and, eq, isNotNull, lt, or } from "drizzle-orm"; import { GetDb } from "../db"; -import { userapikeys, userapikeystoregister } from "../db/schema"; +import { userapikeys, userapikeystoregister, userrefreshtokens } from "../db/schema"; import { logger } from "../logger"; const PRIVKEY = Buffer.from(process.env.AUTH_SIGNING_PRIVKEY_B64!, "base64").toString("utf-8"); const PUBKEY = Buffer.from(process.env.AUTH_SIGNING_PUBKEY_B64!, "base64").toString("utf-8"); +const ACCESS_TOKEN_TTL_SECONDS = Number(process.env.AUTH_ACCESS_TOKEN_TTL_SECONDS || "86400"); +const REFRESH_TOKEN_TTL_SECONDS = Number(process.env.AUTH_REFRESH_TOKEN_TTL_SECONDS || "2592000"); +const REFRESH_TOKEN_RETENTION_SECONDS = Number(process.env.AUTH_REFRESH_TOKEN_RETENTION_SECONDS || "86400"); +const KEY_LOOKUP = process.env.KEY_LOOKUP || "CONST"; +const REFRESH_TOKEN_CLEANUP_INTERVAL_MS = 60 * 60 * 1000; + +type CachedUserAPIKey = { + userId: string, + keyHash: Buffer +}; + +let UserAPIKeyCache: CachedUserAPIKey[] | undefined; +let LastRefreshTokenCleanupAt = 0; + +export type MetagameOAuthTokenResponse = { + access_token: string, + token_type: "bearer", + expires_at: string, + expires_in: number, + refresh_token: string, + refresh_expires_at: string, + refresh_expires_in: number, + account_id: string +}; export function HashUserAPIKey(UserAPIKeyToHash: string){ return crypto.createHash("sha256").update(UserAPIKeyToHash, "utf8").digest("hex"); } +async function RefreshUserAPIKeyCache(){ + const APIKeys = await GetDb().query.userapikeys.findMany(); + UserAPIKeyCache = APIKeys.map((APIKey) => ({ + userId: APIKey.userId, + keyHash: Buffer.from(APIKey.keyHash, "hex") + })).filter((APIKey) => APIKey.keyHash.length === 32); +} + +function HashRefreshToken(RefreshTokenToHash: string){ + return crypto.createHash("sha256").update(RefreshTokenToHash, "utf8").digest("hex"); +} + +function GenerateRefreshToken(){ + return `URT_${crypto.randomBytes(32).toString("hex")}`; +} + +function DateSecondsFromNow(Seconds: number){ + return new Date(Date.now() + Seconds * 1000); +} + +export async function RegisterUserAPIKeyHash(userId: string, keyHash: string){ + await GetDb().insert(userapikeys).values({ + userId: userId, + keyHash: keyHash + }); + + if(UserAPIKeyCache != undefined){ + await RefreshUserAPIKeyCache(); + } +} + export async function DrainAndRegisterUserAPIKeys(){ const APIKeysToRegister = await GetDb().query.userapikeystoregister.findMany(); @@ -23,29 +79,152 @@ export async function DrainAndRegisterUserAPIKeys(){ }); } + await RefreshUserAPIKeyCache(); logger.info(`Registered ${APIKeysToRegister.length} new User API Key(s) on boot!`); } -export async function GetUserIDForAPIKey(UserAPIKey: string){ - const AllAPIKeyHashes = await GetDb().query.userapikeys.findMany(); +function SelectDuplicateAPIKeyMatch(UserIds: string[]){ + if(UserIds.length === 0){ + return undefined; + } - const IncomingUserAPIKeyHashBuffer = Buffer.from(HashUserAPIKey(UserAPIKey), "hex"); + if(UserIds.length === 1){ + return UserIds[0]; + } - let UserId: string | undefined = undefined; + const PreferredLocalUserId = process.env.LOCAL_USER_ID ?? process.env.DEFAULT_USER_ID; + const PreferredUserId = UserIds.find((UserId) => UserId === PreferredLocalUserId); - for(const CmpAPIKeyHash of AllAPIKeyHashes){ - const CmpAPIKeyHashBuffer = Buffer.from(CmpAPIKeyHash.keyHash!, "hex"); + logger.warn(`Duplicate User API Key hash matched ${UserIds.length} users; preferring ${PreferredUserId ?? UserIds[0]}`); + return PreferredUserId ?? UserIds[0]; +} - if(CmpAPIKeyHashBuffer.length !== IncomingUserAPIKeyHashBuffer.length){ - continue; - } +async function GetUserIDForAPIKeyFromDatabase(IncomingUserAPIKeyHash: string){ + const APIKeys = await GetDb().query.userapikeys.findMany({ + where: eq(userapikeys.keyHash, IncomingUserAPIKeyHash) + }); + + return SelectDuplicateAPIKeyMatch(APIKeys.map((APIKey) => APIKey.userId)); +} - if(crypto.timingSafeEqual(IncomingUserAPIKeyHashBuffer, CmpAPIKeyHashBuffer)){ - UserId = CmpAPIKeyHash.userId; +async function GetUserIDForAPIKeyConstantTime(IncomingUserAPIKeyHash: string){ + if(UserAPIKeyCache == undefined){ + await RefreshUserAPIKeyCache(); + } + + const IncomingHash = Buffer.from(IncomingUserAPIKeyHash, "hex"); + const MatchingUserIds: string[] = []; + for(const APIKey of UserAPIKeyCache!){ + if(APIKey.keyHash.length === IncomingHash.length && + crypto.timingSafeEqual(IncomingHash, APIKey.keyHash)){ + MatchingUserIds.push(APIKey.userId); } } - return UserId; + return SelectDuplicateAPIKeyMatch(MatchingUserIds); +} + +export async function GetUserIDForAPIKey(UserAPIKey: string){ + const IncomingUserAPIKeyHash = HashUserAPIKey(UserAPIKey); + if(KEY_LOOKUP === "DIRECT"){ + return GetUserIDForAPIKeyFromDatabase(IncomingUserAPIKeyHash); + } + + return GetUserIDForAPIKeyConstantTime(IncomingUserAPIKeyHash); +} + +export async function CleanupRefreshTokens(Force = false){ + const Now = Date.now(); + if(!Force && Now - LastRefreshTokenCleanupAt < REFRESH_TOKEN_CLEANUP_INTERVAL_MS){ + return; + } + + LastRefreshTokenCleanupAt = Now; + const NowIso = new Date(Now).toISOString(); + const RevokedBeforeIso = new Date(Now - REFRESH_TOKEN_RETENTION_SECONDS * 1000).toISOString(); + await GetDb().delete(userrefreshtokens).where(or( + lt(userrefreshtokens.expiresAt, NowIso), + and( + isNotNull(userrefreshtokens.revokedAt), + lt(userrefreshtokens.revokedAt, RevokedBeforeIso) + ) + )); +} + +async function IssueRefreshTokenForUid(userId: string){ + await CleanupRefreshTokens(); + const RefreshToken = GenerateRefreshToken(); + const TokenHash = HashRefreshToken(RefreshToken); + const IssuedAt = new Date(); + const ExpiresAt = DateSecondsFromNow(REFRESH_TOKEN_TTL_SECONDS); + + await GetDb().insert(userrefreshtokens).values({ + userId: userId, + tokenHash: TokenHash, + issuedAt: IssuedAt.toISOString(), + expiresAt: ExpiresAt.toISOString() + }); + + return { + refreshToken: RefreshToken, + refreshTokenHash: TokenHash, + refreshExpiresAt: ExpiresAt + }; +} + +function BuildMetagameOAuthTokenResponseForUid(userId: string, RefreshToken: Awaited>): MetagameOAuthTokenResponse{ + const AccessExpiresAt = DateSecondsFromNow(ACCESS_TOKEN_TTL_SECONDS); + + return { + access_token: SignMetagameJWTForUid(userId), + token_type: "bearer", + expires_at: AccessExpiresAt.toISOString(), + expires_in: ACCESS_TOKEN_TTL_SECONDS, + refresh_token: RefreshToken.refreshToken, + refresh_expires_at: RefreshToken.refreshExpiresAt.toISOString(), + refresh_expires_in: REFRESH_TOKEN_TTL_SECONDS, + account_id: userId + }; +} + +export async function CreateMetagameOAuthTokenResponseForUid(userId: string): Promise{ + return BuildMetagameOAuthTokenResponseForUid(userId, await IssueRefreshTokenForUid(userId)); +} + +export async function RotateRefreshToken(RefreshToken: string){ + const IncomingRefreshTokenHash = HashRefreshToken(RefreshToken); + const ExistingRefreshToken = await GetDb().query.userrefreshtokens.findFirst({ + where: eq(userrefreshtokens.tokenHash, IncomingRefreshTokenHash) + }); + + if(ExistingRefreshToken == undefined){ + return undefined; + } + + if(ExistingRefreshToken.revokedAt != undefined){ + return undefined; + } + + if(new Date(ExistingRefreshToken.expiresAt).getTime() <= Date.now()){ + return undefined; + } + + const NewRefreshToken = await IssueRefreshTokenForUid(ExistingRefreshToken.userId); + + await GetDb().update(userrefreshtokens).set({ + revokedAt: new Date().toISOString(), + replacedByTokenHash: NewRefreshToken.refreshTokenHash + }).where(eq(userrefreshtokens.tokenHash, IncomingRefreshTokenHash)); + + return BuildMetagameOAuthTokenResponseForUid(ExistingRefreshToken.userId, NewRefreshToken); +} + +export async function RevokeRefreshToken(RefreshToken: string){ + const IncomingRefreshTokenHash = HashRefreshToken(RefreshToken); + + await GetDb().update(userrefreshtokens).set({ + revokedAt: new Date().toISOString() + }).where(eq(userrefreshtokens.tokenHash, IncomingRefreshTokenHash)); } function SignMetagameJWTForUid(userId: string){ @@ -53,7 +232,7 @@ function SignMetagameJWTForUid(userId: string){ userId: userId }, PRIVKEY, { algorithm: "RS256", - expiresIn: "24h", + expiresIn: ACCESS_TOKEN_TTL_SECONDS, issuer: "undaunted-metagame", audience: "undaunted-metagame" }); @@ -67,4 +246,4 @@ function ValidateMetagameJWTAndGetPayload(token: string){ }); } -export { SignMetagameJWTForUid, ValidateMetagameJWTAndGetPayload } \ No newline at end of file +export { SignMetagameJWTForUid, ValidateMetagameJWTAndGetPayload } diff --git a/UndauntedMetagame/src/controllers/login.ts b/UndauntedMetagame/src/controllers/login.ts index 2e86006..d19ffaf 100644 --- a/UndauntedMetagame/src/controllers/login.ts +++ b/UndauntedMetagame/src/controllers/login.ts @@ -2,8 +2,25 @@ import { eq } from "drizzle-orm"; import { GetDb } from "../db"; import { users } from "../db/schema"; +const UsernameByUserId = new Map(); + +export function RememberUsernameForUserId(userId: string, username: string){ + UsernameByUserId.set(userId, username); +} + export async function GetUsernameForUserId(userId: string){ + const CachedUsername = UsernameByUserId.get(userId); + if(CachedUsername != undefined){ + return CachedUsername; + } + let UserFromDb = await GetDb().query.users.findFirst({where: eq(users.userId, userId)}); - return UserFromDb!.name; -} \ No newline at end of file + if(UserFromDb == undefined){ + throw new Error(`No user found for userId ${userId}`); + } + + RememberUsernameForUserId(userId, UserFromDb.name); + + return UserFromDb.name; +} diff --git a/UndauntedMetagame/src/controllers/undauntedapi.ts b/UndauntedMetagame/src/controllers/undauntedapi.ts index 131e163..d643b94 100644 --- a/UndauntedMetagame/src/controllers/undauntedapi.ts +++ b/UndauntedMetagame/src/controllers/undauntedapi.ts @@ -1,8 +1,9 @@ import { randomBytes, randomUUID } from "node:crypto"; import { GetDb } from "../db"; -import { GetUserIDForAPIKey, HashUserAPIKey } from "./auth"; -import { invitecodes, userapikeys, users } from "../db/schema"; +import { GetUserIDForAPIKey, HashUserAPIKey, RegisterUserAPIKeyHash } from "./auth"; +import { invitecodes, users } from "../db/schema"; import { and, eq, gt, or, sql } from "drizzle-orm"; +import { RememberUsernameForUserId } from "./login"; const AUTH_MODE = process.env.AUTH_MODE; const NODE_ENV = process.env.NODE_ENV; @@ -28,15 +29,31 @@ type PlayerLocation = { // TODO: Track more stuff from our matchmaking telemetry EnteredTime: number } +export type PlayerMatchmakingActivity = { + CandidateId: string, + GameMode: string, + HuntId: string, + Phase: string, + StatusReason: string | undefined, + Host: string | undefined, + Port: number | undefined, + ServerId: string | undefined, + UpdatedTime: number, + ReadyTime: number | undefined +} + export type PlayerData = { UserId: string, - Map: string, + Map: string | undefined, + LastHeartbeatAt: number | undefined, HuntId: string | undefined, - EnteredHuntAt: number | undefined + EnteredHuntAt: number | undefined, + Matchmaking: PlayerMatchmakingActivity | undefined }; let PlayerActivityMap: Map = new Map(); let PlayerLocationMap: Map = new Map(); +let PlayerMatchmakingActivityMap: Map = new Map(); export function IsRegistrationMode(Value: unknown): Value is RegistrationMode { return typeof Value === "string" && VALID_REGISTRATION_MODES.includes(Value as RegistrationMode); @@ -107,10 +124,8 @@ export async function RegisterUser(Username: string){ notes: 0 }); - await GetDb().insert(userapikeys).values({ - userId: UID, - keyHash: UUKHash - }); + await RegisterUserAPIKeyHash(UID, UUKHash); + RememberUsernameForUserId(UID, Username); return UUK; } @@ -155,22 +170,44 @@ export async function UpdatePlayerLocation(UserId: string, HuntId: string){ }); } +export async function UpdatePlayerMatchmakingActivity(UserId: string, Activity: Omit){ + PlayerMatchmakingActivityMap.set(UserId, { + ...Activity, + UpdatedTime: Date.now() + }); +} + export async function GetRecentPlayerData(){ let PlayerDataToReturn: PlayerData[] = []; + const RecentPlayerIds = new Set(); PlayerActivityMap.forEach((value, key, map) => { if(Date.now() - value.LastUpdatedTime <= 90 * 1000){ // If entry is < 90s old - const PlayerLocationData: PlayerLocation | undefined = PlayerLocationMap.get(key); - - PlayerDataToReturn.push({ - UserId: key, - Map: value.Map, - HuntId: PlayerLocationData?.HuntId, - EnteredHuntAt: PlayerLocationData?.EnteredTime - }); + RecentPlayerIds.add(key); + } + }); + + PlayerMatchmakingActivityMap.forEach((value, key, map) => { + if(Date.now() - value.UpdatedTime <= 5 * 60 * 1000){ + RecentPlayerIds.add(key); } }); + RecentPlayerIds.forEach((UserId) => { + const PlayerActivityData = PlayerActivityMap.get(UserId); + const PlayerLocationData = PlayerLocationMap.get(UserId); + const MatchmakingActivity = PlayerMatchmakingActivityMap.get(UserId); + + PlayerDataToReturn.push({ + UserId: UserId, + Map: PlayerActivityData?.Map, + LastHeartbeatAt: PlayerActivityData?.LastUpdatedTime, + HuntId: PlayerLocationData?.HuntId ?? MatchmakingActivity?.HuntId, + EnteredHuntAt: PlayerLocationData?.EnteredTime, + Matchmaking: MatchmakingActivity + }); + }); + return PlayerDataToReturn; } diff --git a/UndauntedMetagame/src/db.ts b/UndauntedMetagame/src/db.ts index ae48322..89e6cd2 100644 --- a/UndauntedMetagame/src/db.ts +++ b/UndauntedMetagame/src/db.ts @@ -2,7 +2,15 @@ import { drizzle } from "drizzle-orm/better-sqlite3"; import { migrate } from "drizzle-orm/better-sqlite3/migrator" import * as schema from "./db/schema" - const db = drizzle(process.env.DB_FILENAME!, {schema}); +const Database = require("better-sqlite3"); +const sqlite = new Database(process.env.DB_FILENAME!); + +sqlite.pragma("journal_mode = WAL"); +sqlite.pragma("synchronous = NORMAL"); +sqlite.pragma("busy_timeout = 5000"); +sqlite.pragma("temp_store = MEMORY"); + +const db = drizzle(sqlite, {schema}); let didMigration = false; @@ -14,4 +22,4 @@ export function GetDb(){ } return db; -} \ No newline at end of file +} diff --git a/UndauntedMetagame/src/db/schema.ts b/UndauntedMetagame/src/db/schema.ts index 21a957d..0835f7d 100644 --- a/UndauntedMetagame/src/db/schema.ts +++ b/UndauntedMetagame/src/db/schema.ts @@ -1,4 +1,4 @@ -import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; export const users = sqliteTable("users", { userId: text("userId").notNull().primaryKey(), @@ -33,11 +33,34 @@ export const loadouts = sqliteTable("loadouts", { export const gameserverapikeys = sqliteTable("gameserverapikeys", { id: integer("id").notNull().primaryKey({autoIncrement: true}), keyHash: text("keyHash") +}, (table) => { + return { + keyHashIdx: index("gameserverapikeys_keyHash_idx").on(table.keyHash) + }; }); export const userapikeys = sqliteTable("userapikeys", { userId: text("userId").notNull().primaryKey(), keyHash: text("keyHash").notNull() +}, (table) => { + return { + keyHashIdx: index("userapikeys_keyHash_idx").on(table.keyHash) + }; +}); + +export const userrefreshtokens = sqliteTable("userrefreshtokens", { + id: integer("id").notNull().primaryKey({autoIncrement: true}), + userId: text("userId").notNull(), + tokenHash: text("tokenHash").notNull(), + issuedAt: text("issuedAt").notNull(), + expiresAt: text("expiresAt").notNull(), + revokedAt: text("revokedAt"), + replacedByTokenHash: text("replacedByTokenHash") +}, (table) => { + return { + tokenHashIdx: index("userrefreshtokens_tokenHash_idx").on(table.tokenHash), + userIdIdx: index("userrefreshtokens_userId_idx").on(table.userId) + }; }); export const userapikeystoregister = sqliteTable("userapikeystoregister", { diff --git a/UndauntedMetagame/src/drizzle/0010_moaning_vivisector.sql b/UndauntedMetagame/src/drizzle/0010_moaning_vivisector.sql new file mode 100644 index 0000000..c676b0e --- /dev/null +++ b/UndauntedMetagame/src/drizzle/0010_moaning_vivisector.sql @@ -0,0 +1,14 @@ +CREATE INDEX `gameserverapikeys_keyHash_idx` ON `gameserverapikeys` (`keyHash`);--> statement-breakpoint +CREATE INDEX `userapikeys_keyHash_idx` ON `userapikeys` (`keyHash`);--> statement-breakpoint +CREATE TABLE `userrefreshtokens` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `userId` text NOT NULL, + `tokenHash` text NOT NULL, + `issuedAt` text NOT NULL, + `expiresAt` text NOT NULL, + `revokedAt` text, + `replacedByTokenHash` text +); +--> statement-breakpoint +CREATE INDEX `userrefreshtokens_tokenHash_idx` ON `userrefreshtokens` (`tokenHash`);--> statement-breakpoint +CREATE INDEX `userrefreshtokens_userId_idx` ON `userrefreshtokens` (`userId`); diff --git a/UndauntedMetagame/src/drizzle/meta/0010_snapshot.json b/UndauntedMetagame/src/drizzle/meta/0010_snapshot.json new file mode 100644 index 0000000..2231cd9 --- /dev/null +++ b/UndauntedMetagame/src/drizzle/meta/0010_snapshot.json @@ -0,0 +1,464 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "5abc2003-eb54-4ce5-8657-532bb84abb11", + "prevId": "8f84b7e2-a042-4ae9-a7e4-ef94abe59eb2", + "tables": { + "breadcrumbs": { + "name": "breadcrumbs", + "columns": { + "characterId": { + "name": "characterId", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "breadcrumbs": { + "name": "breadcrumbs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updateVersion": { + "name": "updateVersion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "characters": { + "name": "characters", + "columns": { + "characterId": { + "name": "characterId", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdDate": { + "name": "createdDate", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lastModifiedDate": { + "name": "lastModifiedDate", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updateVersion": { + "name": "updateVersion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "encounteredcontent": { + "name": "encounteredcontent", + "columns": { + "characterId": { + "name": "characterId", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encounteredcontent": { + "name": "encounteredcontent", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "gameserverapikeys": { + "name": "gameserverapikeys", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "keyHash": { + "name": "keyHash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "gameserverapikeys_keyHash_idx": { + "name": "gameserverapikeys_keyHash_idx", + "columns": [ + "keyHash" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "gameserverapikeystoregister": { + "name": "gameserverapikeystoregister", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inventories": { + "name": "inventories", + "columns": { + "characterId": { + "name": "characterId", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "instancedItems": { + "name": "instancedItems", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stackedItems": { + "name": "stackedItems", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invitecodes": { + "name": "invitecodes", + "columns": { + "invitecode": { + "name": "invitecode", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "usesRemaining": { + "name": "usesRemaining", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "infiniteUses": { + "name": "infiniteUses", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "loadouts": { + "name": "loadouts", + "columns": { + "characterId": { + "name": "characterId", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "loadouts": { + "name": "loadouts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "persistent": { + "name": "persistent", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "userapikeys": { + "name": "userapikeys", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "keyHash": { + "name": "keyHash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "userapikeys_keyHash_idx": { + "name": "userapikeys_keyHash_idx", + "columns": [ + "keyHash" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "userapikeystoregister": { + "name": "userapikeystoregister", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "userrefreshtokens": { + "name": "userrefreshtokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tokenHash": { + "name": "tokenHash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "issuedAt": { + "name": "issuedAt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revokedAt": { + "name": "revokedAt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "replacedByTokenHash": { + "name": "replacedByTokenHash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "userrefreshtokens_tokenHash_idx": { + "name": "userrefreshtokens_tokenHash_idx", + "columns": [ + "tokenHash" + ], + "isUnique": false + }, + "userrefreshtokens_userId_idx": { + "name": "userrefreshtokens_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "isAdmin": { + "name": "isAdmin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/UndauntedMetagame/src/drizzle/meta/_journal.json b/UndauntedMetagame/src/drizzle/meta/_journal.json index aa13f4a..8db1816 100644 --- a/UndauntedMetagame/src/drizzle/meta/_journal.json +++ b/UndauntedMetagame/src/drizzle/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1783299942758, "tag": "0009_nice_dazzler", "breakpoints": true + }, + { + "idx": 10, + "version": "6", + "when": 1783774483294, + "tag": "0010_moaning_vivisector", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/UndauntedMetagame/src/routes/eos.ts b/UndauntedMetagame/src/routes/eos.ts index 81ab2fa..21b91f6 100644 --- a/UndauntedMetagame/src/routes/eos.ts +++ b/UndauntedMetagame/src/routes/eos.ts @@ -1,31 +1,60 @@ import { Router } from "express"; import { logger } from "../logger"; -import { GetUserIDForAPIKey, SignMetagameJWTForUid } from "../controllers/auth"; +import { CreateMetagameOAuthTokenResponseForUid, GetUserIDForAPIKey, RevokeRefreshToken, RotateRefreshToken, ValidateMetagameJWTAndGetPayload } from "../controllers/auth"; import { HasUndauntedMetagameAuth } from "../middleware/HasUndauntedMetagameAuth"; import { GetUsernameForUserId } from "../controllers/login"; export const eosRouter = Router(); eosRouter.post("/account/api/oauth/token", async (req, res) => { + if(req.body.grant_type === "refresh_token"){ + const RefreshToken = req.body.refresh_token; + + if(typeof RefreshToken !== "string" || RefreshToken.length === 0){ + res.status(400); + res.json({ + error: "invalid_refresh_token" + }); + return; + } + + const TokenResponse = await RotateRefreshToken(RefreshToken); + + if(TokenResponse == undefined){ + res.status(400); + res.json({ + error: "invalid_refresh_token" + }); + return; + } + + res.json({ + ...TokenResponse, + "features": ["Achievements", "AntiCheat", "Ecom", "Voice"], + "organization_id": "o-krlzxj88qrtb69fredeuaf887bl5az", + "product_id": "prod-jackal", + "sandbox_id": "jackal", + "deployment_id": "53565ba467df4edbb6f5a3d939a8b4f2", + "account_id": TokenResponse.account_id + }); + + return; + } + if(process.env.AUTH_MODE === "NONE" && process.env.NODE_ENV !== "production"){ const UserId = req.body.exchange_code; logger.info(`Logging in ${UserId}!`); - const AuthToken = SignMetagameJWTForUid(UserId); + const TokenResponse = await CreateMetagameOAuthTokenResponseForUid(UserId); res.json({ - "access_token": AuthToken, - "token_type": "bearer", - "expires_at": "2085-09-09T01:01:01.703Z", // TODO: We sign 24hr JWTs so we're unlikely to hit this, but just in case (tm) + ...TokenResponse, "features": ["Achievements", "AntiCheat", "Ecom", "Voice"], "organization_id": "o-krlzxj88qrtb69fredeuaf887bl5az", "product_id": "prod-jackal", "sandbox_id": "jackal", "deployment_id": "53565ba467df4edbb6f5a3d939a8b4f2", - "expires_in": 86400, - "refresh_token": "refresh.token.lol", // TODO: IDK if we need to support this considering our intended flow, but flagged regardless - "refresh_expires_at": "2085-09-09T01:01:01.703Z", "account_id": UserId }); } @@ -37,20 +66,15 @@ eosRouter.post("/account/api/oauth/token", async (req, res) => { if(UserId != undefined){ logger.info(`Logging in ${UserId}!`); - const AuthToken = SignMetagameJWTForUid(UserId); + const TokenResponse = await CreateMetagameOAuthTokenResponseForUid(UserId); res.json({ - "access_token": AuthToken, - "token_type": "bearer", - "expires_at": "2085-09-09T01:01:01.703Z", // TODO: We sign 24hr JWTs so we're unlikely to hit this, but just in case (tm) + ...TokenResponse, "features": ["Achievements", "AntiCheat", "Ecom", "Voice"], "organization_id": "o-krlzxj88qrtb69fredeuaf887bl5az", "product_id": "prod-jackal", "sandbox_id": "jackal", "deployment_id": "53565ba467df4edbb6f5a3d939a8b4f2", - "expires_in": 86400, - "refresh_token": "refresh.token.lol", // TODO: IDK if we need to support this considering our intended flow, but flagged regardless - "refresh_expires_at": "2085-09-09T01:01:01.703Z", "account_id": UserId }); } @@ -69,18 +93,34 @@ eosRouter.post("/account/api/oauth/token", async (req, res) => { eosRouter.get("/account/api/oauth/verify", (req, res) => { logger.info("Verifying token"); - // TODO: EOS treats this as a "just checking in" endpoint, so I've gone with a minimal stub. Validate this is correct. + const AuthHeader = req.headers.authorization; - res.json({ - "active": true, - "scope": "basic_profile friends_list presence", - "token_type": "bearer", - "expires_in": 86400, - "expires_at": "2085-09-09T01:01:01.703Z", - "account_id": "9626f441055349ce8cb7d7d5a483eaa2", - "client_id": "xyza7891lhxMVYGCON7LgnKZZ8HQGD5H", - "application_id": "fghi4567O03HROxEjwbn7kgXpBhnhWwv" - }); + if(AuthHeader == undefined || !AuthHeader.toLowerCase().startsWith("bearer ")){ + res.status(401); + res.send(); + return; + } + + try{ + const Payload = ValidateMetagameJWTAndGetPayload(AuthHeader.slice("bearer ".length)) as any; + const ExpiresAt = new Date(Payload.exp * 1000); + const ExpiresIn = Math.max(0, Math.floor((ExpiresAt.getTime() - Date.now()) / 1000)); + + res.json({ + "active": true, + "scope": "basic_profile friends_list presence", + "token_type": "bearer", + "expires_in": ExpiresIn, + "expires_at": ExpiresAt.toISOString(), + "account_id": Payload.userId, + "client_id": "xyza7891lhxMVYGCON7LgnKZZ8HQGD5H", + "application_id": "fghi4567O03HROxEjwbn7kgXpBhnhWwv" + }); + } + catch{ + res.status(401); + res.send(); + } }); eosRouter.get("/account/api/public/account/:AccId", (req, res) => { @@ -95,25 +135,27 @@ eosRouter.get("/account/api/public/account/:AccId/externalAuths", (req, res) => res.json({}); }); -eosRouter.delete("/account/api/oauth/sessions/kill", (req, res) => { +eosRouter.delete("/account/api/oauth/sessions/kill", async (req, res) => { logger.info("Session kill (stubbed)"); - // TODO: Is this needed? + if(typeof req.body?.refresh_token === "string"){ + await RevokeRefreshToken(req.body.refresh_token); + } res.json({}); }) -eosRouter.delete("/account/api/oauth/sessions/kill/:AuthToken", (req, res) => { +eosRouter.delete("/account/api/oauth/sessions/kill/:AuthToken", async (req, res) => { logger.info("Session kill (stubbed)"); - // TODO: Is this needed? + await RevokeRefreshToken(req.params.AuthToken); res.json({}); }) -eosRouter.get("/account/api/public/account", HasUndauntedMetagameAuth, (req: any, res) => { +eosRouter.get("/account/api/public/account", HasUndauntedMetagameAuth, async (req: any, res) => { const UserId = req.AuthData.userId; - const Username = GetUsernameForUserId(UserId); + const Username = await GetUsernameForUserId(UserId); logger.info(`Account info for userId ${UserId}`); @@ -137,4 +179,4 @@ eosRouter.get("/account/api/public/account", HasUndauntedMetagameAuth, (req: any "minorVerified": false, "minorStatus": "NOT_MINOR" }); -}); \ No newline at end of file +}); diff --git a/UndauntedMetagame/src/server.ts b/UndauntedMetagame/src/server.ts index 2c83822..dc26710 100644 --- a/UndauntedMetagame/src/server.ts +++ b/UndauntedMetagame/src/server.ts @@ -1,6 +1,6 @@ import { app } from "./app"; import { DrainAndRegisterAPIKeys } from "./controllers/apikeys"; -import { DrainAndRegisterUserAPIKeys } from "./controllers/auth"; +import { CleanupRefreshTokens, DrainAndRegisterUserAPIKeys } from "./controllers/auth"; import { GetDb } from "./db"; import { logger } from "./logger"; @@ -10,9 +10,10 @@ GetDb(); // This runs migrations TODO make this more explicit DrainAndRegisterAPIKeys().then(async () => { await DrainAndRegisterUserAPIKeys(); + await CleanupRefreshTokens(true); app.listen(PORT, () => { logger.info(`Undaunted Metagame on port ${PORT}`); logger.info(`Clear Skies, Slayer.`); }); -}); \ No newline at end of file +}); diff --git a/UndauntedMetagame/tsconfig.json b/UndauntedMetagame/tsconfig.json index 814fd9e..6fd64a1 100644 --- a/UndauntedMetagame/tsconfig.json +++ b/UndauntedMetagame/tsconfig.json @@ -9,7 +9,9 @@ "strict": true, "esModuleInterop": true, "skipLibCheck": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "incremental": true, + "tsBuildInfoFile": ".tsbuildinfo" }, "include": ["src/**/*.ts"] -} \ No newline at end of file +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3c50120 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "private": true, + "packageManager": "pnpm@11.13.0", + "scripts": { + "build:metagame": "pnpm --dir UndauntedMetagame run build", + "build:deploy": "pnpm --dir UndauntedDeployServer run build", + "build:internal": "powershell -NoProfile -ExecutionPolicy Bypass -File .vscode/Build-DeployReleaseDll.ps1", + "build:servers": "pnpm --parallel --aggregate-output --dir UndauntedMetagame run build --dir UndauntedDeployServer run build", + "build:all": "powershell -NoProfile -Command \"$server = Start-Process pnpm -ArgumentList 'run','build:servers' -NoNewWindow -PassThru; $internal = Start-Process pnpm -ArgumentList 'run','build:internal' -NoNewWindow -PassThru; Wait-Process -InputObject $server,$internal; if ($server.ExitCode -ne 0 -or $internal.ExitCode -ne 0) { exit 1 }\"" + }, + "devDependencies": {} +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..777ec6c --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2238 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} + + UndauntedDeployServer: + dependencies: + better-sqlite3: + specifier: ^12.11.1 + version: 12.11.1 + dotenv: + specifier: ^17.4.2 + version: 17.4.2 + drizzle-orm: + specifier: 0.45.2 + version: 0.45.2(better-sqlite3@12.11.1) + express: + specifier: 5.2.1 + version: 5.2.1 + jsonwebtoken: + specifier: ^9.0.3 + version: 9.0.3 + pino: + specifier: ^10.3.1 + version: 10.3.1 + pino-pretty: + specifier: ^13.1.3 + version: 13.1.3 + devDependencies: + '@types/express': + specifier: 5.0.6 + version: 5.0.6 + '@types/jsonwebtoken': + specifier: ^9.0.10 + version: 9.0.10 + '@types/node': + specifier: 26.0.0 + version: 26.0.0 + drizzle-kit: + specifier: 0.31.10 + version: 0.31.10 + esbuild: + specifier: 0.28.1 + version: 0.28.1 + tsx: + specifier: 4.22.4 + version: 4.22.4 + typescript: + specifier: 6.0.3 + version: 6.0.3 + + UndauntedMetagame: + dependencies: + better-sqlite3: + specifier: ^12.11.1 + version: 12.11.1 + dotenv: + specifier: ^17.4.2 + version: 17.4.2 + drizzle-orm: + specifier: 0.45.2 + version: 0.45.2(better-sqlite3@12.11.1) + express: + specifier: 5.2.1 + version: 5.2.1 + jsonwebtoken: + specifier: ^9.0.3 + version: 9.0.3 + pino: + specifier: ^10.3.1 + version: 10.3.1 + pino-pretty: + specifier: ^13.1.3 + version: 13.1.3 + saxes: + specifier: ^6.0.0 + version: 6.0.0 + ws: + specifier: ^8.21.0 + version: 8.21.0 + devDependencies: + '@types/express': + specifier: 5.0.6 + version: 5.0.6 + '@types/jsonwebtoken': + specifier: ^9.0.10 + version: 9.0.10 + '@types/node': + specifier: 26.0.0 + version: 26.0.0 + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + drizzle-kit: + specifier: 0.31.10 + version: 0.31.10 + esbuild: + specifier: 0.28.1 + version: 0.28.1 + tsx: + specifier: 4.22.4 + version: 4.22.4 + typescript: + specifier: 6.0.3 + version: 6.0.3 + +packages: + + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.hirok.io' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.hirok.io' + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/express-serve-static-core@5.1.2': + resolution: {integrity: sha512-d3KvEXBSo/lOAMc2u6fkyDHBvetBHeqD7wm/AcXfLpSOQwlmG9D/aQ0SFswVjv05p7ullQS7Mjohj6/VdbZuTg==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@26.0.0': + resolution: {integrity: sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==} + + '@types/qs@6.15.1': + resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + better-sqlite3@12.11.1: + resolution: {integrity: sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==} + engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + drizzle-kit@0.31.10: + resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==} + hasBin: true + + drizzle-orm@0.45.2: + resolution: {integrity: sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fast-copy@4.0.4: + resolution: {integrity: sha512-eVAiWVNPSEGIzDl5yPuLrx8fNMogScXvD9xp1Kzd41FjRIz2I3sSIcxsFeM5EzFfHAfobdvs8ZySffUopljvIA==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + node-abi@3.94.0: + resolution: {integrity: sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==} + engines: {node: '>=10'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-pretty@13.1.3: + resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==} + hasBin: true + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + engines: {node: '>=0.6'} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + real-require@1.0.0: + resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + + tar-fs@2.1.5: + resolution: {integrity: sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + thread-stream@4.2.0: + resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==} + engines: {node: '>=20'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tsx@4.22.4: + resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==} + engines: {node: '>=18.0.0'} + hasBin: true + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + +snapshots: + + '@drizzle-team/brocli@0.10.2': {} + + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.14.0 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@pinojs/redact@0.4.0': {} + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 26.0.0 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 26.0.0 + + '@types/express-serve-static-core@5.1.2': + dependencies: + '@types/node': 26.0.0 + '@types/qs': 6.15.1 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.2 + '@types/serve-static': 2.2.0 + + '@types/http-errors@2.0.5': {} + + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 26.0.0 + + '@types/ms@2.1.0': {} + + '@types/node@26.0.0': + dependencies: + undici-types: 8.3.0 + + '@types/qs@6.15.1': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@1.2.1': + dependencies: + '@types/node': 26.0.0 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 26.0.0 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 26.0.0 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + atomic-sleep@1.0.0: {} + + base64-js@1.5.1: {} + + better-sqlite3@12.11.1: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + body-parser@2.3.0: + dependencies: + bytes: 3.1.2 + content-type: 2.0.0 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + on-finished: 2.4.1 + qs: 6.15.3 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + + buffer-equal-constant-time@1.0.1: {} + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + chownr@1.1.4: {} + + colorette@2.0.20: {} + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + dateformat@4.6.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + depd@2.0.0: {} + + detect-libc@2.1.2: {} + + dotenv@17.4.2: {} + + drizzle-kit@0.31.10: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.12 + tsx: 4.22.4 + + drizzle-orm@0.45.2(better-sqlite3@12.11.1): + optionalDependencies: + better-sqlite3: 12.11.1 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + encodeurl@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + expand-template@2.0.3: {} + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.3 + range-parser: 1.3.0 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-copy@4.0.4: {} + + fast-safe-stringify@2.1.1: {} + + file-uri-to-path@1.0.0: {} + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs-constants@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + github-from-package@0.0.0: {} + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + help-me@5.0.0: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ipaddr.js@1.9.1: {} + + is-promise@4.0.0: {} + + joycon@3.1.1: {} + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.8.5 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.once@4.1.1: {} + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mimic-response@3.1.0: {} + + minimist@1.2.8: {} + + mkdirp-classic@0.5.3: {} + + ms@2.1.3: {} + + napi-build-utils@2.0.0: {} + + negotiator@1.0.0: {} + + node-abi@3.94.0: + dependencies: + semver: 7.8.5 + + object-inspect@1.13.4: {} + + on-exit-leak-free@2.1.2: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + parseurl@1.3.3: {} + + path-to-regexp@8.4.2: {} + + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-pretty@13.1.3: + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 4.0.4 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pump: 3.0.4 + secure-json-parse: 4.1.0 + sonic-boom: 4.2.1 + strip-json-comments: 5.0.3 + + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.2.0 + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.94.0 + pump: 3.0.4 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.5 + tunnel-agent: 0.6.0 + + process-warning@5.0.0: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + qs@6.15.3: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + + quick-format-unescaped@4.0.4: {} + + range-parser@1.3.0: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + unpipe: 1.0.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + real-require@0.2.0: {} + + real-require@1.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + safe-buffer@5.2.1: {} + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + secure-json-parse@4.1.0: {} + + semver@7.8.5: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.3.0 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + split2@4.2.0: {} + + statuses@2.0.2: {} + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-json-comments@2.0.1: {} + + strip-json-comments@5.0.3: {} + + tar-fs@2.1.5: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + thread-stream@4.2.0: + dependencies: + real-require: 1.0.0 + + toidentifier@1.0.1: {} + + tsx@4.22.4: + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typescript@6.0.3: {} + + undici-types@8.3.0: {} + + unpipe@1.0.0: {} + + util-deprecate@1.0.2: {} + + vary@1.1.2: {} + + wrappy@1.0.2: {} + + ws@8.21.0: {} + + xmlchars@2.2.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..96a2473 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,6 @@ +packages: + - "UndauntedMetagame" + - "UndauntedDeployServer" +allowBuilds: + better-sqlite3: true + esbuild: true