diff --git a/.github/workflows/perf-baseline-update.yml b/.github/workflows/perf-baseline-update.yml new file mode 100644 index 00000000..2a03558d --- /dev/null +++ b/.github/workflows/perf-baseline-update.yml @@ -0,0 +1,91 @@ +name: Update Performance Baseline + +on: + push: + branches: [master] + paths: + - 'server/src/**' + - 'client/src/**' + - 'protocol/**' + + workflow_dispatch: + +jobs: + update-baseline: + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Map local.hytopiahosting.com to localhost + run: echo "127.0.0.1 local.hytopiahosting.com" | sudo tee -a /etc/hosts + + - name: Install server dependencies + run: cd server && npm ci + + - name: Build SDK + run: cd server && npm run build + + - name: Install perf-tools + run: cd packages/perf-tools && PUPPETEER_SKIP_DOWNLOAD=true npm ci && npm run build + + - name: Run benchmarks (3 rounds, averaged) + run: | + mkdir -p .perf-baseline + for preset in idle stress; do + echo "=== Running ${preset} benchmark (3 rounds) ===" + for i in 1 2 3; do + cd packages/perf-tools + npx hytopia-bench run --preset ${preset} --output "../../.perf-baseline/${preset}-run${i}.json" --verbose || true + cd ../.. + done + done + + - name: Average baselines + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const dir = '.perf-baseline'; + const presets = ['idle', 'stress']; + for (const preset of presets) { + const runs = []; + for (let i = 1; i <= 3; i++) { + const path = `${dir}/${preset}-run${i}.json`; + if (fs.existsSync(path)) { + const data = JSON.parse(fs.readFileSync(path, 'utf-8')); + if (data.baseline) runs.push(data.baseline); + } + } + if (runs.length === 0) continue; + const avg = {}; + const keys = ['avgTickMs', 'maxTickMs', 'p95TickMs', 'p99TickMs', 'ticksOverBudgetPct', 'avgMemoryMb']; + for (const key of keys) { + const values = runs.map(r => r[key]).filter(v => v !== undefined); + avg[key] = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0; + } + avg.operations = runs[0].operations || {}; + fs.writeFileSync(`${dir}/${preset}.json`, JSON.stringify(avg, null, 2)); + console.log(`${preset} baseline averaged from ${runs.length} runs`); + } + + - name: Cache baseline + uses: actions/cache/save@v4 + with: + path: .perf-baseline/ + key: perf-baseline-master-${{ github.sha }} + + - name: Also save as latest + uses: actions/cache/save@v4 + with: + path: .perf-baseline/ + key: perf-baseline-master diff --git a/.github/workflows/perf-gate.yml b/.github/workflows/perf-gate.yml new file mode 100644 index 00000000..f4fc4d2c --- /dev/null +++ b/.github/workflows/perf-gate.yml @@ -0,0 +1,122 @@ +name: Performance Gate + +on: + pull_request: + branches: [master] + paths: + - 'server/src/**' + - 'client/src/**' + - 'protocol/**' + +jobs: + perf-benchmark: + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Map local.hytopiahosting.com to localhost + run: echo "127.0.0.1 local.hytopiahosting.com" | sudo tee -a /etc/hosts + + - name: Install server dependencies + run: cd server && npm ci + + - name: Build SDK + run: cd server && npm run build + + - name: Install perf-tools + run: cd packages/perf-tools && PUPPETEER_SKIP_DOWNLOAD=true npm ci && npm run build + + - name: Restore baseline cache + id: cache-baseline + uses: actions/cache/restore@v4 + with: + path: .perf-baseline/ + key: perf-baseline-${{ github.base_ref }} + + - name: Run idle benchmark + run: | + cd packages/perf-tools + npx hytopia-bench run --preset idle --output ../../.perf-results/idle.json --verbose + continue-on-error: true + + - name: Run stress benchmark + run: | + cd packages/perf-tools + npx hytopia-bench run --preset stress --output ../../.perf-results/stress.json --verbose + continue-on-error: true + + - name: Compare with baseline + if: steps.cache-baseline.outputs.cache-hit == 'true' + run: | + cd packages/perf-tools + RESULTS="" + for preset in idle stress; do + if [ -f "../../.perf-baseline/${preset}.json" ] && [ -f "../../.perf-results/${preset}.json" ]; then + echo "=== Comparing ${preset} ===" + npx hytopia-bench compare \ + "../../.perf-baseline/${preset}.json" \ + "../../.perf-results/${preset}.json" \ + --warn 5 --fail 10 || true + fi + done + + - name: Post results as PR comment + if: always() + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const results = []; + const dir = '.perf-results'; + if (fs.existsSync(dir)) { + for (const file of fs.readdirSync(dir)) { + if (file.endsWith('.json')) { + const data = JSON.parse(fs.readFileSync(`${dir}/${file}`, 'utf-8')); + results.push(data); + } + } + } + if (results.length === 0) { + console.log('No benchmark results to post'); + return; + } + let body = '## Performance Benchmark Results\n\n'; + for (const r of results) { + body += `### ${r.scenario}\n`; + body += `| Metric | Value |\n|--------|-------|\n`; + if (r.baseline) { + const b = r.baseline; + body += `| Avg Tick | ${b.avgTickMs?.toFixed(2) ?? 'N/A'}ms |\n`; + body += `| P95 Tick | ${b.p95TickMs?.toFixed(2) ?? 'N/A'}ms |\n`; + body += `| P99 Tick | ${b.p99TickMs?.toFixed(2) ?? 'N/A'}ms |\n`; + body += `| Max Tick | ${b.maxTickMs?.toFixed(2) ?? 'N/A'}ms |\n`; + body += `| Over Budget | ${b.ticksOverBudgetPct?.toFixed(1) ?? 'N/A'}% |\n`; + body += `| Avg Memory | ${b.avgMemoryMb?.toFixed(1) ?? 'N/A'}MB |\n`; + } + body += '\n'; + } + body += '\n---\n*Generated by @hytopia/perf-tools*\n'; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + + - name: Upload results artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: perf-results + path: .perf-results/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 40b878db..85aac67b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -node_modules/ \ No newline at end of file +node_modules/ +server/src/playground.mjs +server/src/perf-harness.mjs +packages/*/dist/ +packages/perf-tools/perf-results/*/ diff --git a/CLAUDE.md b/CLAUDE.md index 3a1ae8a1..d672221a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,7 +20,7 @@ HYTOPIA is a multiplayer voxel game engine monorepo. The **server** (TypeScript/ - **Physics**: Rapier3D (`@dimforge/rapier3d-simd-compat`) at 60 Hz, default gravity `y = -32` - **Networking**: WebTransport (QUIC) preferred, WebSocket fallback. Packets serialized with msgpackr, large payloads gzip-compressed - **Protocol**: `protocol/` defines all packet schemas (AJV-validated). Published as `@hytopia.com/server-protocol` -- **Rendering**: Three.js `WebGLRenderer` + `MeshBasicMaterial` (no dynamic lights). Post-processing: SMAA, bloom, outline. Chunk meshes built in Web Worker via greedy meshing + AO +- **Rendering**: Three.js `WebGLRenderer` + `MeshBasicMaterial` (no dynamic lights). Post-processing: SMAA, bloom, outline. Chunk meshes built in a Web Worker with face culling + AO (no greedy quad merging) - **Persistence**: `@hytopia.com/save-states` for player/global KV data - **Singleton pattern**: Most server systems use `ClassName.instance`; client systems owned by `Game` singleton diff --git a/CODEBASE_DOCUMENTATION.md b/CODEBASE_DOCUMENTATION.md index d19d5bf8..f436e9a0 100644 --- a/CODEBASE_DOCUMENTATION.md +++ b/CODEBASE_DOCUMENTATION.md @@ -16,6 +16,7 @@ PROTOCOL: protocol/ - Packet schemas + definitions (@hytopia.com/server-protocol SDK: sdk/ - Git submodule → hytopiagg/sdk (build output lands here) EXAMPLES: sdk-examples/ - Reference games built with the SDK ASSETS: assets/release/ - Default game assets (audio, blocks, maps, models, particles, skyboxes, ui) +PERF: packages/perf-tools/ - Benchmark CLI + trace analysis (`hytopia-bench`), headless client metrics, synthetic + real-game presets (`zoo-game-full` single-client benchmark, `zoo-game-observe` 5-client joinable Zoo run), helper scripts for linking/running external games (including linked SDK runtime deps, target-ref dependency prep via `ensure-node-modules.sh`, `apply-instrumentation-overlay.sh` for temporary legacy-ref PerfBridge/PerfHarness patching, `overlays/legacy-server/` for older PerfBaseline refs, `overlays/minimal-server/` for telemetry-minimal refs that need injected monitor/network hooks, repeatable HyFire2/Zoo Game workflows, and `run-owned-stack-suite.sh` for one-command multi-game runs against a chosen engine ref/PR with `origin`/`upstream` fetch fallback). The perf CLI now supports repeated-run median workflows via `BenchmarkSeriesAggregator.ts`, `hytopia-bench aggregate`, `hytopia-bench compare-series`, and `run-owned-stack-suite.sh --repeat `, so noisy real-game/client scenarios can be judged on stable medians instead of one-off runs. Older engine refs now prefer overlayed client/server perf hooks, normalize report outputs to stable paths, then fall back to validated legacy `/__perf` normalization plus compare-time metric skipping instead of misleading zero baselines. CONFIG: package.json - Monorepo root (npm workspaces) server/package.json - Server deps + build scripts server/tsconfig.json - Strict TS, path alias @/* → ./src/* @@ -140,9 +141,18 @@ shared/types/math/Vector3Like.ts - Vector3 interface errors/ErrorHandler.ts - Fatal error handling + crash protection events/EventRouter.ts - Typed event emitter (eventemitter3) events/Events.ts - Event payload type definitions +bots/BotManager.ts, bots/BotPlayer.ts - Server-side bot players for perf/stress tests +metrics/Monitor.ts - @Monitor decorators + helper wrappers +metrics/CpuProfiler.ts - V8 CPU profile + heap snapshot capture (debug tooling) +metrics/PerformanceMonitor.ts - Tick profiler + operation percentiles + spikes +metrics/NetworkMetrics.ts - Byte/packet/serialization counters metrics/Telemetry.ts - Span-based performance profiling models/ModelRegistry.ts - GLTF model preloading + bounding box extraction persistence/PersistenceManager.ts - Player/global KV storage via @hytopia.com/save-states +perf/PerfHarness.ts - Env-gated /__perf endpoints for perf-tools +perf/PerfBlockChurner.ts - Tick-driven block churn stressor (perf-tools) +perf/PerfWorldGenerator.ts - Synthetic block-world generator for perf-tools scenarios +perf/perf-harness.ts - Benchmark server entry (build:perf-harness → src/perf-harness.mjs) server/src/assets/AssetsLibrary.ts - Asset path resolution ``` @@ -423,4 +433,4 @@ zombies-fps/ - Zombie FPS - **Dual transport** — WebTransport (QUIC) preferred, WebSocket fallback. Reliable stream + unreliable datagrams - **msgpackr serialization** — All packets serialized with msgpackr, large payloads gzip-compressed - **60 Hz physics / 30 Hz network** — Server physics ticks at 60 Hz, network sync flushes every 2 ticks -- **Web Worker meshing** — Client offloads greedy meshing + AO to a dedicated Web Worker +- **Web Worker meshing** — Client offloads face-culling meshing + AO to a dedicated Web Worker (no greedy quad merging) diff --git a/README.md b/README.md index 30d19f0f..66d9feed 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ The `Game` singleton owns all subsystem managers. Key systems: |---|---| | `NetworkManager` | WebTransport (HTTP/3) with WebSocket fallback. Deserializes msgpack packets and dispatches typed events | | `Renderer` | Three.js `WebGLRenderer` + `EffectComposer`. Post-processing: SMAA, selective bloom, outline pass, `CSS2DRenderer` for in-world UI | -| `ChunkMeshManager` + `ChunkWorkerClient` | Voxel mesh generation via greedy meshing with ambient occlusion, offloaded to a Web Worker | +| `ChunkMeshManager` + `ChunkWorkerClient` | Voxel mesh generation with face culling + ambient occlusion, offloaded to a Web Worker (no greedy quad merging) | | `EntityManager` | Entity lifecycle and GLTF model rendering | | `InputManager` + `MobileManager` | Keyboard/mouse/gamepad input and touch/joystick for mobile | | `UIManager` | HTML/CSS overlay UI system for game developer UIs and in-world `SceneUI` elements | diff --git a/ai-memory/docs/perf-branch-state-2026-03-06/FINAL.md b/ai-memory/docs/perf-branch-state-2026-03-06/FINAL.md new file mode 100644 index 00000000..de1afe8d --- /dev/null +++ b/ai-memory/docs/perf-branch-state-2026-03-06/FINAL.md @@ -0,0 +1,396 @@ +# HYTOPIA Performance Framework State Report + +**Date:** 2026-03-06 +**Repo:** `web3dev1337/hytopia-source` +**Branch:** `feature/perf-external-notes-verification-20260305` + +## Executive Summary + +The original idea for this branch was correct: + +Build a reusable performance framework for HYTOPIA engine work so future SDK/client changes can be measured against repeatable synthetic scenarios and real games such as HyFire2 and Zoo Game. + +That framework now exists. + +What happened after that is also clear: + +- the branch built the real framework +- the framework was verified against synthetic and real-game scenarios +- the branch then got mixed with a local blob-shadow investigation +- this cleanup separates those concerns again + +Follow-up update from local real-game verification: + +- joinable external-game runs exposed a local-dev crash path where sessionless local players still attempted live platform cosmetics lookup +- that engine-side issue is now fixed in [PlatformGateway.ts](/home/ab/GitHub/hytopia/work1/server/src/networking/PlatformGateway.ts) and [Player.ts](/home/ab/GitHub/hytopia/work1/server/src/players/Player.ts) +- result: local HyFire2 observation runs no longer fall over on human join just because the production GraphQL cosmetics websocket rejects the request +- HyFire2 itself also needed a game-side PlayerCamera compatibility fix so bots-only spectator setup no longer crashes after human team selection during observation runs + +After this cleanup, the branch should be understood as: + +- **permanent framework code** kept +- **real-game benchmark glue** kept when it is broadly useful +- **feature-under-test monkey patches** removed +- **raw local experiment output** removed from the working tree +- **findings** preserved in this report + +## Original Goal + +The intended outcome was a benchmark system that can answer: + +- did this server-side change hurt tick time or memory? +- did this client-side change hurt FPS, frame time, draw calls, or triangles? +- does a change regress only desktop, or also throttled/mobile conditions? +- can we replay the same game situations over time rather than inventing ad hoc tests? + +That goal is reflected in: + +- [init.md](/home/ab/GitHub/hytopia/work1/ai-memory/feature/perf-external-notes-verification-20260305-2249094/init.md) +- [plan.md](/home/ab/GitHub/hytopia/work1/ai-memory/feature/perf-external-notes-verification-20260305-2249094/plan.md) +- [progress.md](/home/ab/GitHub/hytopia/work1/ai-memory/feature/perf-external-notes-verification-20260305-2249094/progress.md) +- [perf-final-2026-03-05/FINAL.md](/home/ab/GitHub/hytopia/work1/ai-memory/docs/perf-final-2026-03-05/FINAL.md) + +## What We Actually Built + +### Server-Side Perf Instrumentation + +Core files: + +- [PerformanceMonitor.ts](/home/ab/GitHub/hytopia/work1/server/src/metrics/PerformanceMonitor.ts) +- [NetworkMetrics.ts](/home/ab/GitHub/hytopia/work1/server/src/metrics/NetworkMetrics.ts) +- [CpuProfiler.ts](/home/ab/GitHub/hytopia/work1/server/src/metrics/CpuProfiler.ts) +- [PerfHarness.ts](/home/ab/GitHub/hytopia/work1/server/src/perf/PerfHarness.ts) +- [PerfWorldGenerator.ts](/home/ab/GitHub/hytopia/work1/server/src/perf/PerfWorldGenerator.ts) +- [PerfBlockChurner.ts](/home/ab/GitHub/hytopia/work1/server/src/perf/PerfBlockChurner.ts) + +Capabilities: + +- tick timing snapshots +- operation-level timings +- heap and RSS reporting +- network metrics +- perf endpoints for automated runs +- synthetic world generation +- block churn stressors + +### Benchmark Runner and CLI + +Core files: + +- [cli.ts](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/cli.ts) +- [BenchmarkRunner.ts](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/runners/BenchmarkRunner.ts) +- [BenchmarkSeriesAggregator.ts](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/runners/BenchmarkSeriesAggregator.ts) +- [MetricCollector.ts](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/runners/MetricCollector.ts) +- [ProcessMonitor.ts](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/runners/ProcessMonitor.ts) +- [ServerApiClient.ts](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/runners/ServerApiClient.ts) +- [BaselineComparer.ts](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/runners/BaselineComparer.ts) +- [ConsoleReporter.ts](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/reporters/ConsoleReporter.ts) +- [JsonReporter.ts](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/reporters/JsonReporter.ts) + +Capabilities: + +- scenario-based benchmark execution +- JSON report output +- baseline comparisons +- repeated-run median aggregation +- series comparison verdicts for noisy scenarios +- regression thresholds +- OS-level process monitoring +- log capture +- external-server mode + +### Client-Side Perf Metrics + +Core files: + +- [PerfBridge.ts](/home/ab/GitHub/hytopia/work1/client/src/core/PerfBridge.ts) +- [HeadlessClient.ts](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/runners/HeadlessClient.ts) + +Capabilities: + +- FPS +- frame time +- draw calls +- triangles +- entities +- chunks +- GLTF stats +- JS heap +- browser CPU throttling + +This is what made client feature benchmarking real instead of only measuring server tick time. + +### Scenario and Preset System + +Representative presets: + +- [idle.yaml](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/presets/idle.yaml) +- [stress.yaml](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/presets/stress.yaml) +- [join-storm.yaml](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/presets/join-storm.yaml) +- [block-churn.yaml](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/presets/block-churn.yaml) +- [entity-density.yaml](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/presets/entity-density.yaml) +- [multi-world.yaml](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/presets/multi-world.yaml) +- [blocks-10m-dense.yaml](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/presets/blocks-10m-dense.yaml) +- [hyfire2-bots.yaml](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/presets/hyfire2-bots.yaml) +- [zoo-game-bots.yaml](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/presets/zoo-game-bots.yaml) +- [zoo-game-full.yaml](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/presets/zoo-game-full.yaml) +- [zoo-game-observe.yaml](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/presets/zoo-game-observe.yaml) + +`zoo-game-full.yaml` is the cleaned real-game walkthrough benchmark retained from the local investigation. It is intentionally single-client: one benchmark browser joins, sends `/fillzoo`, and walks the route while client metrics are collected. It is not the “human joins a world with 5 other movers” observation mode. + +`zoo-game-observe.yaml` is the joinable observation preset. It launches 5 real benchmark browser clients, has each one send `/fillzoo` to populate its own zoo, and then moves them during the measured phase so a human observer joins a genuinely busy 6-slot Zoo world. CPU throttling is still a runner option, not hardcoded in either preset. + +### Real Game Integration + +Core files: + +- [ensure-node-modules.sh](/home/ab/GitHub/hytopia/work1/packages/perf-tools/scripts/ensure-node-modules.sh) +- [link-sdk.sh](/home/ab/GitHub/hytopia/work1/packages/perf-tools/scripts/link-sdk.sh) +- [setup-game.sh](/home/ab/GitHub/hytopia/work1/packages/perf-tools/scripts/setup-game.sh) +- [run-external-game-benchmark.sh](/home/ab/GitHub/hytopia/work1/packages/perf-tools/scripts/run-external-game-benchmark.sh) +- [run-owned-stack-suite.sh](/home/ab/GitHub/hytopia/work1/packages/perf-tools/scripts/run-owned-stack-suite.sh) + +What these do: + +- build the local SDK from this repo +- link it into external game repos and install the linked SDK's external runtime deps +- run a real-game preset end-to-end against an external game using the current source checkout under test +- run the core synthetic presets plus owned-game presets from one wrapper command against a chosen engine branch, commit, or PR number +- let HyFire2 or Zoo Game run against local engine changes + +Cross-ref hardening added after testing `RZDESIGN/hytopia-source@merged-all-prs-into-one`: + +- target engine worktrees no longer borrow current-branch `client/node_modules` or `server/node_modules` blindly +- dependency reuse only happens when the target package lockfile or manifest matches; otherwise the target ref gets its own install +- SDK linking for external-game runs now does a runtime `build:server` build instead of the full declaration/docs pipeline, so older engine refs do not fail just because their type/doc build is stale +- the suite auto-picks the actual free client port starting from `4173` and launches Vite with `--strictPort`, preventing silent `4173` -> `4174` drift +- `run-owned-stack-suite.sh` now resolves `--engine-ref pr:` and other fetched refs through `origin` first and then `upstream`, so upstream PRs can be benchmarked directly from this fork checkout +- benchmark JSON now records validation/capability state so missing snapshots are surfaced as warnings/issues instead of silently becoming zero baselines +- `compare` now skips non-shared metric families such as server snapshots or render counters when one side lacks them, instead of treating missing data as an improvement +- `hytopia-bench aggregate` can combine repeated benchmark JSONs into a single median report, preserving validation/capability metadata and the source file list +- `hytopia-bench compare-series` compares two repeated benchmark sets via median aggregation and emits a simple series verdict (`improves`, `neutral`, `regresses`, `inconclusive`) based on the core metrics +- `run-owned-stack-suite.sh --repeat ` now runs each scenario multiple times, stores the per-run JSON under `repeats//`, and writes a median aggregate to the stable top-level scenario path used by follow-up compare commands + +The simplest “test this engine PR across our stack” entrypoint is now: + +```bash +bash packages/perf-tools/scripts/run-owned-stack-suite.sh \ + --engine-ref pr:2 \ + --client-url http://localhost:4173 \ + --repeat 3 +``` + +That wrapper can: + +- resolve a branch, commit, or `pr:` to a temporary engine worktree +- run internal synthetic presets with client metrics enabled +- run Zoo Game and HyFire2 through the external-game wrapper with the owned local paths baked in +- write all JSON outputs plus a per-run markdown summary into `packages/perf-tools/perf-results/owned-stack/` + +Important limitation: + +- very old engine refs now get a temporary instrumentation overlay first, which patches in the current client `PerfBridge`, a compatible server `PerfHarness` shim, and the legacy entrypoint glue needed for repeatable real-game runs +- if a target ref still cannot expose the full modern server metric surface after that overlay, the framework falls back to normalized legacy `/__perf` snapshots and labels the missing metric families explicitly instead of pretending they are full-stack apples-to-apples + +Important clarification: + +HyFire2 and Zoo Game are not first-class game source trees inside this repo. This repo provides the engine plus the tooling to benchmark those games from their own directories. + +### Local Paths Used On This Machine + +The concrete local game paths that were actually discovered and used during verification on this machine are: + +- HyFire2: `/home/ab/GitHub/games/hyfire2-sdk-compat` +- Zoo Game: `/home/ab/GitHub/games/hytopia/zoo-game/work1` + +These paths are machine-specific and do not belong in the repo-wide codebase inventory, but they do belong in this perf state/runbook so the next real-game benchmark does not require rediscovery. + +### CI Automation + +Core files: + +- [perf-gate.yml](/home/ab/GitHub/hytopia/work1/.github/workflows/perf-gate.yml) +- [perf-baseline-update.yml](/home/ab/GitHub/hytopia/work1/.github/workflows/perf-baseline-update.yml) + +Current CI state: + +- baseline capture exists +- PR perf gating exists +- current CI is still oriented around lightweight scenarios like `idle` and `stress` +- full external-game runs remain more manual + +## What Was Verified + +The branch progress log says the following were completed: + +- OS-level monitoring +- PerfHarness fallback mode +- log capture +- HyFire2 runs +- Zoo Game runs +- client PerfBridge metrics +- headless client automation +- blob-shadow A/B investigation using real client metrics + +Representative result files already in the repo: + +| Scenario | Source | Summary | +|---|---|---| +| HyFire2 bots | [hyfire2-bots.json](/home/ab/GitHub/hytopia/work1/packages/perf-tools/perf-results/hyfire2-bots.json) | avg tick `0.61ms`, p99 `1.34ms`, avg memory `431MB` | +| Zoo Game full PerfHarness | [zoo-game-bots-full.json](/home/ab/GitHub/hytopia/work1/packages/perf-tools/perf-results/zoo-game-bots-full.json) | avg tick `0.25ms`, p99 `0.85ms`, avg memory `313MB` | +| Stress A/B baseline | [stress-baseline-no-shadows.json](/home/ab/GitHub/hytopia/work1/packages/perf-tools/perf-results/stress-baseline-no-shadows.json) | client avg FPS about `27.8` | +| Stress A/B blob shadows | [stress-with-blob-shadows.json](/home/ab/GitHub/hytopia/work1/packages/perf-tools/perf-results/stress-with-blob-shadows.json) | client avg FPS about `26.1` | +| Mobile stress baseline | [mobile-baseline.json](/home/ab/GitHub/hytopia/work1/packages/perf-tools/perf-results/mobile-baseline.json) | client avg FPS about `11.0` | +| Mobile stress blob shadows | [mobile-blob-shadows.json](/home/ab/GitHub/hytopia/work1/packages/perf-tools/perf-results/mobile-blob-shadows.json) | client avg FPS about `11.8` | + +Bottom line: + +- the framework was exercised end-to-end +- it produced usable data +- it reached both synthetic and real-game scenarios +- HyFire2 human-join observation runs are now part of the validated real-game flow, provided the game repo is on its SDK-compat branch with the PlayerCamera hidden-node fix + +## Branch Timeline + +| Commit | Meaning | +|---|---| +| `e518ad3` | imported external perf notes and verification material | +| `8581645` | broad perf framework research pass | +| `0e7f689` | initial framework implementation | +| `adb7561` | wired perf-tools end to end | +| `6ec796d` | expanded perf harness and more scenarios | +| `0793124` | added process monitoring and real-game presets | +| `b757df9` | restored map compression codecs and captured real-game results | +| `0ed330d` | added missing entity APIs and Zoo Game full PerfHarness benchmark | +| `2f69c38` | added client-side PerfBridge and Puppeteer benchmarking | +| `aac6de2` | fixed headless navigation and error handling | +| `4e716cc` | improved headless connection and earlier PR #2 blob-shadow A/B work | +| `2b3ebf2` | made A/B flow more deterministic | +| `b6bdca2` | added mobile CPU throttle benchmark flow | + +## What This Cleanup Kept + +This cleanup retains the broadly reusable framework improvements that were still only local: + +- `--external-server` support in [cli.ts](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/cli.ts) +- `send_chat` scenario action in [ScenarioLoader.ts](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/runners/ScenarioLoader.ts) +- chat-triggered setup support in [HeadlessClient.ts](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/runners/HeadlessClient.ts) +- external-server handling, legacy-server metric normalization, and validation-aware baseline generation in [BenchmarkRunner.ts](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/runners/BenchmarkRunner.ts) +- a cleaned [zoo-game-full.yaml](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/presets/zoo-game-full.yaml) preset +- a documented [zoo-game-observe.yaml](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/presets/zoo-game-observe.yaml) preset for live join/observation runs +- runner-level `--cpu-throttle` support so desktop/mobile/low-end comparisons no longer require editing YAML +- scoped local HTTPS handling and legacy `/__perf` snapshot compatibility in [ServerApiClient.ts](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/runners/ServerApiClient.ts) instead of a global TLS-disable environment hack +- temporary target-ref instrumentation overlay support in [apply-instrumentation-overlay.sh](/home/ab/GitHub/hytopia/work1/packages/perf-tools/scripts/apply-instrumentation-overlay.sh) + +These are deliberate framework improvements, not feature-under-test patches. + +There is also one engine stability fix that came out of the real-game validation itself: + +- local sessionless dev players no longer trigger live platform cosmetics fetches on join +- `PlatformGateway` now skips GraphQL cosmetics access when the platform gateway is unavailable in local development +- `Player` now only requests cosmetics for real platform-backed sessions + +## What This Cleanup Removed + +### Removed from the Working Tree + +- local blob-shadow changes in `client/src/entities/Entity.ts` +- local blob-shadow quality toggles in `client/src/settings/SettingsManager.ts` +- local transparent-sort fallback in `client/src/three/utils.ts` +- incidental `client/package-lock.json` churn +- raw local Zoo Game desktop/4x/16x JSON outputs +- the loose `zoo-game-blob-shadows-report.md` experiment file + +### Why Those Were Removed + +They were not framework code. They were a mixture of: + +- feature-under-test code copied from upstream PR #2 +- local monkey patches needed to make that feature benchmarkable +- ad hoc raw output from one investigation session + +That material belongs in a focused feature-evaluation branch or report, not in the core framework state. + +## Blob-Shadow Investigation Findings + +The raw local files were removed from the working tree during cleanup, but the results are preserved here. + +### Correctness Finding + +PR #2 blob-shadow meshes did not populate the transparent sort metadata expected by `getTransparentSortKey()`. Without a fallback, rendering crashed during benchmarking. + +That means the feature under test was not benchmarkable as-is. + +### Performance Summary from the Local Zoo Game Run + +| Tier | Baseline FPS | Blob Shadows FPS | Change | Verdict | +|---|---|---|---|---| +| Desktop | `25.1` | `21.0` | `-16.4%` | real regression | +| 4x CPU throttle | `14.1` | `16.1` | `+14.4%` | likely variance / neutral | +| 16x CPU throttle | `7.6` | `7.5` | `-1.5%` | roughly neutral on average | + +Other relevant findings: + +- desktop min FPS dropped from `17` to `7` +- frame time rose from `41.8ms` to `52.8ms` on desktop +- draw calls and triangles barely changed +- likely cost was not geometry count but shadow update/lifecycle overhead + +Interpretation: + +- the framework successfully surfaced a concrete correctness bug +- it also surfaced a likely desktop performance regression +- that work was real analysis, but it was not permanent framework code + +## Current Assessment + +If the question is: + +“Did we actually build the performance framework I asked for?” + +The answer is **yes**. + +If the question is: + +“Were we also mixing in local one-off patching while investigating a feature?” + +The answer is **yes**. + +If the question is: + +“Has that now been separated?” + +The answer should now be **yes**: + +- reusable framework additions retained +- temporary feature-under-test patches removed +- experiment findings documented here +- raw local experiment output removed from the tree + +## Remaining Gaps + +1. Full external-game benchmarking is still more manual than synthetic presets. +2. CI is still centered on lightweight built-in scenarios rather than full game walkthroughs. +3. HyFire2/Zoo Game benchmarking still depends on local setup and linked SDK flows. +4. Some external-game compatibility fixes belong in the game repos, not here. HyFire2 now needs its own latest-SDK compatibility patch set for removed server light APIs, controller setup changes, animation-stop API changes, and one bad `SiteMarker` asset path. +5. Observation-mode external-game runs are now stable for local human joins, but extreme low-end throttled browser clients can still churn or reconnect under heavy load. + +## Read This First Tomorrow + +If you only read a few files, read these: + +1. [this report](/home/ab/GitHub/hytopia/work1/ai-memory/docs/perf-branch-state-2026-03-06/FINAL.md) +2. [perf-final-2026-03-05/FINAL.md](/home/ab/GitHub/hytopia/work1/ai-memory/docs/perf-final-2026-03-05/FINAL.md) +3. [progress.md](/home/ab/GitHub/hytopia/work1/ai-memory/feature/perf-external-notes-verification-20260305-2249094/progress.md) +4. [BenchmarkRunner.ts](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/runners/BenchmarkRunner.ts) +5. [PerfBridge.ts](/home/ab/GitHub/hytopia/work1/client/src/core/PerfBridge.ts) +6. [zoo-game-full.yaml](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/presets/zoo-game-full.yaml) +7. [zoo-game-observe.yaml](/home/ab/GitHub/hytopia/work1/packages/perf-tools/src/presets/zoo-game-observe.yaml) + +## Bottom Line + +The framework is real. + +The branch was doing the right thing conceptually. + +What was wrong was that a useful framework branch had become mixed with a local feature investigation. This cleanup keeps the framework, removes the monkey patches, and leaves one report that explains the whole state without needing to reconstruct it from scattered files. diff --git a/ai-memory/docs/perf-external-notes-2026-03-05/FINDINGS.md b/ai-memory/docs/perf-external-notes-2026-03-05/FINDINGS.md new file mode 100644 index 00000000..ad698935 --- /dev/null +++ b/ai-memory/docs/perf-external-notes-2026-03-05/FINDINGS.md @@ -0,0 +1,80 @@ +# External Notes vs. HYTOPIA Source (Verification + PR Cross-Check) + +Base reference for verification in this branch: `origin/master` at `24a295d` (2026-03-05). + +## What Was Imported + +Unmodified external notes live in `ai-memory/docs/perf-external-notes-2026-03-05/raw/`. + +## Quick Take + +The external docs mix: + +- **Accurate observations about the current client** (notably: face culling exists; greedy meshing does not; geometry churn is high; packet decompression is synchronous). +- **Roadmap/architecture assumptions that do not match `master`** (procedural streaming, time-budgeted collider queues, LOD/occlusion/face-limit systems, several referenced constants/functions). + +So: use them as *idea input*, but treat many “current state” statements as unverified unless they point to code that exists on `master`. + +## Claim Verification (Against `master`) + +### Client meshing/rendering + +- ✅ **Face culling exists**: `client/src/workers/ChunkWorker.ts` culls faces when neighbor blocks are solid/opaque. +- ❌ **Greedy meshing is not implemented**: `client/src/workers/ChunkWorker.ts` emits per-face quads (4 vertices per visible face) with no quad merging pass. +- ❌ **Vertex pooling is not present**: `client/src/chunks/ChunkMeshManager.ts` recreates a new `BufferGeometry` for each batch update and disposes the old geometry. +- ❌ **LOD / cave occlusion / “face limit safety caps” described in notes are not found** via repo search on `client/src/` (`lod`, `occlusion`, face-count thresholds, BFS visibility, etc.). + +### Client networking + +- ✅ **Synchronous gzip decompression on the main thread**: `client/src/network/NetworkManager.ts` calls `gunzipSync` (fflate) before msgpack decode. + +### Server networking (entity/chunk sync) + +- ✅ **Entity pos/rot are a dominant sync path (and split to unreliable when pos/rot-only)**: `server/src/networking/NetworkSynchronizer.ts`. +- ❌ **No entity quantization/delta fields exist today**: `protocol/schemas/Entity.ts` has only `p` (Vector) and `r` (Quaternion). `server/src/networking/Serializer.ts` serializes full float arrays. +- ❌ **No chunk pacing/segmentation is implemented**: `server/src/networking/NetworkSynchronizer.ts` batches *all queued chunk syncs* into a single packet each sync. + +### Server colliders / chunk streaming + +Several external docs reference a *procedural streaming* pipeline (chunks-per-tick, queued collider chunk processing, async region I/O). Those specific codepaths/constants (e.g. `CHUNKS_PER_TICK`, `processPendingColliderChunks`, `COLLIDER_MAX_CHUNK_DISTANCE`, `server/src/worlds/maps/*`) are **not present on `master`**. + +## Notable Errors / Corrections in the Notes + +- **Quantized position range math is wrong as written**: + - If you encode `pq = round(x * 256)` into **int16**, the representable world range is about **±128 blocks**, not ±32768 blocks. + - To keep **1/256 block precision** over large worlds, you need larger integers (e.g. int32), smaller quantization, or chunk-relative encoding. + +## How This Relates to Your Performance PRs + +PRs authored by you that touch performance (as of 2026-03-05): + +- #2 (OPEN) `analysis/codebase-audit`: https://github.com/web3dev1337/hytopia-source/pull/2 +- #3 (OPEN) `docs/iphone-pro-performance-analysis`: https://github.com/web3dev1337/hytopia-source/pull/3 +- #4 (OPEN) `fix/fps-cap-medium-low`: https://github.com/web3dev1337/hytopia-source/pull/4 +- #5 (OPEN) `fix/cap-mobile-dpr`: https://github.com/web3dev1337/hytopia-source/pull/5 +- #6 (OPEN) `feature/map-compression`: https://github.com/web3dev1337/hytopia-source/pull/6 +- #7 (OPEN) `review/mirror-upstream-pr-9`: https://github.com/web3dev1337/hytopia-source/pull/7 +- #8 (OPEN) `review/mirror-upstream-pr-10` (stacked on #7): https://github.com/web3dev1337/hytopia-source/pull/8 +- #9 (OPEN) `review/mirror-upstream-pr-11`: https://github.com/web3dev1337/hytopia-source/pull/9 +- #10 (CLOSED) `fix/cap-mobile-devicepixelratio` (superseded): https://github.com/web3dev1337/hytopia-source/pull/10 + +Where they overlap with the external notes: + +- **High-DPI / mobile GPU load**: + - #4 adds a 60 FPS cap for MEDIUM/LOW (matches the “uncapped 120Hz” problem described in #3). + - #5 caps mobile pixel ratio (matches the “3x DPR” issue described in #3). + - #9 introduces a **pixel budget** based effective pixel ratio and reduces outline overhead (complementary to #3). +- **Outline pass overhead**: + - #9 removes per-mesh define mutation in `SelectiveOutlinePass` by prebuilding shader variants (reduces CPU/shader churn). It does **not** reduce the outline shader’s sampling cost. +- **View-distance mesh visibility**: + - `master` currently iterates all batch meshes each frame. #9 adds cached visibility sets and updates visibility only when the camera crosses a “cell” boundary or settings change. +- **Map size / load time**: + - #6 (compressed maps) addresses the external “JSON map size” concern; the external “binary streaming maps” discussion is broader than #6’s scope. + +## What’s Still Missing (Relative to the External Notes + Your PRs) + +- **Greedy meshing / quad merging** in `client/src/workers/ChunkWorker.ts`. +- **Entity sync quantization / deltas / distance-based rates** (protocol + serializer + client deserializer work). +- **Chunk packet pacing/segmentation** to avoid bursty chunk arrays at join / fast movement. +- **Off-main-thread decompression/decoding** for network payloads (or reduced use of sync `gunzipSync`). + diff --git a/ai-memory/docs/perf-external-notes-2026-03-05/README.md b/ai-memory/docs/perf-external-notes-2026-03-05/README.md new file mode 100644 index 00000000..c1ef1518 --- /dev/null +++ b/ai-memory/docs/perf-external-notes-2026-03-05/README.md @@ -0,0 +1,7 @@ +# External Performance Notes (Imported) + +These documents were copied from the Windows mount (`/mnt/c/Users/AB/Downloads`) on **2026-03-05** and treated as *unverified external notes*. + +- Canonical copies live in `raw/`. +- Some downloads existed as duplicate filenames with ` (1)` suffixes; those duplicates were identical and were not re-copied. + diff --git a/ai-memory/docs/perf-external-notes-2026-03-05/raw/COLLIDER_ARCHITECTURE_RESEARCH.md b/ai-memory/docs/perf-external-notes-2026-03-05/raw/COLLIDER_ARCHITECTURE_RESEARCH.md new file mode 100644 index 00000000..3fe81f1a --- /dev/null +++ b/ai-memory/docs/perf-external-notes-2026-03-05/raw/COLLIDER_ARCHITECTURE_RESEARCH.md @@ -0,0 +1,159 @@ +# Collider Architecture Research + +**Purpose:** Guide the refactor of Hytopia’s block collider system from O(world) to O(nearby chunks). +**Audience:** Engineers implementing Phase 1 (Collider Locality) and Phase 2 (Incremental Voxel Updates). + +--- + +## 1. Current Architecture + +### 1.1 Block Type → Collider + +- One collider per **block type** (dirt, stone, etc.), not per block. +- Voxel collider: Rapier voxel grid; each cell = block present/absent. +- Trimesh collider: Used for non-cube blocks; rebuilt when any block of that type changes. + +### 1.2 Critical Path + +``` +setBlock / addChunkBlocks + → _addBlockTypePlacement + → _getBlockTypePlacements() // iterates ALL chunks of this block type + → _combineVoxelStates(collider) // merges placements into voxel grid + → collider.addToSimulation / setVoxel +``` + +**Problem:** `_getBlockTypePlacements` and `_combineVoxelStates` touch every chunk that contains the block type. As world size grows, this becomes O(world). + +--- + +## 2. Target Architecture: Spatial Locality + +### 2.1 Principle + +- Colliders should only include blocks from chunks **within N chunks of any player** (e.g. N=4). +- When a chunk unloads (player moves away), remove its blocks from colliders. +- When a chunk loads, add its blocks to colliders only if it’s within the active radius. + +### 2.2 Data Structure Change + +**Current:** `_blockTypePlacements` is global (or implicitly spans all chunks). + +**Target:** Maintain a **spatial index**: + +```ts +// Chunk key (bigint) → for each block type in that chunk: Set of global coordinates +private _chunkBlockPlacements: Map>> = new Map(); + +// Active chunk keys: chunks within COLLIDER_RADIUS of any player +private _activeColliderChunkKeys: Set = new Set(); +``` + +- On chunk load: add chunk key to index; add block placements. +- On chunk unload: remove chunk key; remove blocks from colliders. +- `_getBlockTypePlacements` for collider: only return placements from `_activeColliderChunkKeys`. +- `_combineVoxelStates`: only iterate over placements from active chunks. + +### 2.3 Update Flow + +``` +Player moves + → Update _activeColliderChunkKeys (chunks within radius) + → For chunks that left radius: remove from colliders + → For chunks that entered radius: add to colliders + → _combineVoxelStates only over active placements +``` + +--- + +## 3. Incremental Voxel Updates + +### 3.1 Current + +- Adding a chunk: all 4096 blocks added at once to the voxel collider. +- Heavy: `setVoxel` 4096 times + propagation. + +### 3.2 Target + +- Add blocks in **batches** (e.g. 256–512 per tick). +- Time-budget: stop when budget exceeded; resume next tick. +- Rapier voxel API: check if it supports incremental `setVoxel` without full rebuild. + +### 3.3 Implementation Sketch + +```ts +private _pendingVoxelAdds: Array<{ chunk: Chunk; blockTypeId: number; nextIndex: number }> = []; + +function processPendingVoxelAdds(timeBudgetMs: number) { + const start = performance.now(); + while (this._pendingVoxelAdds.length > 0 && (performance.now() - start) < timeBudgetMs) { + const next = this._pendingVoxelAdds[0]; + const chunk = next.chunk; + const count = Math.min(256, chunk.blockCountForType(next.blockTypeId) - next.nextIndex); + for (let i = 0; i < count; i++) { + const idx = next.nextIndex + i; + const globalCoord = chunk.getGlobalCoordinateFromIndex(idx); + collider.setVoxel(globalCoord, true); + } + next.nextIndex += count; + if (next.nextIndex >= chunk.blockCountForType(next.blockTypeId)) { + this._pendingVoxelAdds.shift(); + } + } +} +``` + +--- + +## 4. Trimesh Optimization + +### 4.1 Current + +- Trimesh collider rebuilt whenever any block of that type is added/removed. +- Rebuild = collect all placements, generate mesh, replace collider. + +### 4.2 Options + +1. **Spatial locality:** Only include trimesh blocks from active chunks. Reduces vertex count for large worlds. +2. **Deferred rebuild:** Queue rebuild; execute in next tick within time budget. +3. **Per-chunk trimesh:** If block type is sparse, consider per-chunk trimesh instances instead of one giant trimesh. (Larger change.) + +**Recommendation:** Start with (1) and (2). (3) is Phase 6. + +--- + +## 5. Collider Unload + +When a chunk unloads: + +1. Remove its block placements from the spatial index. +2. For each block type in that chunk: + - Voxel: `setVoxel(coord, false)` for each placement. + - Trimesh: trigger rebuild (only over active chunks). +3. Remove chunk from `_activeColliderChunkKeys`. + +--- + +## 6. Rapier Voxel API Notes + +- Check `rapier3d` docs for `ColliderDesc.heightfield` vs `ColliderDesc.voxel`. +- Voxel colliders: typically a 3D grid; `setVoxel` may or may not support incremental updates. +- If full rebuild required per update: minimize rebuild frequency (batch changes) and scope (active chunks only). + +--- + +## 7. Success Criteria + +| Metric | Before | After | +|--------|--------|-------| +| Chunks scanned per collider update | O(world) | O(active) ~100–300 | +| Time per `_combineVoxelStates` | 5–50 ms | <2 ms | +| Collider add spikes | Full chunk at once | Batched, time-budgeted | + +--- + +## References + +- `ChunkLattice.ts` – `_addChunkBlocksToColliders`, `_combineVoxelStates`, `_getBlockTypePlacements` +- Rapier3D voxel API +- Minecraft: per-section collision, spatial culling diff --git a/ai-memory/docs/perf-external-notes-2026-03-05/raw/ENTITY_SYNC_DELTA_COMPRESSION_DESIGN.md b/ai-memory/docs/perf-external-notes-2026-03-05/raw/ENTITY_SYNC_DELTA_COMPRESSION_DESIGN.md new file mode 100644 index 00000000..c689a4de --- /dev/null +++ b/ai-memory/docs/perf-external-notes-2026-03-05/raw/ENTITY_SYNC_DELTA_COMPRESSION_DESIGN.md @@ -0,0 +1,226 @@ +# Entity Sync: Delta / Compression Design + +**Goal:** Reduce entity position/rotation packet size and bandwidth (currently ~90% of all packets) by replacing full pos/rot with delta or compressed formats. + +--- + +## 1. Current State + +### Flow +- **Server:** Every tick, `entityManager.checkAndEmitUpdates()` runs; each entity calls `checkAndEmitUpdates()`. +- **Entity:** Emits `UPDATE_POSITION` or `UPDATE_ROTATION` when change exceeds threshold: + - **Position:** `ENTITY_POSITION_UPDATE_THRESHOLD_SQ = 0.04²` (0.04 block) + - **Rotation:** `ENTITY_ROTATION_UPDATE_THRESHOLD = cos(3°/2)` (~3°) + - **Player:** Looser position threshold `0.1²` blocks +- **NetworkSynchronizer:** Queues `{ i: id, p: [x,y,z] }` and/or `{ i: id, r: [x,y,z,w] }`. +- **Every 2 ticks (30 Hz):** Splits into reliable vs unreliable; pos/rot-only goes to **unreliable** channel. +- **Serializer:** `serializeVector` → `[x, y, z]`, `serializeQuaternion` → `[x, y, z, w]` (full floats). +- **Transport:** msgpackr with `useFloat32: FLOAT32_OPTIONS.ALWAYS` → 4 bytes per float. + +### Per-Entity Packet Size (approx) +| Format | Bytes (msgpack) | +|--------|-----------------| +| `{ i, p }` pos-only | ~25–35 | +| `{ i, r }` rot-only | ~30–40 | +| `{ i, p, r }` both | ~50–65 | +| 10 entities, pos+rot | ~500–650 | + +With 20 entities at 30 Hz: **~15–20 KB/s** for entity sync alone. + +--- + +## 2. Options for Delta / Compression + +### Option A: Quantized Position (Fixed-Point) + +**Idea:** Encode position as integers. 1 unit = 1/256 block → 0.004 block precision. + +- Range ±32768 blocks → 16-bit signed per axis. +- 3 × 2 bytes = **6 bytes** vs 3 × 4 = 12 bytes (float32). +- **~50% smaller** for position. + +**Implementation:** +```ts +// Server +const QUANT = 256; +p: [Math.round(x * QUANT), Math.round(y * QUANT), Math.round(z * QUANT)] + +// Client +position.x = p[0] / QUANT; // etc. +``` + +**Trade-off:** Precision ~0.004 block. For player/NPC movement this is fine. For very small objects, may need higher quant (e.g. 1024). + +--- + +### Option B: Quantized Quaternion (Smallest-Three) + +**Idea:** Unit quaternion has `q.x² + q.y² + q.z² + q.w² = 1`. Store the 3 components with largest magnitude; reconstruct 4th. + +- 3 × 2 bytes (quantized) = **6 bytes** vs 4 × 4 = 16 bytes. +- **~62% smaller** for rotation. + +**Implementation:** Standard "smallest three" quaternion compression (e.g. [RigidBodyDynamics](https://github.com/gameworks-builder/rigid-body-dynamics) style). Needs protocol change to support packed format. + +--- + +### Option C: Yaw-Only for Player Rotation + +**Idea:** Many entities (players, NPCs) only rotate around Y. Send 1 float (yaw) instead of 4. + +- **4 bytes** vs 16 bytes. +- **75% smaller** for rotation when applicable. + +**Caveat:** Doesn't work for entities with pitch/roll (e.g. flying, vehicles). Use as opt-in per entity type. + +--- + +### Option D: Delta Encoding (Δ from Last Sent) + +**Idea:** Send `Δp = p - p_last` instead of absolute `p`. Small movements → small deltas → msgpack encodes as smaller integers. + +- No schema change; still `[dx, dy, dz]` but values typically small. +- msgpack variable-length integers: small values use 1 byte. +- **Benefit:** 20–50% smaller when movement is small. No extra state on client if server tracks last-sent. + +**Implementation:** Server stores `_lastSentPosition` per entity per player (or broadcast). Send delta; client adds to last known position. Requires client to track "last applied" position. + +--- + +### Option E: Bulk / AoS Format + +**Idea:** Instead of `[{i:1,p:[x,y,z]},{i:2,p:[x,y,z]},...]` use structure of arrays: + +```ts +{ ids: [1,2,3], p: [[x,y,z],[x,y,z],[x,y,z]] } +``` + +- Avoids repeating keys `i`, `p` for every entity (msgpack dedup helps but structure still has overhead). +- **Benefit:** ~15–25% smaller from less map/array framing. + +**Caveat:** Requires new packet schema and client deserializer changes. All-or-nothing; can't mix with current EntitySchema in same packet. + +--- + +### Option F: Distance-Based Sync Rate + +**Idea:** Sync nearby entities at 30 Hz, distant at 10 Hz or 5 Hz. + +- **Benefit:** Fewer packets for far entities; natural LOD. +- **Implementation:** In `checkAndEmitUpdates` or NetworkSynchronizer, track distance from each player; only queue updates for entity if `tick % rateDivisor === 0` based on distance band. + +--- + +## 3. Recommended Approach + +### Phase 1: Low-Risk Wins (1–2 days each) + +| # | Change | Impact | Effort | +|---|--------|--------|--------| +| 1 | **Quantized position** (1/256 block) | ~50% smaller pos | 1 day | +| 2 | **Distance-based sync rate** (30/15/5 Hz bands) | Fewer far-entity updates | 1 day | +| 3 | **Yaw-only rotation** for player entities | ~75% smaller rot for players | 0.5 day | + +### Phase 2: Schema Changes (3–5 days) + +| # | Change | Impact | Effort | +|---|--------|--------|--------| +| 4 | **Quantized quaternion** (smallest-three) | ~62% smaller rot | 2–3 days | +| 5 | **Bulk entity update packet** | ~15–25% smaller framing | 2 days | + +### Phase 3: Advanced (Optional) + +| # | Change | Impact | Effort | +|---|--------|--------|--------| +| 6 | **Delta encoding** | Additional 20–50% when movement small | 2–3 days | +| 7 | **Client-side prediction** | Reduce perceived latency, fewer corrections | 1+ week | + +--- + +## 4. Protocol Changes Required + +### Option 1: Extend EntitySchema (Backwards Compatible) + +Add optional compressed fields; client detects and uses when present: + +```ts +// New optional fields +EntitySchema = { + i: number; + p?: VectorSchema; // existing: [x,y,z] float + r?: QuaternionSchema; // existing: [x,y,z,w] float + pq?: [number,number,number]; // quantized position (1/256 block) + rq?: [number,number,number]; // quantized quaternion (smallest-three) + ry?: number; // yaw only (radians) + // ... +} +``` + +- Server sends `pq` instead of `p` when quantized format enabled. +- Client checks `pq` first, falls back to `p`. +- Old clients ignore `pq`; new clients prefer `pq` when present. + +### Option 2: New Packet Type + +Add `EntityPosRotBulkPacket`: + +```ts +{ + ids: number[], + positions?: Int16Array | number[][], // quantized + rotations?: number[][] | Int16Array[] // quantized or yaw-only +} +``` + +- Used only for unreliable pos/rot updates. +- Existing `EntitiesPacket` still used for spawn/reliable updates. + +--- + +## 5. Key Files + +| Component | Path | +|-----------|------| +| Entity update emission | `server/src/worlds/entities/Entity.ts` (checkAndEmitUpdates) | +| Player threshold | `server/src/worlds/entities/PlayerEntity.ts` | +| Network sync queue | `server/src/networking/NetworkSynchronizer.ts` | +| Serializer | `server/src/networking/Serializer.ts` | +| Protocol schema | `protocol/schemas/Entity.ts` | +| Client deserializer | `client/src/network/Deserializer.ts` | +| Client entity update | `client/src/entities/EntityManager.ts` (_updateEntity) | +| Transport | `server/src/networking/Connection.ts`, `client/.../NetworkManager.ts` | + +--- + +## 6. Quantization Constants (Suggested) + +```ts +// Position: 1/256 block = 0.0039 block precision +const POSITION_QUANT = 256; + +// Position range: ±32768 blocks (16-bit signed) +// Covers ~1km in each direction +const POSITION_MAX = 32767; +const POSITION_MIN = -32768; + +// Quaternion: 16-bit per component, range [-1, 1] → 1/32767 precision +const QUATERNION_QUANT = 32767; +``` + +--- + +## 7. Success Metrics + +| Metric | Current | Target (Phase 1) | Target (Phase 2) | +|--------|---------|------------------|------------------| +| Entity bytes/update (10 entities) | ~500–650 | ~300–400 | ~200–280 | +| Entity sync % of total packets | ~90% | ~70% | ~50% | +| Bandwidth (20 entities, 30 Hz) | ~15–20 KB/s | ~8–12 KB/s | ~5–8 KB/s | + +--- + +## 8. References + +- [Quaternion Compression (smallest three)](http://gafferongames.com/networked-physics/snapshot-compression/) +- [Minecraft entity sync (delta/quantization)](https://wiki.vg/Protocol#Entity_Metadata) +- Current codebase: `Entity.ts` (checkAndEmitUpdates), `NetworkSynchronizer.ts` (entity sync split), `Serializer.ts` (serializeVector/Quaternion) diff --git a/ai-memory/docs/perf-external-notes-2026-03-05/raw/GREEDY_MESHING_IMPLEMENTATION_GUIDE.md b/ai-memory/docs/perf-external-notes-2026-03-05/raw/GREEDY_MESHING_IMPLEMENTATION_GUIDE.md new file mode 100644 index 00000000..66642683 --- /dev/null +++ b/ai-memory/docs/perf-external-notes-2026-03-05/raw/GREEDY_MESHING_IMPLEMENTATION_GUIDE.md @@ -0,0 +1,182 @@ +# Greedy Meshing Implementation Guide + +**Purpose:** Step-by-step guide for implementing greedy quad merging (cubic/canonical meshing) in Hytopia’s ChunkWorker. +**Audience:** Engineers implementing Phase 4 (Greedy Meshing). +**Prerequisites:** Read [0fps Part 1](https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/) and [Part 2](https://0fps.net/2012/07/07/meshing-minecraft-part-2/). + +--- + +## 1. Algorithm Overview + +### 1.1 Input and Output + +- **Input:** Chunk of 16³ blocks. Each block has type ID, optional rotation. +- **Output:** Merged quads (position, size, normal, block type, AO, light). + +### 1.2 High-Level Steps + +1. **Group by (block type, normal, material flags).** Faces with same texture and normal are mergeable. +2. **For each direction** (±X, ±Y, ±Z): + - Build a 2D slice of visible faces (e.g. for +Y, iterate Y layers; for each layer, collect top faces). + - Run 2D greedy merge: combine adjacent same-type faces into rectangles. +3. **Emit merged quads** with correct UVs, AO, and lighting. + +--- + +## 2. Detailed Algorithm (0fps Style) + +### 2.1 Slice Extraction + +For direction `+Y` (top faces): + +- For each Y level `y = 0..15`: + - For each (x, z) in 16×16: + - If block at (x, y, z) is solid and block at (x, y+1, z) is air/transparent: + - Add face with normal (0, 1, 0), block type = block at (x, y, z). + - This gives a 16×16 grid of “face presence” per block type. + - Run 2D greedy merge on this grid. + +Repeat for −Y, ±X, ±Z. + +### 2.2 2D Greedy Merge (Per Slice, Per Block Type) + +``` +for each row j in slice: + for each column i in slice: + if visited[i,j]: continue + if no face at (i,j): continue + blockType = face at (i,j) + width = 1 + while i+width < 16 and same block at (i+width, j) and same AO/light: + width++ + height = 1 + while j+height < 16: + row OK = true + for k = 0 to width-1: + if different block or visited[i+k, j+height]: row OK = false; break + if !row OK: break + height++ + mark (i,j)..(i+width-1, j+height-1) as visited + emit quad: origin (i,j), size (width, height), blockType +``` + +### 2.3 Lexicographic Order (0fps) + +To get deterministic, visually stable meshes, merge in a fixed order (e.g. top-to-bottom, left-to-right) and prefer the lexicographically smallest representation when multiple merges are possible. + +--- + +## 3. Integration with ChunkWorker + +### 3.1 Current Flow (Simplified) + +``` +for each block in chunk: + for each face (6 directions): + if face visible (neighbor empty/transparent): + emit quad +``` + +### 3.2 New Flow + +``` +// Group 1: Opaque solid blocks (greedy) +for dir in [+X,-X,+Y,-Y,+Z,-Z]: + slice = extractVisibleFaces(chunk, dir) + for blockType in unique block types in slice: + subslice = slice filtered by blockType + quads = greedyMerge2D(subslice, dir) + emit quads with AO, light + +// Group 2: Transparent / special (per-face, existing logic) +for each block in chunk: + if block is transparent or special: + for each face: + if visible: emit quad +``` + +### 3.3 AO and Lighting + +- Ambient occlusion: compute per-vertex AO from neighbor blocks (as today). +- Light: sample from light volume (as today). +- For merged quads: corners may have different AO/light. Options: + - **Option A:** Use min AO/light of the merged region (slightly darker; simpler). + - **Option B:** Subdivide quad where AO/light changes (more quads, better quality). + - **Recommendation:** Start with Option A; optimize later. + +--- + +## 4. Data Structures + +### 4.1 Slice Representation + +```ts +// 16x16 grid, value = block type ID (0 = no face) +type Slice = Uint8Array; // 256 elements + +// Or: (blockTypeId, ao, light) per cell if we merge only when all match +interface SliceCell { + blockTypeId: number; + ao: number; + light: number; +} +``` + +### 4.2 Visited Mask + +```ts +// 16x16 boolean +const visited = new Uint8Array(256); // 1 bit per cell, or just 256 bytes +``` + +### 4.3 Merged Quad Output + +```ts +interface MergedQuad { + x: number; // local origin + y: number; + z: number; + width: number; // in blocks, along one horizontal axis + height: number; // in blocks, along other axis + normal: [number, number, number]; + blockTypeId: number; + ao: number; // or per-corner if subdividing + light: number; +} +``` + +--- + +## 5. Implementation Order + +| Step | Task | Est. Time | +|------|------|-----------| +| 1 | Slice extraction for +Y (top faces) | 1 day | +| 2 | 2D greedy merge for +Y slice | 1 day | +| 3 | Apply to all 6 directions | 0.5 day | +| 4 | AO/light handling for merged quads | 1 day | +| 5 | Integration: replace per-face loop for opaque solids | 1 day | +| 6 | Benchmark: vertex count and build time | 0.5 day | +| 7 | Edge cases: chunk boundaries, multi-type batches | 1 day | + +--- + +## 6. Expected Results + +| Terrain Type | Before (vertices) | After (est.) | Reduction | +|--------------|-------------------|--------------|-----------| +| Flat 16×16 | ~6000 | ~200 | ~30× | +| Hilly | ~8000 | ~800 | ~10× | +| Caves | ~4000 | ~600 | ~7× | +| Mixed | ~6000 | ~500 | ~12× | + +Build time may increase by 10–30% due to extra passes; vertex reduction should yield net FPS gain. + +--- + +## 7. References + +- [0fps Part 1 – Meshing in a Minecraft Game](https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/) +- [0fps Part 2 – Multiple block types](https://0fps.net/2012/07/07/meshing-minecraft-part-2/) +- [mikolalysenko/greedy-mesher](https://github.com/mikolalysenko/greedy-mesher) (JavaScript reference) +- [Vercidium greedy voxel meshing gist](https://gist.github.com/Vercidium/a3002bd083cce2bc854c9ff8f0118d33) diff --git a/ai-memory/docs/perf-external-notes-2026-03-05/raw/MAP_ENGINE_ARCHITECTURE.md b/ai-memory/docs/perf-external-notes-2026-03-05/raw/MAP_ENGINE_ARCHITECTURE.md new file mode 100644 index 00000000..f58f543a --- /dev/null +++ b/ai-memory/docs/perf-external-notes-2026-03-05/raw/MAP_ENGINE_ARCHITECTURE.md @@ -0,0 +1,272 @@ +# Hytopia Map Engine Architecture + +This document describes how the Hytopia map engine is set up, its data flow, and a roadmap for adapting it to support **binary maps** for extremely large worlds (e.g., 100k×100k×64 blocks). + +--- + +## 1. Architecture Overview + +The map engine spans **server** (authoritative block state), **client** (rendering, meshing), and **protocol** (network serialization). Maps are loaded once at world initialization and populate a chunk-based block lattice. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ MAP LOAD PIPELINE │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ JSON Map File World.loadMap() ChunkLattice │ +│ (blockTypes, blocks, ───────────────► initializeBlockEntries() │ +│ entities) │ │ │ +│ │ │ ▼ │ +│ │ │ ChunkLattice clears, │ +│ │ │ creates Chunks, │ +│ │ │ builds colliders │ +│ │ │ │ │ +│ │ ▼ ▼ │ +│ │ BlockTypeRegistry Map │ +│ │ (block types) (sparse chunks) │ +│ │ │ │ +│ │ ▼ │ +│ │ NetworkSynchronizer │ +│ │ (chunk sync to │ +│ │ clients) │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 2. WorldMap Interface (JSON Format) + +Maps conform to the `WorldMap` interface used by `World.loadMap()`: + +| Section | Purpose | Location | +|---------------|--------------------------------------------------------|-------------------------------| +| `blockTypes` | Block type definitions (id, name, textureUri, etc.) | `server/src/worlds/World.ts` | +| `blocks` | Block placements keyed by `"x,y,z"` string | `WorldMap.blocks` | +| `entities` | Entity spawns keyed by `"x,y,z"` position | `WorldMap.entities` | + +### Block Format in JSON + +Each block entry is either: + +- **Short form:** `"x,y,z": ` (e.g. `"-25,0,-16": 7`) +- **Extended form:** `"x,y,z": { "i": , "r": }` + +Coordinates are **world block coordinates** (integers). Block type IDs are 0–255 (0 = air, 1–255 = registered block types). + +### Size Implications of JSON Maps + +| Factor | Impact | +|---------------------------|-----------------------------------------------------------------------| +| Sparse object keys | Each block = `"x,y,z"` string key (10–20+ chars) + JSON overhead | +| No chunk-level batching | All blocks listed individually; no spatial grouping | +| Parsing cost | Full JSON parse loads entire map into memory before processing | +| File size | `boilerplate-small.json` ≈ 4,600+ lines; `big-world` ≈ 309,000+ lines | + +For a **100k×100k×64** fully dense map: + +- Blocks: 640 billion +- JSON would be impractically huge (hundreds of GB+ as text) +- Even sparse terrain would produce multi-GB JSON for large worlds + +--- + +## 3. Chunk Model + +### Chunk Dimensions + +| Constant | Value | Location | +|----------------|-------|--------------------------------------| +| `CHUNK_SIZE` | 16 | `server/src/worlds/blocks/Chunk.ts` | +| `CHUNK_VOLUME` | 4096 | 16³ blocks per chunk | +| `MAX_BLOCK_TYPE_ID` | 255 | `Chunk.ts` | + +Chunk origins are multiples of 16 on each axis (e.g. `(0,0,0)`, `(16,0,0)`, `(0,16,0)`). + +### Chunk Storage + +- **`Chunk._blocks`:** `Uint8Array(4096)` – block type ID per voxel +- **`Chunk._blockRotations`:** `Map` – sparse map of block index → rotation +- **Block index:** `x + (y << 4) + (z << 8)` (local coords 0–15) + +Chunks are stored in `ChunkLattice._chunks` as `Map` keyed by packed chunk origin: + +```typescript +// ChunkLattice._packCoordinate() – 54 bits per axis +chunkKey = (x << 108) | (y << 54) | z +``` + +--- + +## 4. Load Flow: `World.loadMap()` + +```typescript +// server/src/worlds/World.ts +public loadMap(map: WorldMap) { + this.chunkLattice.clear(); + + // 1. Register block types + if (map.blockTypes) { + for (const blockTypeData of map.blockTypes) { + this.blockTypeRegistry.registerGenericBlockType({ ... }); + } + } + + // 2. Iterate blocks as generator, feed to ChunkLattice + if (map.blocks) { + const blockEntries = function* () { + for (const key in mapBlocks) { + const blockValue = mapBlocks[key]; + const blockTypeId = typeof blockValue === 'number' ? blockValue : blockValue.i; + const blockRotationIndex = typeof blockValue === 'number' ? undefined : blockValue.r; + const [x, y, z] = key.split(',').map(Number); + yield { globalCoordinate: { x, y, z }, blockTypeId, blockRotation }; + } + }; + this.chunkLattice.initializeBlockEntries(blockEntries()); + } + + // 3. Spawn entities + if (map.entities) { ... } +} +``` + +### `ChunkLattice.initializeBlockEntries()` + +- Clears the lattice +- For each block: resolves chunk, creates chunk if needed, calls `chunk.setBlock()` +- Tracks block placements per type for colliders +- After all blocks: builds one collider per block type (voxel or trimesh) + +--- + +## 5. Client-Server Chunk Sync + +Chunks are serialized and sent to clients via `NetworkSynchronizer`: + +| Protocol Field | Description | +|----------------|--------------------------------------| +| `c` | Chunk origin `[x, y, z]` | +| `b` | Block IDs `Uint8Array \| number[]` (4096) | +| `r` | Rotations: flat `[blockIndex, rotIndex, ...]` | +| `rm` | Chunk removed flag | + +- **Serializer:** `Serializer.serializeChunk()` → `protocol.ChunkSchema` +- **Client:** `Deserializer.deserializeChunk()` → `DeserializedChunk` +- **ChunkWorker:** Receives `chunk_update`, registers chunk, builds meshes + +The client does **not** load the JSON map. It receives chunks from the server over the network after a player joins a world. + +--- + +## 6. Key Files Reference + +| Component | Path | +|----------------------|--------------------------------------------------| +| WorldMap interface | `server/src/worlds/World.ts` | +| loadMap | `server/src/worlds/World.ts` | +| ChunkLattice | `server/src/worlds/blocks/ChunkLattice.ts` | +| Chunk | `server/src/worlds/blocks/Chunk.ts` | +| ChunkSchema (proto) | `protocol/schemas/Chunk.ts` | +| Serializer | `server/src/networking/Serializer.ts` | +| ChunkWorker (client) | `client/src/workers/ChunkWorker.ts` | +| Deserializer | `client/src/network/Deserializer.ts` | + +--- + +## 7. Binary Map Adaptation Roadmap for 100k×100k×64 + +To support huge maps efficiently, the engine should move from JSON to **binary map sources** with **chunk-level loading** and **streaming**. + +### 7.1 Binary Chunk Format (Proposed) + +Store one file or region per chunk (or region of chunks): + +``` +chunk.{cx}.{cy}.{cz}.bin OR region.{rx}.{ry}.{rz}.bin +``` + +**Suggested layout per chunk (raw):** + +| Offset | Size | Content | +|--------|--------|------------------------------------------| +| 0 | 12 | Origin (3× int32: x, y, z) | +| 12 | 4096 | Block IDs (Uint8Array) | +| 4108 | var | Sparse rotations: count + [idx, rot]... | + +Or use a compact format (e.g. run-length encoding for air, or palette indices) for sparse chunks. + +### 7.2 Streaming / Lazy Loading + +- **Do not** load the entire map into memory. +- Use a **chunk provider** that: + - Accepts `(chunkOriginX, chunkOriginY, chunkOriginZ)` and returns chunk data + - Reads from binary files, memory-mapped files, or a database +- Replace the current `loadMap()` bulk load with: + - Initial load of a small seed area (e.g. spawn region) + - On-demand loading when `ChunkLattice.getOrCreateChunk()` needs a chunk not yet in memory + +### 7.3 Implementation Strategy + +1. **`MapProvider` interface** + ```typescript + interface MapProvider { + getChunk(origin: Vector3Like): ChunkData | null | Promise; + getBlockTypes(): BlockTypeOptions[]; + } + ``` + +2. **`BinaryMapProvider`** + - Reads `.bin` chunk files from disk or object storage + - Maps chunk origin → file path or byte range + - Returns `{ blocks: Uint8Array, rotations: Map }` + +3. **ChunkLattice changes** + - Replace `initializeBlockEntries()` full load with lazy `getOrCreateChunk()` that: + - Checks `_chunks` cache + - If miss: calls `MapProvider.getChunk()`, creates `Chunk`, inserts into `_chunks` + - Optionally preload chunks in a radius around player(s) + +4. **Block types** + - Keep block types in a small JSON or separate binary; they are tiny compared to block data. + - Load once at startup; no need to stream. + +### 7.4 Scale Estimates for 100k×100k×64 + +| Metric | Value | +|---------------------------|--------------------------| +| World dimensions | 100,000 × 100,000 × 64 | +| Chunks (16³) | 6,250 × 6,250 × 4 ≈ 156M chunks | +| Bytes per chunk (raw) | ~4.1 KB (blocks only) | +| Raw block data (if dense) | ~640 GB | +| Sparse (e.g. surface) | Much less; only store non-air chunks | + +Binary format advantages: + +- No JSON parsing; direct `Uint8Array` use +- Chunk-level I/O; load only what’s needed +- Possible memory-mapping for large files +- Optional compression (e.g. LZ4, Zstd) per chunk or region + +### 7.5 Migration Path + +1. **Phase 1:** Add `BinaryMapProvider` that reads chunk `.bin` files; `loadMap()` can accept `WorldMap | MapProvider`. +2. **Phase 2:** Make `ChunkLattice.getOrCreateChunk()` use the provider when a chunk is missing. +3. **Phase 3:** Add tooling to convert existing JSON maps → binary chunk files. +4. **Phase 4:** Optional region/compression format for production. + +--- + +## 8. Summary + +| Current (JSON) | Target (Binary + Streaming) | +|----------------------------|----------------------------------| +| Full map in memory | Chunk-level loading | +| Single large JSON parse | Small reads per chunk | +| Sparse object keys | Dense `Uint8Array` per chunk | +| Not viable for 100k³ scale | Designed for huge worlds | + +The existing `Chunk` and `ChunkLattice` design already matches a chunk-oriented model. The main changes are: + +1. Replace JSON as the map source with a binary chunk provider. +2. Add lazy loading so chunks are fetched on demand. +3. Provide conversion tools and a clear binary chunk layout. diff --git a/ai-memory/docs/perf-external-notes-2026-03-05/raw/MINECRAFT_ARCHITECTURE_RESEARCH.md b/ai-memory/docs/perf-external-notes-2026-03-05/raw/MINECRAFT_ARCHITECTURE_RESEARCH.md new file mode 100644 index 00000000..c7280c72 --- /dev/null +++ b/ai-memory/docs/perf-external-notes-2026-03-05/raw/MINECRAFT_ARCHITECTURE_RESEARCH.md @@ -0,0 +1,161 @@ +# Minecraft Architecture Research + +**Purpose:** Inform Hytopia’s voxel engine design with lessons from Minecraft Java and Bedrock. +**Audience:** Engineers implementing chunk loading, colliders, and meshing. +**Sources:** Technical wikis, decompilations, community analysis, engine talks. + +--- + +## 1. Chunk System Overview + +### 1.1 Chunk Structure + +| Version | Chunk Size | Subchunk | Notes | +|---------|------------|----------|-------| +| Java | 16×256×16 (XZ columns) | 16×16×16 sections | Vertical column; sections loaded independently | +| Bedrock | 16×256×16 | 16×16×16 | Similar; different storage layout | + +**Hytopia:** 16×16×16 chunks, 2×2×2 batches (32³). Aligns with common practice. + +### 1.2 Loading States (Java 1.14+) + +Minecraft separates chunk lifecycle into distinct states: + +| State | Purpose | +|-------|---------| +| **Empty** | Not loaded | +| **Structure** | Structures placed | +| **Noise** | Terrain generated | +| **Surface** | Surface blocks, biomes | +| **Carvers** | Caves, ravines | +| **Features** | Trees, ores, etc. | +| **Entity ticking** | Physics, entities, block updates | + +**Key insight:** Entity ticking requires a 5×5 grid of loaded chunks around the center chunk. Border chunks can be “lazy” (block updates only, no entities). This **spatial locality** keeps entity/physics work bounded. + +**Hytopia takeaway:** Only tick entities and step physics for chunks near players. Don’t pay for distant chunks. + +### 1.3 Spawn Chunks + +- 19×19 chunks (Java) or 23×23 (Bedrock) always loaded around spawn. +- Only center ~12×12 process entities. +- Reduces load/unload churn at spawn. + +**Hytopia:** Preload radius already exists; consider an “always loaded” spawn core for hubs. + +--- + +## 2. File I/O and Region Format + +### 2.1 Region Files + +- One file per 32×32 chunk region (XZ). +- Anvil format: 4 KB header (1024 entries × 4 bytes) + chunk payloads. +- Chunks stored with length prefix + compression (typically zlib; Bedrock uses different schemes). +- **Async I/O:** Modern implementations use background threads; main thread never blocks on disk. + +### 2.2 Chunk Serialization + +- Block IDs, block states, light, heightmap, biomes stored per chunk. +- Compression reduces size by ~90% for typical terrain. + +**Hytopia:** Region format exists; `readChunkAsync` and `writeChunk` (sync) are in place. Priority: make persist async. + +--- + +## 3. Terrain Generation + +### 3.1 Worker Pool + +- Terrain generation runs in worker threads. +- Main thread requests chunk; worker generates; result returned asynchronously. +- Multiple workers allow parallelism. + +### 3.2 Generation Stages + +- Noise → carvers → features (trees, ores). +- Each stage can be parallelized or deferred. + +**Hytopia:** `TerrainWorkerPool` + `generateChunkAsync` exist. Ensure `requestChunk` uses this path and doesn’t fall back to sync. + +--- + +## 4. Physics and Collision + +### 4.1 Chunk-Section Colliders + +- Collision is built per 16×16×16 section. +- Sections far from players may not have colliders at all, or use simplified shapes. +- Colliders are created/updated in batches, not all at once. + +### 4.2 Spatial Partitioning + +- Physics world uses spatial partitioning (e.g. broadphase). +- Entity vs. block collision: only check nearby chunks. +- No global scan over entire world. + +**Hytopia gap:** `_combineVoxelStates` iterates all chunks of a block type. Must restrict to nearby chunks. + +--- + +## 5. Meshing and Rendering + +### 5.1 Greedy Meshing (Ambient Occlusion) + +- Minecraft uses an approximation of greedy meshing (block model merging). +- Adjacent faces of same block type are merged into larger quads where possible. +- Results in 2–64× fewer quads than per-face rendering. + +### 5.2 Occlusion Culling + +- Section-level visibility: if a section is fully behind solid terrain, skip rendering. +- BFS from camera through air/transparent blocks; mark visible sections. +- ~10–15% frame time savings in cave-heavy areas. + +### 5.3 LOD + +- Distant chunks use lower-detail meshes or impostors. +- Reduces overdraw and vertex count. + +**Hytopia:** Face culling ✅; greedy meshing ❌; occlusion partial; LOD step 2/4. Biggest win: greedy meshing. + +--- + +## 6. Network + +### 6.1 Chunk Packets + +- Chunks sent incrementally; rate-limited to avoid client flood. +- Delta updates for modified chunks (block changes) vs. full chunk for new loads. + +### 6.2 Entity Sync + +- Position/rotation use compact encodings (fixed-point or quantized). +- Entities use delta or relative positioning where possible. +- Distant entities may sync at lower rate. + +**Source:** [Minecraft Protocol (wiki.vg)](https://wiki.vg/Protocol#Entity_Metadata) + +--- + +## 7. Lessons for Hytopia + +| Minecraft Pattern | Hytopia Status | Action | +|-------------------|----------------|--------| +| Async chunk load | ✅ `requestChunk` + `getChunkAsync` | Verify usage | +| Async I/O | ✅ `readChunkAsync` | Make persist async | +| Worker terrain gen | ✅ TerrainWorkerPool | Verify | +| Collider locality | ❌ O(world) scans | Phase 1: spatial index, scoped merge | +| Greedy meshing | ❌ | Phase 4 | +| Occlusion | ⚠️ Partial | Phase 5 | +| Entity quantization | ❌ | Phase 3 | +| Distance-based sync | ❌ | Phase 3 | + +--- + +## References + +- [Chunk Loading – Technical Minecraft Wiki](https://techmcdocs.github.io/pages/GameMechanics/ChunkLoading/) +- [Minecraft Protocol – wiki.vg](https://wiki.vg/Protocol) +- [0fps Meshing in a Minecraft Game](https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/) +- [Fabric Modding Documentation (chunk loading states)](https://fabricmc.net/wiki/) diff --git a/ai-memory/docs/perf-external-notes-2026-03-05/raw/NETWORK_PROTOCOL_2026_RESEARCH.md b/ai-memory/docs/perf-external-notes-2026-03-05/raw/NETWORK_PROTOCOL_2026_RESEARCH.md new file mode 100644 index 00000000..722d42a8 --- /dev/null +++ b/ai-memory/docs/perf-external-notes-2026-03-05/raw/NETWORK_PROTOCOL_2026_RESEARCH.md @@ -0,0 +1,130 @@ +# Network Protocol 2026 Research + +**Purpose:** Modern entity sync and chunk sync patterns for low-bandwidth, low-latency voxel multiplayer. +**Audience:** Engineers implementing Phase 3 (Entity Sync Compression). + +--- + +## 1. Entity Sync: Industry Patterns + +### 1.1 Minecraft (Java) + +- Entity position/rotation sent as fixed-point or scaled integers. +- Metadata uses compact type tags. +- Delta updates for moving entities; full state on spawn or major change. + +### 1.2 Source Engine / Garry’s Mod + +- **Delta compression:** Send only changed fields; baseline is last full update. +- **Quantization:** Position in 1/16 or 1/32 unit; angles in 16-bit. + +### 1.3 Overwatch / Modern FPS + +- Client-side prediction + server reconciliation. +- Entity updates at 20–60 Hz for nearby; lower for distant. +- Snapshot compression: delta from previous snapshot. + +### 1.4 Gaffer On Games (Networked Physics) + +- [Snapshot Compression](http://gafferongames.com/networked-physics/snapshot-compression/) +- Quaternion: store 3 largest components (smallest-three); 4th derived. +- Position: fixed-point or quantized. +- Delta encoding: send difference from last acked state. + +--- + +## 2. Quantization Formulas + +### 2.1 Position (Fixed-Point) + +```ts +const QUANT = 256; // 1/256 block = 0.0039 block precision +const clamp = (v: number) => Math.max(-32768, Math.min(32767, Math.round(v * QUANT))); + +// Encode +pq: [clamp(x), clamp(y), clamp(z)] // Int16Array or [number, number, number] + +// Decode +x = pq[0] / QUANT; +``` + +**Range:** ±32768 blocks ≈ ±524 km. More than enough. + +### 2.2 Quaternion (Smallest-Three) + +- Unit quaternion: `q.x² + q.y² + q.z² + q.w² = 1`. +- One component can be derived from the other three. +- Store the 3 components with largest magnitude; 1 byte for index of omitted component. +- Quantize each stored component to 16-bit: `value * 32767` for range [-1, 1]. + +**Size:** 1 + 3×2 = 7 bytes vs 4×4 = 16 bytes (float32). ~56% smaller. + +**Reference:** [Gaffer On Games](http://gafferongames.com/networked-physics/snapshot-compression/) + +### 2.3 Yaw-Only (Euler) + +- For entities that only rotate around Y: send 1 float (radians) or 16-bit quantized. +- `yaw = 2*PI * (int16 / 65536)`. +- 2 bytes vs 16 bytes for full quaternion. + +--- + +## 3. Distance-Based Sync Rate + +| Distance Band | Sync Rate | Use Case | +|---------------|-----------|----------| +| 0–4 chunks | 30 Hz | Player, nearby NPCs | +| 4–8 chunks | 15 Hz | Mid-range entities | +| 8+ chunks | 5 Hz | Far entities, environmental | + +**Implementation:** In `checkAndEmitUpdates` or NetworkSynchronizer, compute distance from nearest player; only emit if `tick % rateDivisor === 0`. + +--- + +## 4. Bulk Format (Structure of Arrays) + +Instead of: + +```json +[ + { "i": 1, "p": [10.5, 20.1, 30.2] }, + { "i": 2, "p": [11.2, 20.0, 31.1] } +] +``` + +Use: + +```json +{ + "ids": [1, 2], + "p": [[2693, 5146, 7733], [2867, 5120, 7962]] +} +``` + +- Quantized positions in `p` (Int16). +- Avoids repeating keys; msgpack benefits from smaller maps. +- **Caveat:** New packet type; client must support. Can run parallel to existing EntitiesPacket during migration. + +--- + +## 5. Protocol Versioning + +- Add optional fields to EntitySchema: `pq`, `rq`, `ry`. +- Old clients ignore unknown fields; new clients prefer them. +- Server flag: `useQuantizedEntitySync=true` (default for new connections after version bump). + +--- + +## 6. Chunk Delta Updates (Phase 6) + +- When a single block changes, send delta: `{ chunkId, blockIndex, blockTypeId }` instead of full chunk. +- Client applies delta to local chunk; requests full chunk if out of sync. +- Reduces bandwidth for frequent block edits (mining, building). + +--- + +## 7. References + +- [Gaffer On Games – Snapshot Compression](http://gafferongames.com/networked-physics/snapshot-compression/) +- [Minecraft Protocol – wiki.vg](https://wiki.vg/Protocol) +- [ENTITY_SYNC_DELTA_COMPRESSION_DESIGN.md](../ENTITY_SYNC_DELTA_COMPRESSION_DESIGN.md) – Hytopia-specific design diff --git a/ai-memory/docs/perf-external-notes-2026-03-05/raw/SMOOTH_WORLD_STREAMING_REFACTOR_PLAN.md b/ai-memory/docs/perf-external-notes-2026-03-05/raw/SMOOTH_WORLD_STREAMING_REFACTOR_PLAN.md new file mode 100644 index 00000000..cead4812 --- /dev/null +++ b/ai-memory/docs/perf-external-notes-2026-03-05/raw/SMOOTH_WORLD_STREAMING_REFACTOR_PLAN.md @@ -0,0 +1,180 @@ +# Smooth World Streaming Refactor Plan + +> **Canonical roadmap:** See [VOXEL_ENGINE_2026_MASTER_PLAN.md](./VOXEL_ENGINE_2026_MASTER_PLAN.md) for the full executive plan and phased roadmap. This document provides additional context and cross-references. + +**Goal:** Peak performance for the procedurally generated world—smooth streaming, no lag spikes, Minecraft/Hytale/bloxd-level polish. + +**Sources:** Codebase analysis, [VOXEL_PERFORMANCE_MASTER_PLAN.md](./VOXEL_PERFORMANCE_MASTER_PLAN.md), [CHUNK_LOADING_ARCHITECTURE.md](./CHUNK_LOADING_ARCHITECTURE.md), [VOXEL_RENDERING_RESEARCH.md](./VOXEL_RENDERING_RESEARCH.md), [PR #21](https://github.com/hytopiagg/hytopia-source/pull/21), and industry patterns from Minecraft, Hytale, and Bloxd. + +--- + +## 1. Competitive Analysis: Minecraft vs Hytale vs Bloxd vs Hytopia + +| Aspect | Minecraft | Hytale | Bloxd | Hytopia (Current) | +|--------|-----------|--------|-------|-------------------| +| **Chunk load** | Worker threads, async | Worker pool | JS async | ✅ `requestChunk` + `getChunkAsync` (TerrainWorkerPool) | +| **File I/O** | Async | Async | N/A (streaming) | ✅ `readChunkAsync` (PersistenceChunkProvider) | +| **Terrain gen** | Worker threads | Worker pool | — | ✅ `generateChunkAsync` (TerrainWorkerPool) | +| **Physics colliders** | Deferred, O(chunk) | Batched, spatial | Custom voxel | ❌ Sync, O(world) via `_combineVoxelStates` | +| **Collider locality** | Per-chunk, near player | Spatial culling | — | ⚠️ Partial (COLLIDER_MAX_CHUNK_DISTANCE=3) | +| **Greedy meshing** | ✅ | ✅ (mesh culling) | ✅ | ❌ 1 quad/face, ~64× extra geometry | +| **Chunk send rate** | Incremental, rate-limited | Batched | Streaming | ⚠️ MAX_CHUNKS_PER_SYNC=8, can burst | +| **Entity sync** | Delta / compressed | — | — | Full pos/rot 30 Hz, 90%+ of packets | +| **LOD** | ✅ | Variable chunk sizes | — | ✅ (step 2/4) | +| **Occlusion** | Cave culling | Partial | — | ⚠️ Only when over face limit | +| **Vertex pooling** | — | — | ✅ | ⚠️ Partial (size-match reuse) | +| **Map compression** | Region format | — | — | ❌ JSON maps large; PR #21 adds compression | + +**Gap summary:** Hytopia’s biggest gaps are (1) collider work O(world) and sync, (2) no greedy meshing, (3) entity sync volume, (4) JSON map size for non-procedural games. Procedural world already uses async load + worker terrain gen; collider and client-side mesh work are the main bottlenecks. + +--- + +## 2. PR #21 Relevance to Procedural World + +[PR #21: Compressed world maps](https://github.com/hytopiagg/hytopia-source/pull/21) targets **JSON maps** (`loadMap(map.json)`), not procedural/region worlds. It adds: + +| Feature | Applies to Procedural? | Notes | +|---------|------------------------|-------| +| `map.compressed.json` | ❌ | JSON map format only | +| `map.chunks.bin` (chunk cache) | ❌ | Prebaked JSON map chunks | +| Chunk cache collider build | ⚠️ Partially | “perf: speed up chunk cache collider build” can inform collider design | +| Brotli compression | ❌ | For map JSON, not region .bin | +| Auto-detect / `hytopia map-compress` | ❌ | JSON map workflow | + +**Recommendation:** Merge PR #21 for JSON-map games (huntcraft, boilerplate, etc.). For procedural world, reuse the collider build approach where relevant. Procedural persistence uses region `.bin`; consider Brotli for region payloads later. + +--- + +## 3. Root Cause Summary + +When a player joins and blocks have physics: + +1. **Physics step (60 Hz):** Rapier steps the entire world, including all block colliders + player rigid body. +2. **Collider creation:** `_addChunkBlocksToColliders` → `_combineVoxelStates` scans all chunks of each block type (O(world)). +3. **Entity sync (30 Hz):** Full position/rotation for entities/players every 2 ticks; dominates packet volume. +4. **Chunk sync:** Up to 8 chunks per sync; client mesh build can spike main thread. +5. **Client mesh:** No greedy meshing → 2–64× more vertices than needed. +6. **ADD_CHUNK events:** Environmental entity spawn per chunk runs synchronously. + +--- + +## 4. Refactoring Plan (Prioritized) + +### Phase 1: Stop the Bleeding (1–2 weeks) + +| # | Task | Impact | Effort | Files | +|---|------|--------|--------|-------| +| 1.1 | **Collider locality – spatial index** | High | 3–5 days | `ChunkLattice.ts` | +| 1.2 | **Scoped `_combineVoxelStates`** | High | 2–3 days | `ChunkLattice.ts` | +| 1.3 | **Time-budget collider processing** | Medium | ✅ Done | `playground.ts` | +| 1.4 | **CHUNKS_PER_TICK = 3** | ✅ Done | — | `playground.ts` | +| 1.5 | **Defer environmental entity spawn** | Medium | 1 day | `playground.ts` | + +**1.1–1.2:** Replace global scans with spatial indexing. `_getBlockTypePlacements` and `_combineVoxelStates` should only consider chunks within a radius (e.g. 4–5 chunks) of any player. Add a spatial index (e.g. chunk key → block placements) and only merge voxel state for nearby chunks. + +### Phase 2: Main Thread Freedom (2–3 weeks) + +| # | Task | Impact | Effort | Files | +|---|------|--------|--------|-------| +| 2.1 | **Async persistChunk** | Medium | 1–2 days | `PersistenceChunkProvider.ts`, `RegionFileFormat.ts` | +| 2.2 | **Worker terrain gen verification** | — | 0.5 day | `TerrainWorkerPool.ts`, `ProceduralChunkProvider.ts` | +| 2.3 | **Incremental voxel collider updates** | High | 3–5 days | `ChunkLattice.ts` | +| 2.4 | **Chunk send pacing** | Medium | 1–2 days | `NetworkSynchronizer.ts` | + +**2.1:** `persistChunk` currently calls `writeChunk` (sync). Move to async; queue writes and process in background. + +**2.3:** Add blocks to voxel colliders in batches (e.g. 256–512/tick) instead of full chunk. Use Rapier voxel API if it supports incremental updates. + +### Phase 3: Network & Sync (2–3 weeks) + +| # | Task | Impact | Effort | Files | +|---|------|--------|--------|-------| +| 3.1 | **Entity delta/compression** | High | 5–7 days | `NetworkSynchronizer.ts`, `Serializer.ts`, protocol | +| 3.2 | **Chunk delta updates** | Medium | 3–4 days | `NetworkSynchronizer.ts`, `ChunkLattice` | +| 3.3 | **Predictive chunk preload** | Medium | 2–3 days | `playground.ts` | + +**3.1:** Send position/rotation deltas or use quantized floats. Reference: Minecraft’s entity compression, Hytale’s QUIC usage. + +### Phase 4: Client Render Pipeline (3–4 weeks) + +| # | Task | Impact | Effort | Files | +|---|------|--------|--------|-------| +| 4.1 | **Greedy meshing (quad merging)** | Very high | 5–7 days | `ChunkWorker.ts` | +| 4.2 | **Vertex pooling** | Medium | 2–3 days | `ChunkMeshManager.ts`, `ChunkWorker.ts` | +| 4.3 | **Occlusion culling always-on** | Medium | 2–3 days | `ChunkManager.ts`, `Renderer.ts` | +| 4.4 | **Mesh apply budget** | Low | 1 day | `ChunkManager.ts` | + +**4.1:** Implement 0fps-style greedy meshing for opaque solids. Merge adjacent same-type faces; expect 2–64× fewer vertices. References: [0fps](https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/), [mikolalysenko/greedy-mesher](https://github.com/mikolalysenko/greedy-mesher). + +### Phase 5: Long-Term & Polish (ongoing) + +| # | Task | Impact | Effort | +|---|------|--------|--------| +| 5.1 | LOD impostors for distant chunks | Medium | 2–3 weeks | +| 5.2 | Brotli for region .bin payloads | Low | 1 week | +| 5.3 | Block/face limits (safety cap) | Low | <1 day | +| 5.4 | Profiling hooks (tick, chunk, mesh) | Low | 2–3 days | + +--- + +## 5. Implementation Order + +``` +Week 1–2: Phase 1 (collider locality, scoped _combineVoxelStates, defer env spawn) +Week 3–4: Phase 2 (async persistChunk, incremental voxel, chunk send pacing) +Week 5–6: Phase 3 (entity delta, chunk delta, predictive preload) +Week 7–10: Phase 4 (greedy meshing, vertex pooling, occlusion) +Ongoing: Phase 5 +``` + +--- + +## 6. Success Metrics + +| Metric | Current (Est.) | Target | +|--------|----------------|--------| +| Lag spikes when walking | Every ~5 steps | None within preload radius | +| Server tick time (p99) | 50–200 ms | < 16 ms | +| Chunk load (blocking) | 20–100 ms | < 5 ms (async) | +| Vertices per flat chunk | ~6000 | ~200–500 (greedy) | +| Client frame time | Spikes on new chunks | Stable ~16 ms (60 fps) | +| Entity packet share | ~90% | < 50% (delta/compression) | + +--- + +## 7. Key Files Reference + +| Component | Path | +|-----------|------| +| Chunk load loop | `server/src/playground.ts` | +| Collider processing | `server/src/worlds/blocks/ChunkLattice.ts` | +| Physics simulation | `server/src/worlds/physics/Simulation.ts` | +| Mesh generation | `client/src/workers/ChunkWorker.ts` | +| Chunk sync | `server/src/networking/NetworkSynchronizer.ts` | +| Region I/O | `server/src/worlds/maps/RegionFileFormat.ts` | +| Terrain gen | `server/src/worlds/maps/TerrainGenerator.ts`, `TerrainWorkerPool.ts` | +| Procedural provider | `server/src/worlds/maps/ProceduralChunkProvider.ts` | +| Persistence provider | `server/src/worlds/maps/PersistenceChunkProvider.ts` | +| World loop | `server/src/worlds/WorldLoop.ts` | + +--- + +## 8. PR #21 Action Items + +1. **Merge PR #21** for JSON-map games (boilerplate, huntcraft, etc.). +2. **Reuse chunk cache collider patterns** in `ChunkLattice` if applicable. +3. **Later:** Consider Brotli for region payloads or a similar compression layer. + +--- + +## 9. References + +- [VOXEL_PERFORMANCE_MASTER_PLAN.md](./VOXEL_PERFORMANCE_MASTER_PLAN.md) +- [CHUNK_LOADING_ARCHITECTURE.md](./CHUNK_LOADING_ARCHITECTURE.md) +- [VOXEL_RENDERING_RESEARCH.md](./VOXEL_RENDERING_RESEARCH.md) +- [OPTIMIZATION_STRATEGY.md](./OPTIMIZATION_STRATEGY.md) +- [PR #21 – Compressed world maps](https://github.com/hytopiagg/hytopia-source/pull/21) +- [0fps Greedy Meshing](https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/) +- [mikolalysenko/greedy-mesher](https://github.com/mikolalysenko/greedy-mesher) +- [Minecraft Chunk Loading (Technical Wiki)](https://techmcdocs.github.io/pages/GameMechanics/ChunkLoading/) +- [Hytale Engine Technical Deep Dive](https://hytalecharts.com/news/hytale-engine-technical-deep-dive) diff --git a/ai-memory/docs/perf-external-notes-2026-03-05/raw/VOXEL_ENGINE_2026_MASTER_PLAN.md b/ai-memory/docs/perf-external-notes-2026-03-05/raw/VOXEL_ENGINE_2026_MASTER_PLAN.md new file mode 100644 index 00000000..c74ee120 --- /dev/null +++ b/ai-memory/docs/perf-external-notes-2026-03-05/raw/VOXEL_ENGINE_2026_MASTER_PLAN.md @@ -0,0 +1,218 @@ +# Voxel Engine 2026: World-Class Performance Master Plan + +**Document Owner:** Head of Development +**Classification:** Engineering Roadmap +**Target:** Minecraft/Hytale-grade smoothness; browser-first, 2026-ready +**Version:** 1.0 +**Date:** March 2026 + +--- + +## Executive Summary + +Hytopia aims to deliver voxel gameplay that feels as smooth and responsive as Minecraft and Hytale, while running in the browser. The current architecture has solid foundations—async chunk loading, worker terrain generation, deferred colliders—but several bottlenecks prevent parity with industry leaders. This plan addresses those gaps with a phased, research-backed approach that delivers measurable improvements without over-engineering. + +**Key thesis:** The lag and stutter are almost entirely **software architecture** issues, not hardware. Minecraft and Hytale run smoothly on similar hardware because they use different patterns. We close the gap by adopting those patterns. + +**Target outcome:** Walk/fly through a procedural world with **no perceptible lag spikes** within the preload radius, **stable 60 FPS** on the client, and **<16 ms server tick times** (p99). + +--- + +## Part 1: Strategic Context + +### 1.1 Industry Benchmark: What “On Par” Means + +| Game | Chunk Load | Physics | Rendering | Network | Notes | +|------|------------|---------|-----------|---------|-------| +| **Minecraft Java** | Worker threads, region format | Per-chunk colliders, deferred | Greedy meshing (approximate), occlusion | Delta/delta-like entity sync | 15+ years of iteration | +| **Minecraft Bedrock** | Async pipeline, priority queue | Spatial partitioning | Meshing + LOD | Variable tick rate by distance | C++ / C#; mobile-first | +| **Hytale** | Worker pool, variable chunk sizes | Batched, spatial | Mesh culling, LOD | QUIC, lower latency | Modern engine, Flecs ECS | +| **Bloxd.io** | Browser streaming | Custom voxel physics | Face culling, vertex pooling | JS-based | Browser-only | + +**Hytopia’s position:** We are browser-bound (Node server + Web client). We can’t use C++ or multiple cores on the client, but we *can* adopt the same *concepts*: async I/O, spatial locality, greedy meshing, quantized network formats, and time-budgeted main-thread work. + +### 1.2 Gap Analysis (Prioritized) + +| Priority | Gap | Impact | Root Cause | +|----------|-----|--------|------------| +| P0 | Collider work O(world) | Tick spikes, unplayable under load | `_combineVoxelStates` scans all chunks of each block type | +| P0 | No greedy meshing | 2–64× more vertices than needed | Per-face quads, no merging | +| P1 | Entity sync volume | ~90% of packets | Full pos/rot floats, no quantization | +| P1 | Sync chunk persist | Main-thread blocking | `writeChunk` sync | +| P2 | No occlusion culling | Overdraw in caves | All loaded batches rendered | +| P2 | No distance-based entity LOD | Far entities same cost as near | Single sync rate | +| P3 | Vertex allocation churn | GC spikes on mesh updates | No pooling | + +--- + +## Part 2: Phased Roadmap + +### Phase 0: Foundation & Instrumentation (Week 1) + +**Goal:** Establish baselines and guardrails before major refactors. + +| Task | Owner | Deliverable | +|------|-------|-------------| +| Profiling hooks | Eng | Tick duration, chunk load time, collider time, mesh build time | +| Metrics dashboard | Eng | Real-time charts for key metrics | +| Block/face limits | Eng | Hard cap (e.g. 500K faces) to avoid meltdown | +| Regression suite | QA | Automated “fly-through” test, capture tick/frame times | + +**Success:** We can measure and reproduce performance issues in CI and on-device. + +--- + +### Phase 1: Collider Locality (Weeks 2–3) + +**Goal:** Remove O(world) collider scans. Physics and chunk work must scale with **visible/nearby** chunks only. + +| Task | Effort | Description | +|------|--------|-------------| +| Spatial index for block placements | 3 days | Chunk key → block placements; no global iteration | +| Scoped `_combineVoxelStates` | 2 days | Merge only chunks within N chunks of any player | +| Collider unload for distant chunks | 1 day | Remove colliders when chunk unloads; don’t keep in physics | +| Time-budget verification | 0.5 day | Ensure 8 ms cap is respected; tune if needed | + +**Files:** `ChunkLattice.ts`, `playground.ts` + +**Success:** Tick time (p99) drops from 50–200 ms to <25 ms under typical load. + +--- + +### Phase 2: Main-Thread Freedom (Weeks 4–5) + +**Goal:** No sync blocking on I/O or heavy computation on the game loop. + +| Task | Effort | Description | +|------|--------|-------------| +| Async `persistChunk` | 1.5 days | Queue writes; flush in background | +| Async provider audit | 0.5 day | Confirm `requestChunk` → `getChunkAsync` path is used | +| Incremental voxel collider updates | 4 days | Add blocks in batches (256–512/tick) instead of full chunk | +| Chunk send pacing | 1.5 days | Smooth chunk sync; avoid burst of 8 chunks in one tick | + +**Files:** `PersistenceChunkProvider.ts`, `RegionFileFormat.ts`, `ChunkLattice.ts`, `NetworkSynchronizer.ts` + +**Success:** Chunk load + persist never block tick; no “catch up” spikes. + +--- + +### Phase 3: Entity Sync Compression (Weeks 6–7) + +**Goal:** Reduce entity pos/rot from ~90% of packets to <50%, with no perceptible quality loss. + +| Task | Effort | Description | +|------|--------|-------------| +| Quantized position (1/256 block, 16-bit) | 1 day | Server sends `pq`; client decodes | +| Yaw-only rotation for players | 0.5 day | 1 float vs 4 for player avatars | +| Distance-based sync rate (30/15/5 Hz) | 1 day | Near = 30 Hz, mid = 15 Hz, far = 5 Hz | +| Quantized quaternion (smallest-three) | 2 days | For NPCs and other full-rotation entities | +| Bulk pos/rot packet (optional) | 2 days | Structure-of-arrays for unreliable updates | + +**Files:** `Serializer.ts`, `NetworkSynchronizer.ts`, `protocol/schemas/Entity.ts`, `Deserializer.ts`, `EntityManager.ts` + +**Success:** Entity sync bytes/update reduced by 50–60%; bandwidth share <50%. + +--- + +### Phase 4: Greedy Meshing (Weeks 8–10) + +**Goal:** Cut vertex count by 2–64× for typical terrain; stable 60 FPS on chunk load. + +| Task | Effort | Description | +|------|--------|-------------| +| Greedy mesh algorithm (opaque solids) | 5 days | 0fps-style sweep and merge; ref `docs/research/GREEDY_MESHING_IMPLEMENTATION_GUIDE.md` | +| Integration with ChunkWorker | 2 days | Per-batch-type merge; transparent blocks unchanged | +| AO + lighting on merged quads | 1 day | Ensure ambient occlusion and lighting still apply | +| Benchmarks and tuning | 1 day | Measure build time vs vertex reduction | + +**Files:** `ChunkWorker.ts`, `ChunkMeshManager.ts` + +**Success:** Flat chunk: ~6000 vertices → ~200–500; frame time stable on new chunk load. + +--- + +### Phase 5: Render Pipeline Polish (Weeks 11–13) + +**Goal:** GPU efficiency and graceful degradation on low-end devices. + +| Task | Effort | Description | +|------|--------|-------------| +| Vertex pooling | 2 days | Reuse BufferGeometry/ArrayBuffers; avoid per-frame allocations | +| Occlusion culling always-on | 2 days | BFS from camera; cull hidden batches | +| Mesh apply budget | 1 day | Limit meshes applied per frame; spread load | +| Block/face limits enforcement | 0.5 day | Reduce view distance when over cap | + +**Files:** `ChunkMeshManager.ts`, `ChunkManager.ts`, `ChunkWorker.ts`, `Renderer.ts` + +**Success:** No GC spikes on chunk load; overdraw reduced in cave-heavy areas. + +--- + +### Phase 6: Long-Term (Month 4+) + +| Task | Impact | Effort | +|------|--------|--------| +| LOD impostors for distant chunks | Medium | 2–3 weeks | +| Brotli (or similar) for region payloads | Low | 1 week | +| Predictive chunk preload | Medium | 1 week | +| Client-side entity prediction | Medium (latency) | 2+ weeks | + +--- + +## Part 3: Research Documentation + +The following research docs support implementation and design decisions: + +| Document | Purpose | +|----------|---------| +| [MINECRAFT_ARCHITECTURE_RESEARCH.md](./research/MINECRAFT_ARCHITECTURE_RESEARCH.md) | How Minecraft structures chunk loading, colliders, and meshing | +| [GREEDY_MESHING_IMPLEMENTATION_GUIDE.md](./research/GREEDY_MESHING_IMPLEMENTATION_GUIDE.md) | Step-by-step greedy meshing for ChunkWorker | +| [COLLIDER_ARCHITECTURE_RESEARCH.md](./research/COLLIDER_ARCHITECTURE_RESEARCH.md) | Spatial locality and incremental colliders | +| [NETWORK_PROTOCOL_2026_RESEARCH.md](./research/NETWORK_PROTOCOL_2026_RESEARCH.md) | Modern entity sync: quantization, delta, LOD | + +**Mandate:** Engineers implementing Phase 2+ work must read the relevant research doc before coding. + +--- + +## Part 4: Success Metrics + +| Metric | Baseline (Current) | Phase 3 Target | Phase 6 Target | +|--------|--------------------|----------------|----------------| +| Server tick time (p99) | 50–200 ms | <25 ms | <16 ms | +| Chunk load (blocking) | 20–100 ms | 0 (async) | 0 | +| Vertices per flat chunk | ~6000 | ~200–500 | ~200–500 | +| Entity sync % of packets | ~90% | ~60% | <50% | +| Client frame time (p99) | Spikes to 50+ ms | <25 ms | <16 ms | +| Perceived lag spikes | Every ~5 steps | None in preload | None | + +--- + +## Part 5: Risks & Mitigations + +| Risk | Mitigation | +|------|------------| +| Greedy meshing regresses build time | Time-budget; fallback to non-greedy if over budget | +| Protocol changes break old clients | Backward-compatible optional fields; version handshake | +| Collider refactor introduces physics bugs | Rigorous test: spawn, walk, mine, place; compare before/after | +| Scope creep | Phases are fixed; Phase 6 is explicitly “long-term” | + +--- + +## Part 6: Dependencies & Prerequisites + +- **PR #21 (Compressed JSON maps):** Merge for JSON-map games; not blocking procedural world. +- **TerrainWorkerPool:** Already in place; verify `getChunkAsync` is used in playground. +- **Protocol package:** Schema changes require protocol version bump; coordinate with SDK consumers. +- **Browser support:** Target evergreen browsers; no polyfills for cutting-edge APIs. + +--- + +## Part 7: Sign-Off + +This plan represents a realistic path to Minecraft/Hytale-grade smoothness for Hytopia’s procedural world. It prioritizes the highest-impact bottlenecks (colliders, greedy meshing, entity sync) and defers nice-to-haves (LOD impostors, prediction) to later phases. + +**Recommendation:** Approve and execute Phase 0–1 immediately. Re-evaluate after Phase 3 based on metrics and user feedback. + +--- + +*— Head of Development* diff --git a/ai-memory/docs/perf-external-notes-2026-03-05/raw/VOXEL_PERFORMANCE_MASTER_PLAN.md b/ai-memory/docs/perf-external-notes-2026-03-05/raw/VOXEL_PERFORMANCE_MASTER_PLAN.md new file mode 100644 index 00000000..9b7fdda0 --- /dev/null +++ b/ai-memory/docs/perf-external-notes-2026-03-05/raw/VOXEL_PERFORMANCE_MASTER_PLAN.md @@ -0,0 +1,153 @@ +# Voxel Engine Performance Master Plan +## Making Hytopia as Smooth as Minecraft & Hytale + +**Problem:** Lag every ~5 steps; constant chunk rendering; engine feels clunky compared to Minecraft/Hytale. + +**Conclusion:** This is primarily a **software/codebase architecture** issue, not hardware. Minecraft and Hytale run smoothly on similar hardware because they use different architectures. The plan below addresses the gaps. + +--- + +## Part 1: Root Cause Analysis + +### Why "Every 5 Steps" Lag Happens + +| Step | What Happens | Bottleneck | +|------|--------------|------------| +| 1 | Player moves → enters new chunk/batch range | Server loads 1 chunk/tick (CHUNKS_PER_TICK=1) | +| 2 | `getOrCreateChunk` runs | **Sync** disk read or procedural gen blocks main thread | +| 3 | Chunk queued for collider | `processPendingColliderChunks(1)` – 1/tick | +| 4 | `_addChunkBlocksToColliders` | **Heavy:** 4096 blocks, voxel propagation, `_combineVoxelStates` scans ALL chunks of that block type | +| 5 | Server sends chunk to client | Network ok, but chunk sync triggers client work | +| 6 | Client receives ChunksPacket | Posts to ChunkWorker | +| 7 | ChunkWorker builds mesh | **No greedy meshing** – 1 quad per face, 64× more than optimal for flat terrain | +| 8 | Mesh sent back, added to scene | BufferGeometry creation, possible GC spike | +| 9 | Main thread applies mesh | Can cause frame hitch | + +### Current vs. Minecraft/Hytale + +| Aspect | Hytopia (Current) | Minecraft / Hytale | +|--------|-------------------|---------------------| +| Chunk load | Sync on main thread | Worker threads, async | +| File I/O | `fs.readSync`, `zlib.gunzipSync` | Async, or worker | +| Terrain gen | Sync in main thread | Worker pool | +| Collider creation | Sync, 1/tick, O(world size) | Deferred, batched, O(chunk) | +| Mesh generation | Worker ✅ | Worker ✅ | +| Greedy meshing | ❌ (1 quad/face) | ✅ (merged quads, 2–64× fewer) | +| LOD | ✅ (step 2/4) | ✅ + impostors | +| Occlusion culling | Only when over face limit | Chunk-section visibility | +| Chunk send rate | Per ADD_CHUNK event | Batched, rate-limited | + +--- + +## Part 2: Prioritized Fixes + +### Tier 1: Quick Wins (1–3 days each) + +| # | Fix | Impact | Effort | Files | +|---|-----|--------|--------|-------| +| 1 | **Increase CHUNKS_PER_TICK** to 2–3 | Fewer "catch up" spikes when moving | 5 min | `playground.ts` | +| 2 | **Time-budget collider processing** | Cap ms per tick (e.g. 8 ms), process multiple chunks if time allows | Medium | `ChunkLattice.ts`, `playground.ts` | +| 3 | **Chunk send batching** | Don’t flood client; batch chunk sync every N ms or per tick | Medium | `NetworkSynchronizer.ts` | +| 4 | **Avoid collider work for distant chunks** | Only add colliders for chunks within 2–3 chunks of player | Medium | `ChunkLattice.ts`, `playground.ts` | + +### Tier 2: High Impact (3–7 days each) + +| # | Fix | Impact | Effort | Notes | +|---|-----|--------|--------|-------| +| 5 | **Greedy meshing (quad merging)** | 2–64× fewer vertices for terrain | 3–5 days | ChunkWorker; ref 0fps, mikolalysenko/greedy-mesher | +| 6 | **Async chunk provider** | `getChunk()` returns `Promise`; no main-thread blocking | 2–3 days | PersistenceChunkProvider, ProceduralChunkProvider, ChunkLattice | +| 7 | **Worker terrain generation** | Move `generateChunk` to `worker_threads` | 2–3 days | TerrainGenerator, ProceduralChunkProvider | +| 8 | **Async file I/O** | `fs.promises`, `zlib.gunzip` async | 1–2 days | RegionFileFormat.ts | + +### Tier 3: Architectural (1–2 weeks each) + +| # | Fix | Impact | Effort | Notes | +|---|-----|--------|--------|-------| +| 9 | **Incremental colliders** | Add blocks to voxel collider in batches (e.g. 256/tick) instead of full chunk | High | Rapier voxel API; ChunkLattice | +| 10 | **Collider locality** | `_getBlockTypePlacements` and `_combineVoxelStates` should not scan entire world | High | ChunkLattice; spatial indexing | +| 11 | **Chunk preloading by prediction** | Load chunks in movement direction before player arrives | Medium | playground.ts, loadChunksAroundPlayers | +| 12 | **Vertex pooling** | Reuse BufferGeometry / ArrayBuffers to reduce allocations and GC | Medium | ChunkMeshManager, ChunkWorker | + +### Tier 4: Polish (Ongoing) + +| # | Fix | Impact | Effort | +|---|-----|--------|--------| +| 13 | **Occlusion culling always-on** | Not just when over face limit | Medium | +| 14 | **LOD impostors** | Billboard or simplified mesh for very far chunks | High | +| 15 | **Profiling hooks** | Tick time, chunk load time, mesh build time | Low | +| 16 | **Block/face limits** | Hard cap to avoid meltdown on weak devices | Low | + +--- + +## Part 3: Recommended Implementation Order + +### Phase 1: Stop the Bleeding (Week 1) + +1. **Time-budget collider processing** – Cap at 8 ms/tick; process as many chunks as fit. +2. **Increase CHUNKS_PER_TICK** to 2–3. +3. **Spatial collider culling** – Only create colliders for chunks within 2–3 chunks of any player. +4. **Chunk send batching** – Batch chunk sync; don’t send 10 chunks in one frame. + +### Phase 2: Main Thread Freedom (Week 2–3) + +5. **Async file I/O** – `fs.promises`, async decompress. +6. **Async chunk provider** – `getChunk()` returns `Promise`; ChunkLattice awaits. +7. **Worker terrain gen** – Move `generateChunk` to worker thread. + +### Phase 3: Render Pipeline (Week 4–5) + +8. **Greedy meshing** – Implement in ChunkWorker for opaque solids; merge adjacent same-type faces. +9. **Vertex pooling** – Reuse geometry buffers where possible. + +### Phase 4: Long-Term (Month 2+) + +10. **Incremental colliders** – Batched voxel updates. +11. **Collider locality** – Remove global scans. +12. **Occlusion always-on** – Reduce overdraw. + +--- + +## Part 4: Hardware vs. Software + +| Factor | Assessment | +|--------|------------| +| **Hardware** | Unlikely primary cause if Minecraft/Hytale run fine. | +| **Software** | Sync I/O, sync terrain gen, heavy collider work, no greedy meshing – all main-thread and render bottlenecks. | +| **Codebase** | Architecture is serviceable but lacks async pipeline and mesh optimization used by mature voxel engines. | + +--- + +## Part 5: Key Files + +| Component | Path | +|-----------|------| +| Chunk load loop | `server/src/playground.ts` | +| Collider processing | `server/src/worlds/blocks/ChunkLattice.ts` | +| Mesh generation | `client/src/workers/ChunkWorker.ts` | +| Chunk sync to client | `server/src/networking/NetworkSynchronizer.ts` | +| Disk I/O | `server/src/worlds/maps/RegionFileFormat.ts` | +| Terrain generation | `server/src/worlds/maps/TerrainGenerator.ts`, `ProceduralChunkProvider.ts` | +| Client chunk handling | `client/src/chunks/ChunkManager.ts` | + +--- + +## Part 6: Success Metrics + +| Metric | Current (Est.) | Target | +|--------|----------------|--------| +| Lag spikes when walking | Every ~5 steps | None within preload radius | +| Tick time (p99) | 50–200 ms | < 16 ms | +| Chunk load time | 20–100 ms (blocking) | < 5 ms (async) | +| Vertices per chunk (flat) | ~6000 (no greedy) | ~200–500 (greedy) | +| Frame time (client) | Spikes on new chunks | Stable 16 ms (60 fps) | + +--- + +## References + +- `docs/CHUNK_LOADING_ARCHITECTURE.md` +- `docs/VOXEL_RENDERING_RESEARCH.md` +- `docs/OPTIMIZATION_STRATEGY.md` +- [0fps Greedy Meshing](https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/) +- [mikolalysenko/greedy-mesher](https://github.com/mikolalysenko/greedy-mesher) +- Hytale engine deep dive: variable chunks, LOD, mesh optimization diff --git a/ai-memory/docs/perf-external-notes-2026-03-05/raw/VOXEL_RENDERING_RESEARCH.md b/ai-memory/docs/perf-external-notes-2026-03-05/raw/VOXEL_RENDERING_RESEARCH.md new file mode 100644 index 00000000..5be2a42d --- /dev/null +++ b/ai-memory/docs/perf-external-notes-2026-03-05/raw/VOXEL_RENDERING_RESEARCH.md @@ -0,0 +1,190 @@ +# Voxel World Smoothness: Research on Minecraft, Hytale, and bloxd + +Deep research into how popular voxel games keep worlds lag-free and smooth during flight/movement. + +--- + +## Summary: What These Games Do + +| Technique | Minecraft | Hytale | bloxd | Hytopia (Current) | +|-----------|-----------|--------|-------|-------------------| +| **Face culling** | ✅ | ✅ | ✅ | ✅ (ChunkWorker) | +| **Greedy meshing** | ✅ (approximation) | ✅ | ✅ | ❌ | +| **Chunk batching** | ✅ (16×16×16) | Variable sizes | ✅ | ✅ (2×2×2 batches) | +| **Async mesh generation** | ✅ (worker) | ✅ | ✅ | ✅ (ChunkWorker) | +| **View distance** | ✅ | ✅ | ✅ | ✅ | +| **LOD (distant simplification)** | ✅ | ✅ | ✅ | ❌ | +| **Occlusion / cave culling** | ✅ (advanced) | Partial | Partial | ❌ | +| **Vertex pooling** | — | — | ✅ | ❌ | +| **Block/face limits** | Implicit | — | — | ❌ | + +--- + +## 1. Face Culling (Already Implemented ✅) + +**What it does:** Only render faces that are visible—i.e. faces where the adjacent block is empty or transparent. Interior faces between solid blocks are never drawn. + +**0fps comparison:** On a solid 8×8×8 cube: +- Stupid method: 3,072 quads (6 per block) +- Culling: 384 quads (1 per surface face) +- **~8× reduction** + +**Hytopia status:** Already in `ChunkWorker.ts` (lines 962–985). Neighbor check per face; solid opaque neighbors → face is culled. **No change needed.** + +--- + +## 2. Greedy Meshing / Greedy Quad Merging (Not Implemented ❌) + +**What it does:** Merge adjacent faces with the same texture/material into larger quads. Instead of many small quads, you get fewer large quads covering the same surface. + +**0fps example:** Same 8×8×8 solid cube: +- Culling: 384 quads +- Greedy: **6 quads** (one per side) +- **64× reduction over culling** + +**Algorithm (0fps):** +1. Sweep the 3D volume in 3 directions (X, Y, Z) +2. For each 2D slice, identify visible faces +3. Greedily merge adjacent same-type faces into rectangles +4. Order: top-to-bottom, left-to-right; pick the lexicographically minimal mesh + +**Multiple block types:** Group by (block type, normal direction). Mesh each group separately. + +**Performance trade-off:** +- Greedy is slower to *build* than culling (more passes, more logic) +- But produces far fewer vertices → faster rendering and less GPU memory +- Modern bottleneck is often CPU→GPU transfer; fewer vertices = less data = smoother + +**Hytopia status:** ChunkWorker emits one quad per visible face. No merging. + +**Recommendation:** High impact. Implement greedy meshing in ChunkWorker for opaque solid blocks first. Reference: [0fps greedy meshing](https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/), [mikolalysenko/greedy-mesher](https://github.com/mikolalysenko/greedy-mesher). + +--- + +## 3. Occlusion / Cave Culling (Not Implemented ❌) + +**What it does:** Don’t render chunks (or chunk sections) that are completely hidden behind solid terrain. E.g. caves behind a mountain. + +**Minecraft (Tommo’s Advanced Cave Culling, 2014):** +- Works on 16×16×16 chunk sections +- Builds a connectivity graph of transparent/air paths +- BFS from camera to find visible sections +- Culls sections unreachable through air/transparent blocks +- ~14% frame time improvement + +**Hytopia status:** No occlusion culling. All loaded chunks in view distance are rendered if in frustum. + +**Recommendation:** Medium impact, higher complexity. Consider chunk-section visibility BFS. Less urgent than greedy meshing. + +--- + +## 4. Level of Detail (LOD) (Not Implemented ❌) + +**What it does:** Render distant chunks with simpler geometry—fewer quads, lower resolution, or simplified shapes. + +**Hytale:** Variable chunk sizes; LOD where distant chunks use lower-detail meshes. + +**Typical approach:** +- Near: Full detail +- Mid: Merged/simplified mesh +- Far: Very low poly or impostors + +**Hytopia status:** No LOD. All chunks use the same mesh quality. + +**Recommendation:** Medium impact. Could start with “skip every other block” or similar for distant batches. More complex: proper LOD meshes. + +--- + +## 5. Async Mesh Generation (Already Implemented ✅) + +**What it does:** Build chunk meshes in a worker thread so the main thread stays responsive. + +**Hytopia status:** `ChunkWorker.ts` runs in a Web Worker. Mesh building is off the main thread. **Already good.** + +--- + +## 6. Block / Face Limits + +**What it does:** Cap total blocks or faces to avoid overload. E.g. stop loading chunks if face count exceeds a threshold. + +**Hytopia status:** No hard limit. Chunk count is bounded by view distance, but no per-frame or total face limit. + +**Recommendation:** Low priority. Could add a safety cap (e.g. max 500K faces) to avoid extreme lag on weak devices. + +--- + +## 7. Vertex Pooling (bloxd / High-Performance Engines) + +**What it does:** Reuse vertex buffers instead of allocating new ones per chunk. Reduces allocations and GC. + +**Impact:** Can improve frame times by tens of percent in allocation-heavy setups. + +**Hytopia status:** New geometry per batch. No pooling. + +**Recommendation:** Lower priority. Consider if profiling shows allocation/GC as a bottleneck. + +--- + +## 8. Server-Side Optimizations (Already Addressed) + +- **View distance:** Reduced default, `/view` command +- **Chunk load/unload:** With grace period +- **Prioritize by view direction:** Load chunks in front first +- **Unload distant chunks:** Keeps memory bounded + +--- + +## Prioritized Implementation Plan + +| Priority | Technique | Impact | Complexity | Effort | +|----------|-----------|--------|------------|--------| +| 1 | **Greedy meshing** | High | Medium | 2–3 days | +| 2 | **LOD for distant chunks** | Medium | Medium | 1–2 days | +| 3 | **Occlusion / cave culling** | Medium | High | 3+ days | +| 4 | **Block/face limit cap** | Low (safety) | Low | <1 day | +| 5 | **Vertex pooling** | Low–Medium | Medium | 1–2 days | + +--- + +## Greedy Meshing Implementation Sketch + +For `ChunkWorker._createChunkBatchGeometries`: + +1. **Current flow:** Per block → per face → if visible → emit quad. +2. **New flow (opaque solids):** + - Collect visible faces with (normal, blockTypeId, textureUri, AO, light) as keys + - For each direction (±X, ±Y, ±Z), build a 2D grid of visible faces + - Run greedy merge per slice (0fps algorithm) + - Emit merged quads instead of per-face quads +3. **Transparent blocks:** Can stay as-is (per-face) or use a separate greedy pass with transparency grouping. +4. **Trimesh blocks:** Keep current logic (no greedy). + +**References:** +- [0fps Part 1](https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/) +- [0fps Part 2 (multiple types)](https://0fps.net/2012/07/07/meshing-minecraft-part-2/) +- [mikolalysenko/greedy-mesher](https://github.com/mikolalysenko/greedy-mesher) (JS) +- [Vercidium greedy voxel meshing gist](https://gist.github.com/Vercidium/a3002bd083cce2bc854c9ff8f0118d33) + +--- + +## Other Considerations + +- **Runs-based meshing:** Alternative to full greedy; ~20% more triangles but ~4× faster build. Good compromise. +- **GPU-driven rendering:** Modern engines use compute shaders for mesh generation. WebGL limits this; workers are the main option. +- **Chunk size:** Hytopia uses 16³ chunks and 2×2×2 batches (32³). Matches common practice. + +--- + +## Implemented (Hytopia) + +- **LOD:** Distant chunks use step 2 or 4 (half/quarter detail). Underground batches get +1 LOD. +- **Block/face limits:** When total faces > 800K, view distance shrinks to 25% and occlusion runs. +- **Vertex pooling:** Mesh updates reuse existing BufferAttributes when size matches (avoids GPU realloc). +- **Occlusion culling:** BFS from camera through air/liquid; only visible batches rendered when over face limit. +- **Underground LOD:** Batches below Y=40 use one extra LOD step (reduces cave geometry; partial greedy benefit). + +## Conclusion + +The largest missing optimization is **full greedy meshing** (quad merging). Face culling is in place, but merging adjacent same-type faces into larger quads can cut vertex/quad count by roughly 2–10× depending on geometry, which directly reduces GPU work and often improves smoothness when flying. + +LOD and occlusion culling are useful next steps; block limits and vertex pooling are refinements for later. diff --git a/ai-memory/docs/perf-final-2026-03-05/FINAL.md b/ai-memory/docs/perf-final-2026-03-05/FINAL.md new file mode 100644 index 00000000..8339d071 --- /dev/null +++ b/ai-memory/docs/perf-final-2026-03-05/FINAL.md @@ -0,0 +1,631 @@ +# HYTOPIA Performance (Client + Server) — Consolidated Findings (2026-03-05) + +Base code reference for all “Verified” statements in this report: + +- `origin/master` @ `24a295d` (2026-03-05) +- Repo: `web3dev1337/hytopia-source` (fork of `hytopiagg/hytopia-source`) + +This is a synthesis of: + +- **Code-verified findings** (client, server, protocol) +- **Your open performance PRs** on the fork +- **Imported third‑party notes** from Windows Downloads (`/mnt/c/Users/AB/Downloads`) captured on **2026-03-05 14:09–14:25** (local time) and stored under: + - `ai-memory/docs/perf-external-notes-2026-03-05/raw/` + - Cross-check doc: `ai-memory/docs/perf-external-notes-2026-03-05/FINDINGS.md` + +--- + +## Executive Summary (What’s Actually Hurting Performance Today) + +### P0 (highest impact, verified in code) + +1) **Server → client chunk delivery is bursty and unbounded** + - On **player join/reconnect**, the server queues **every chunk in the world** for that player (`NetworkSynchronizer._onPlayerJoinedWorld` loops `chunkLattice.getAllChunks()`), then sends them as chunk packets with **no pacing/segmentation**. + - Every chunk is serialized with `Array.from(chunk.blocks)` (4096 numbers) which is extremely allocation-heavy and inflates payload sizes. + +2) **Client chunk meshing is “per visible face”, not greedy** + - Face culling exists, but there is **no quad merging / greedy meshing**, so vertex counts are much higher than necessary in common terrain. + - This increases worker CPU, transfer sizes, main-thread mesh apply costs, GPU memory, and draw overhead. + +3) **Client network decoding can block the main thread** + - Incoming packets are synchronously gzip-decompressed (`gunzipSync`) and msgpack-decoded on the main thread. + - Large chunk packets + sync decompression/decoding are a direct path to visible stutter. + +4) **Client creates/destroys GPU geometry frequently** + - Chunk batch updates replace `BufferGeometry` objects and dispose old ones rather than updating attributes in place (no pooling/reuse). + +### P1 (medium/high impact, verified in code) + +5) **Entity sync bandwidth is larger than it needs to be** + - Entity pos/rot updates are float vectors/quaternions (float32) with no quantization or delta compression. + - The server already routes pos/rot‑only updates to the unreliable channel, which is good, but payload size is still high. + +6) **Protocol + serializer choices force avoidable copying** + - `protocol/schemas/Chunk.ts`’s AJV JSON schema only accepts `b` as `number[]` (4096 entries), so the server serializes chunk blocks via `Array.from`. + - The client then always does `new Uint8Array(chunk.b)`, which **copies** again. + +### P2 (lower impact or situational, verified in code) + +7) **View-distance culling work is O(batches) every frame** + - Each frame, the client iterates all batch IDs and computes distances to decide scene membership. + +8) **MEDIUM/LOW presets have no FPS cap; DPR is unbounded** + - Only `POWER_SAVING` has an `fpsCap` on `master`. + - Renderer pixel ratio uses `window.devicePixelRatio * resolution.multiplier` with no cap. + +--- + +## Verified Issues (with Evidence + Recommended Fixes) + +### 1) Server chunk sync: “full world on join” + no pacing (P0) + +**Evidence (verified):** + +- `server/src/networking/NetworkSynchronizer.ts` + - `_onPlayerJoinedWorld`: queues chunk sync for **all chunks**: + - `for (const chunk of this._world.chunkLattice.getAllChunks()) { ... chunk.serialize() ... }` + - `_collectSyncToOutboundPackets`: turns queued chunk syncs into one packet per sync without pacing: + - `protocol.createPacket(..., sync.valuesArray, tick)` + +**Impact:** + +- Server CPU + memory spikes on join/reconnect (serialize + validate + msgpack pack + gzip). +- Client stutters on receipt (sync gunzip + unpack + per-chunk registry + worker messages + mesh builds). +- Networking bursts can increase packet loss / HOL blocking (and increases likelihood of gzip work). + +**Measured (perf-tools, this branch):** + +- `join-storm` (`boilerplate.json` + 100 joins): `p99TickMs=77.47`, `maxTickMs=112.95`, `serialize_packets avg=14.84ms` +- `blocks-10m-dense` (synthetic ~2442 chunks + 1 join): `maxTickMs=86.36`, `serialize_packets avg=33.13ms` (highly gzip-compressible payload) + +**Fix direction:** + +- Implement **per-player chunk streaming**: + - Maintain `playerVisibleChunkSet` (or batch set) derived from player position + view distance. + - Queue only *newly visible* chunks; send removals when leaving range. +- Add **pacing/segmentation**: + - Enforce a per-player per-tick budget (chunks, bytes, or ms). + - Never enqueue “all chunks” into one `Chunks` packet; emit multiple smaller packets across ticks. + +**Related (your PRs):** + +- None directly address server chunk pacing today. +- PR #6 (map compression) reduces disk/map load size but does not solve network pacing. + +**Related (external notes):** + +- `VOXEL_PERFORMANCE_MASTER_PLAN.md`, `SMOOTH_WORLD_STREAMING_REFACTOR_PLAN.md` correctly push “chunk send pacing” as a requirement, but reference constants/systems that do not exist on `master`. + +--- + +### 2) Server chunk serialization allocates huge arrays (P0) + +**Evidence (verified):** + +- `server/src/networking/Serializer.ts` + - `serializeChunk()` does: + - `b: Array.from(chunk.blocks)` + - `r: Array.from(chunk.blockRotations).flatMap(...)` + +**Impact:** + +- For each chunk sent, allocates a 4096-element `number[]` (and then msgpack serializes it). +- For join sync, this multiplies by total chunk count and happens per joining player. + +**Fix direction (high leverage):** + +- Align protocol schema + serialization to allow `Uint8Array` “bin” payloads: + - Update protocol schema validation to accept `Uint8Array` for `ChunkSchema.b` (and ideally send it). + - Update client deserializer to **avoid copying** when `b` is already `Uint8Array`. + - Goal: `ChunkSchema.b` transmitted as msgpack “bin” (compact, fast) instead of an array of numbers. + +--- + +### 3) Server gzip is synchronous in the hot path (P0/P1) + +**Evidence (verified):** + +- `server/src/networking/Connection.ts` + - `Connection.serializePackets()` uses `gzipSync` for payloads > 64KB. + +**Impact:** + +- Compression runs on the server main thread, causing tick spikes during large chunk flushes. + +**Fix direction:** + +- Reduce the need for gzip by shrinking payloads first (typed arrays for chunks, pacing). +- If gzip remains necessary: + - Consider async compression (worker thread) or a different framing strategy. + +--- + +### 4) Server validates packets with AJV before every send (P1) + +**Evidence (verified):** + +- `server/src/networking/Connection.ts` + - `serializePackets()` calls `protocol.isValidPacket(packet)` for every packet, every send. + +**Impact:** + +- AJV validation of large payload packets (especially chunks) is CPU-expensive. + +**Fix direction (safer than “turn it off”):** + +- Cache validation results per packet object identity for the duration of a sync tick (similar to the serialization cache). +- Consider skipping deep validation for the heaviest, most-constructed packets in production builds, but only with strong safeguards (tests, feature flag). + +--- + +### 5) Client chunk meshing lacks greedy quad merging (P0) + +**Evidence (verified):** + +- `client/src/workers/ChunkWorker.ts` + - Per block → per face → if visible → emit quad. + - Face culling exists (neighbor check); no 2D “merge rectangles” pass. +- External note cross-check: `ai-memory/docs/perf-external-notes-2026-03-05/FINDINGS.md` + +**Impact:** + +- Greatly increases: + - Worker compute time + - Geometry transfer sizes + - Main thread BufferGeometry creation cost + - GPU vertex processing and memory pressure + +**Fix direction:** + +- Implement greedy meshing for opaque solids first: + - See external guide: `ai-memory/docs/perf-external-notes-2026-03-05/raw/GREEDY_MESHING_IMPLEMENTATION_GUIDE.md` + - Keep transparent/liquid/trimesh paths per-face initially if needed. + +**Notes on third-party docs:** + +- The greedy meshing guidance is generally sound, but some example metrics and some “Implemented (Hytopia)” claims in `VOXEL_RENDERING_RESEARCH.md` do **not** match this repo’s `master`. + +--- + +### 6) Client builds new BufferGeometry per update (P0/P1) + +**Evidence (verified):** + +- `client/src/chunks/ChunkMeshManager.ts` + - `_createOrUpdateMesh()` always creates `new BufferGeometry()`. + - On update, disposes old geometry and swaps in the new one. + +**Impact:** + +- GPU buffer churn + JS allocations during chunk streaming and block edits. + +**Fix direction:** + +- Reuse geometries: + - Keep one `BufferGeometry` per batch mesh and update `BufferAttribute` arrays in place. + - If size changes frequently, pool common sizes or chunk updates into fixed “slabs”. + +--- + +### 7) Client network decode is synchronous on main thread (P0) + +**Evidence (verified):** + +- `client/src/network/NetworkManager.ts` + - `gunzipSync` (fflate) used for gzip payloads before `packr.unpack`. + +**Impact:** + +- Large chunk packets can block the render thread causing frame hitches. + +**Fix direction:** + +- Move decompression + unpacking off the main thread (net worker). +- Reduce/avoid gzip by shrinking chunk payloads (typed arrays, pacing). + +--- + +### 8) Client deserialization does extra copying + allocations (P1) + +**Evidence (verified):** + +- `client/src/network/Deserializer.ts` + - `deserializeChunk`: `blocks: chunk.b ? new Uint8Array(chunk.b) : undefined` → always copies. + - `deserializeVector` / `deserializeQuaternion`: allocate new objects per update. + +**Impact:** + +- Additional CPU/GC pressure on the main thread during frequent updates. + +**Fix direction:** + +- Avoid copying `Uint8Array` when already typed. +- For hot-path entity updates, consider: + - Updating existing entity objects in place with primitives/typed arrays + - A new bulk packet format (structure-of-arrays) for pos/rot + +--- + +### 9) Client view-distance culling does per-frame full scans (P2) + +**Evidence (verified):** + +- `client/src/chunks/ChunkManager.ts` + - Each `RendererEventType.Animate` iterates all batch IDs and computes distance. + - Comment notes it may be costly and suggests caching/partitioning. + +**Impact:** + +- Becomes noticeable as batch count grows (CPU time per frame). + +**Fix direction:** + +- Cache visibility and recompute only when: + - camera moves across coarse “cells” + - settings change (view distance) + - batch set changes +- Your PR #9 (upstream mirror) targets this area. + +--- + +### 10) FPS cap + DPR cap missing on `master` (P2 quick wins) + +**Evidence (verified):** + +- `client/src/settings/SettingsManager.ts` + - Only `POWER_SAVING` has `fpsCap: 30`. + - `MEDIUM` / `LOW` have none. +- `client/src/core/Renderer.ts` + - pixel ratio uses `window.devicePixelRatio * resolution.multiplier` with no cap. + +**Fix direction (already in your PRs):** + +- PR #4: adds `fpsCap: 60` for `MEDIUM`/`LOW`. +- PR #5: caps mobile `devicePixelRatio` before applying multiplier. + +--- + +## Entity Sync: What’s Right + What’s Missing (Verified) + +### What’s already good + +- `server/src/networking/NetworkSynchronizer.ts`: + - Pos/rot-only entity updates are identified and sent on the **unreliable** channel. + - This reduces HOL blocking under packet loss. + +### What’s missing (main opportunities) + +- No quantized or delta formats exist in the protocol today (`protocol/schemas/Entity.ts` only has `p` and `r`). +- No distance-based sync LOD. + +### External note accuracy + +- `ENTITY_SYNC_DELTA_COMPRESSION_DESIGN.md` contains good direction (quantize/distance-LOD), but its **int16 range math is wrong** when using a 1/256 quantization factor. + - If you store `round(x * 256)` in int16, the representable range is roughly **±128 blocks**, not ±32768. + - If you want large-world range and fine precision, use chunk-relative encoding and/or wider ints (int32) and/or a different quant. + +--- + +## Colliders / Physics: What’s Real vs. What’s Assumed + +### Verified current collider model + +- `server/src/worlds/blocks/ChunkLattice.ts` + - Maintains colliders per **block type** (voxel or trimesh). + - Voxel updates use `collider.setVoxel(...)` + `propagateVoxelChange(...)`. + - Trimesh block types trigger full collider rebuild on changes (`_recreateTrimeshCollider`). +- `server/src/worlds/physics/Collider.ts` + - `combineVoxelStates` / `propagateVoxelChange` are Rapier-specific voxel-collider edge/transition requirements, not “merge placements” loops. + +### What the external notes get right + +- Trimesh rebuilds can be expensive as block counts grow. +- “Collider locality” (only simulate nearby blocks) is a valid scaling approach for very large worlds, but it is **not implemented** in this repo today. + +### What the external notes get wrong (relative to this repo) + +- Multiple notes reference systems/constants that do not exist on `master`: + - `CHUNKS_PER_TICK`, `MAX_CHUNKS_PER_SYNC`, `TerrainWorkerPool`, `PersistenceChunkProvider`, `RegionFileFormat.ts`, `COLLIDER_MAX_CHUNK_DISTANCE`, `processPendingColliderChunks`, etc. + +--- + +## Your Performance PRs (Fork) — What They Address + +All PRs below are on `web3dev1337/hytopia-source` as of **2026-03-05**. + +- **#4** “cap FPS on MEDIUM/LOW presets” (OPEN) — adds `fpsCap: 60` to `MEDIUM`/`LOW`. +- **#5** “cap mobile devicePixelRatio” (OPEN) — clamps mobile DPR before applying resolution multiplier. +- **#6** “compressed world maps” (OPEN) — reduces map disk size + load time for JSON-map games; not a direct fix for chunk networking. +- **#7–#9** upstream mirrors (OPEN) — prediction/camera smoothing/client perf pass (chunk visibility caching + outline improvements in #9). +- **#2–#3** analysis docs (OPEN) — large audits and device-specific performance writeups. + +This consolidated report focuses on the *root* hot paths on `master` (#4/#5/#9/#6 are relevant solutions for specific slices). + +--- + +## Third-Party Notes: What’s Correct vs Incorrect (Index) + +All files referenced below are imported under `ai-memory/docs/perf-external-notes-2026-03-05/raw/`. + +### Mostly correct about THIS repo (good signal) + +- `MAP_ENGINE_ARCHITECTURE.md` — accurately describes the JSON map → `World.loadMap` → `ChunkLattice` → `NetworkSynchronizer` flow. +- `GREEDY_MESHING_IMPLEMENTATION_GUIDE.md` — sound generic greedy meshing guidance (implementation work remains). +- `NETWORK_PROTOCOL_2026_RESEARCH.md` — good general direction, but contains quantization math errors (see above). + +### Mixed (some correct observations + some incorrect assumptions) + +- `VOXEL_RENDERING_RESEARCH.md` — correct on face culling present / greedy meshing absent; incorrect “Implemented (Hytopia)” section that does not match `master`. +- `COLLIDER_ARCHITECTURE_RESEARCH.md` — correct high-level collider structure (per block type); incorrect about some “critical path” details and assumes locality/pipelines not present. +- `ENTITY_SYNC_DELTA_COMPRESSION_DESIGN.md` — correct about current entity sync shape; useful ideas; incorrect numeric range claims for int16@1/256. + +### Mostly not about THIS repo (assumes systems not present) + +- `VOXEL_PERFORMANCE_MASTER_PLAN.md` +- `SMOOTH_WORLD_STREAMING_REFACTOR_PLAN.md` +- `VOXEL_ENGINE_2026_MASTER_PLAN.md` +- `MINECRAFT_ARCHITECTURE_RESEARCH.md` (Minecraft info is fine; claims about Hytopia’s procedural systems don’t match this repo) + +--- + +## Recommended Plan (Grounded in Current Code) + +### Phase A — immediate wins (days) + +1) Merge **PR #4** (FPS cap) and **PR #5** (mobile DPR cap). +2) Stop copying chunk blocks twice: + - Update client `Deserializer.deserializeChunk` to avoid `new Uint8Array(...)` when `b` is already `Uint8Array`. + - Update protocol + server serializer to send chunk blocks as `Uint8Array` (bin). +3) Implement chunk pacing on join: + - Replace “queue all chunks” join behavior with a time/byte budget. + +### Phase B — largest structural wins (1–2 weeks) + +4) Implement per-player chunk streaming by view distance (server side). +5) Move client decompress+unpack off main thread (or reduce gzip needs enough that it rarely triggers). + +### Phase C — rendering ceiling (2–4+ weeks) + +6) Implement greedy meshing for opaque solids in `ChunkWorker`. +7) Geometry reuse / pooling in `ChunkMeshManager`. +8) Improve view-distance culling algorithm (or merge upstream PR #11 mirror if acceptable). + +--- + +## Where to Find the “Proof / Verification” Doc + +- Verification of external-note claims against `origin/master` lives in: + - `ai-memory/docs/perf-external-notes-2026-03-05/FINDINGS.md` + +--- + +## Performance Framework (this PR branch) — Review + Current State + +This section reviews the “performance framework” implementation added in PR #11 (server module + `packages/perf-tools/` + GitHub Actions). + +### What’s real and useful today (server-side) + +- **`PerformanceMonitor` exists and is integrated** into the tick loop: + - `server/src/metrics/PerformanceMonitor.ts` implements tick history, per-operation stats (p50/p95/p99), spike detection, and snapshots. + - `server/src/worlds/WorldLoop.ts` calls `beginTick()` / `recordPhase()` / `endTick()` when enabled. +- **Operation double-counting is fixed** + - `WorldLoop.recordPhase(...)` now only records per-tick phase breakdown (not per-operation stats), so `Telemetry.startSpan(...)` + `perfMon.measure(...)` remains the single source of truth for operation timings. +- **`NetworkMetrics` is now integrated** + - Wired into `server/src/networking/Connection.ts` (bytes/packets + serialization/compression) and `server/src/players/PlayerManager.ts` (connected player count). +- **Perf harness endpoints are wired** (internal, env-gated): + - When `HYTOPIA_PERF_TOOLS=1`, the server exposes: + - `GET /__perf/snapshot` + - `POST /__perf/reset` + - `POST /__perf/action` (subset: `spawn_bots`, `despawn_bots`, `load_map`, `generate_blocks`, `spawn_entities`, `despawn_entities`, `start_block_churn`, `stop_block_churn`, `create_worlds`, `set_default_world`, `clear_world`, `reset`) +- **Preload/setup work no longer serializes chunk deltas when no players are present** + - `NetworkSynchronizer` skips queueing expensive block/chunk/block-type sync work until at least one player has joined the world (join burst is still unbounded). +- **A dedicated perf harness server entry exists** + - `server/src/perf/perf-harness.ts` → built via `server` script `build:perf-harness` to `server/src/perf-harness.mjs`. +- **`packages/perf-tools/` runs end-to-end for server benchmarks** + - Lockfile added (so `npm ci` works in CI). + - Preset loading fixed (`import.meta.url` → real dirname) and presets are copied into `dist/`. + - The runner starts the perf harness server, executes scenario actions via `/__perf/action`, and polls `/__perf/snapshot` to produce baselines. + +### Remaining gaps / limitations + +- **Client-side metrics are not collected yet** + - `HeadlessClient` (Puppeteer) still expects `window.__HYTOPIA_PERF__`, which the client does not currently define. + - The current runner focuses on **server** metrics (tick + memory + ops). The optional `scenario.clients` setting creates WebSocket connections (server-side “players”), but no FPS stats are collected. +- **No per-tick event stream yet** + - `MetricCollector` supports tick reports/spikes, but `perf-tools` currently collects periodic snapshots only. +- **Network metrics are collected and included in baselines** + - Server tracks bytes/packets/serialization/compression via `NetworkMetrics` and `perf-tools` records them in results JSON. +- **Client-side join stutter is not measured yet** + - We now benchmark the **server-side** join burst (“join-storm”), but we still do not capture client main-thread stutters from gzip+msgpack decode or chunk meshing cost. +- **The GitHub Actions workflows aren’t a hard gate yet** + - Bench steps remain `continue-on-error: true`, and compare uses `|| true`. + +--- + +## Test Coverage (What Was Actually Run) + +This section is a factual log of what was executed against the current PR #11 branch state. + +### Server runtime smoke test (engine boot + tick + bots) + +- Started the engine via `startServer(...)`. +- Loaded `assets/release/maps/boilerplate-small.json`. +- Enabled profiling: `PerformanceMonitor.instance.enable({ snapshotIntervalMs: 0 })`. +- Enabled per-entity profiling: `PerformanceMonitor.instance.enableEntityProfiling(true)`. +- Spawned **25 bots** (`RandomWalkBehavior`). +- Captured a snapshot after ~5s post-start: + - `avgTickMs=0.252`, `p95TickMs=0.382`, `p99TickMs=0.550`, `maxTickMs=12.509`, `ticksOverBudget=0`, `totalTicks=301` (0 players connected). + +### perf-tools end-to-end benchmarks (server-only snapshots) + +Results JSON (generated by `packages/perf-tools`): + +- `ai-memory/docs/perf-final-2026-03-05/results/idle.json` +- `ai-memory/docs/perf-final-2026-03-05/results/stress.json` +- `ai-memory/docs/perf-final-2026-03-05/results/large-world.json` +- `ai-memory/docs/perf-final-2026-03-05/results/many-players.json` +- `ai-memory/docs/perf-final-2026-03-05/results/combined.json` +- `ai-memory/docs/perf-final-2026-03-05/results/join-storm.json` +- `ai-memory/docs/perf-final-2026-03-05/results/block-churn.json` +- `ai-memory/docs/perf-final-2026-03-05/results/entity-density.json` +- `ai-memory/docs/perf-final-2026-03-05/results/multi-world.json` +- `ai-memory/docs/perf-final-2026-03-05/results/blocks-10k-dense.json` +- `ai-memory/docs/perf-final-2026-03-05/results/blocks-500k-dense.json` +- `ai-memory/docs/perf-final-2026-03-05/results/blocks-1m-dense.json` +- `ai-memory/docs/perf-final-2026-03-05/results/blocks-10m-dense.json` +- `ai-memory/docs/perf-final-2026-03-05/results/blocks-1m-multi-world.json` + +#### Idle preset (`idle-baseline`) + +- Warmup: 5s, Measure: 30s +- No real browser clients connected, no bots +- Baseline: + - `avgTickMs=0.05`, `p99TickMs=0.14`, `maxTickMs=0.76`, `avgHeap=40.7MB` + +#### Stress preset (`stress-test`) + +- Warmup: 5s +- Actions: + - Load map: `assets/maps/boilerplate-small.json` + - Spawn bots: 50 `random_walk`, 30 `chase`, 20 `interact` (100 total) +- Stabilize: 5s, Measure: 60s +- Baseline: + - `avgTickMs=0.26`, `p99TickMs=1.27`, `maxTickMs=2.33`, `avgHeap=47.0MB` + +#### Large-world preset (`large-world`) + +- Warmup: 10s +- Actions: + - Load map: `assets/maps/boilerplate.json` + - Spawn bots: 20 `random_walk` +- Stabilize: 10s, Measure: 60s +- Baseline: + - `avgTickMs=0.27`, `p99TickMs=0.52`, `maxTickMs=0.81`, `avgHeap=88.8MB` + +#### Many-players preset (`many-players`) + +- Warmup: 10s +- Clients: 50 WebSocket connections (server-side “players”) +- Actions: + - Spawn bots: 50 `random_walk` +- Measure: 60s +- Baseline: + - `avgTickMs=0.87`, `p99TickMs=3.22`, `maxTickMs=4.22`, `avgHeap=42.0MB` + +#### Combined preset (`combined-stress`) + +- Warmup: 10s +- Clients: 10 WebSocket connections (server-side “players”) +- Actions: + - Load map: `assets/maps/boilerplate.json` + - Spawn bots: 50 `random_walk`, 30 `chase`, 20 `interact` (100 total) +- Stabilize: 10s, Measure: 120s +- Baseline: + - `avgTickMs=1.77`, `p99TickMs=4.68`, `maxTickMs=119.11`, `overBudgetPct=0.8%`, `avgHeap=63.6MB` + +#### Join-storm preset (`join-storm`) + +- Warmup: 5s +- Actions: + - Load map: `assets/maps/boilerplate.json` + - Connect clients: 100 WebSocket connections (server-side “players”) +- Stabilize: 10s, Measure: 60s +- Baseline: + - `avgTickMs=3.84`, `p99TickMs=77.47`, `maxTickMs=112.95`, `overBudgetPct=3.7%` + - `avgSerializePacketsMs=14.84`, `p95SerializePacketsMs=44.25` + - `bytesSentTotal=37.0MB`, `compressTotal=100` + +#### Block-churn preset (`block-churn`) + +- Warmup: 5s +- Clients: 10 WebSocket connections +- Actions: + - Load map: `assets/maps/boilerplate-small.json` + - Start churn: `blocksPerTick=200` within `x/z [-16..16], y [1..5]` +- Measure: 60s +- Baseline: + - `avgTickMs=0.68`, `p99TickMs=1.36`, `maxTickMs=2.75` + - `bytesSentTotal=32.2MB` (block updates to clients) + +#### Entity-density preset (`entity-density`) + +- Warmup: 5s +- Actions: + - Load map: `assets/maps/boilerplate-small.json` + - Spawn: 500 dynamic block entities +- Stabilize: 10s, Measure: 60s +- Baseline: + - `avgTickMs=0.35`, `p99TickMs=0.65`, `maxTickMs=1.22` + - `entities_emit_updates avg=0.18ms` + +#### Multi-world preset (`multi-world`) + +- Warmup: 5s +- Actions: + - Create worlds: default world + 3 additional worlds, each loading `assets/maps/boilerplate-small.json` +- Measure: 60s +- Baseline: + - `avgTickMs=0.03`, `p99TickMs=0.09`, `maxTickMs=1.25` + +#### Blocks-10k-dense preset (`blocks-10k-dense`) + +- World: synthetic dense fill, `blockCount=10_000` (~3 chunks) +- Clients: 50 WebSocket connections +- Measure: 30s +- Baseline: + - `avgTickMs=0.08`, `p99TickMs=1.07`, `maxTickMs=3.52`, `avgHeap=42.1MB` + - `bytesSentTotal=0.7MB`, `compressTotal=0` + +#### Blocks-500k-dense preset (`blocks-500k-dense`) + +- World: synthetic dense fill, `blockCount=500_000` (~123 chunks) +- Clients: 20 WebSocket connections +- Measure: 60s +- Baseline: + - `avgTickMs=0.14`, `p99TickMs=2.43`, `maxTickMs=17.66`, `avgHeap=48.4MB` + - `serialize_packets avg=1.20ms`, `compressTotal=20` + +#### Blocks-1m-dense preset (`blocks-1m-dense`) + +- World: synthetic dense fill, `blockCount=1_000_000` (~245 chunks) +- Clients: 10 WebSocket connections +- Measure: 60s +- Baseline: + - `avgTickMs=0.13`, `p99TickMs=2.04`, `maxTickMs=24.70`, `avgHeap=54.6MB` + - `serialize_packets avg=2.68ms`, `compressTotal=10` + +#### Blocks-10m-dense preset (`blocks-10m-dense`) + +- World: synthetic dense fill, `blockCount=10_000_000` (~2442 chunks) +- Clients: 1 WebSocket connection +- Measure: 60s +- Baseline: + - `avgTickMs=0.15`, `p99TickMs=1.52`, `maxTickMs=86.36`, `avgHeap=55.6MB` + - `serialize_packets avg=33.13ms` (`p95=65.63ms`), `compressTotal=1` + +#### Blocks-1m-multi-world preset (`blocks-1m-multi-world`) + +- Worlds: 4 worlds × `blockCount=1_000_000` each (~245 chunks per world) +- Clients: connect 10 per world in 4 bursts (40 total) +- Measure: 90s +- Baseline: + - `avgTickMs=0.04`, `p99TickMs=0.06`, `maxTickMs=28.31`, `avgHeap=39.9MB` + - `serialize_packets avg=2.15ms`, `compressTotal=40` + +**Notes on synthetic block-count tests** + +- These presets fill chunks with a single repeated block ID, so gzip makes bandwidth look unrealistically good. The bottleneck still shows up as `serialize_packets` time and max tick spikes. +- For a more realistic multi-block map join burst, compare `join-storm` (`boilerplate.json`), which sent `37.0MB` total for 100 joins and hit `p99TickMs=77.47`. + +### Client build check + +- `client` production build passed (`tsc` + `vite build`) after removing an unused local in `client/src/network/NetworkManager.ts`. + +### Not tested + +- No `sdk-examples/*` games were run. +- No real browser gameplay session was used for these benchmarks (no FPS numbers; networking is only exercised if presets specify `clients`). + +### Environment limitations observed + +- WebTransport http3-quiche native addon was not available in this environment, so WebTransport/QUIC wasn’t exercised (server used WebSocket transport). diff --git a/ai-memory/docs/perf-final-2026-03-05/results/block-churn.json b/ai-memory/docs/perf-final-2026-03-05/results/block-churn.json new file mode 100644 index 00000000..ff0af3e2 --- /dev/null +++ b/ai-memory/docs/perf-final-2026-03-05/results/block-churn.json @@ -0,0 +1,96 @@ +{ + "timestamp": "2026-03-05T10:10:38.634Z", + "scenario": "block-churn", + "durationMs": 64135, + "baseline": { + "avgTickMs": 0.6849331709943677, + "maxTickMs": 2.745523999999932, + "p95TickMs": 1.0772681333333822, + "p99TickMs": 1.3614075166666983, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 53.00004514058431, + "operations": { + "entities_tick": { + "avgMs": 0.0011016425849722223, + "p95Ms": 0.0017433833341632028 + }, + "physics_step": { + "avgMs": 0.4908600910006157, + "p95Ms": 0.6150475666666807 + }, + "physics_cleanup": { + "avgMs": 0.003169533204346628, + "p95Ms": 0.00507628333351325 + }, + "simulation_step": { + "avgMs": 0.49752787563245937, + "p95Ms": 0.625502483332654 + }, + "entities_emit_updates": { + "avgMs": 0.0005685967802767758, + "p95Ms": 0.0008665333331767518 + }, + "serialize_packets": { + "avgMs": 0.0540394719882092, + "p95Ms": 0.0907738000004277 + }, + "send_packets": { + "avgMs": 0.031098482472493673, + "p95Ms": 0.1263187999997778 + }, + "send_all_packets": { + "avgMs": 0.328743960390142, + "p95Ms": 0.5316539666670754 + }, + "network_synchronize_cleanup": { + "avgMs": 0.004660384153499193, + "p95Ms": 0.006042183333102002 + }, + "network_synchronize": { + "avgMs": 0.3520175592994608, + "p95Ms": 0.562322033333506 + }, + "world_tick": { + "avgMs": 0.682740707841139, + "p95Ms": 1.0147215833332364 + }, + "ticker_tick": { + "avgMs": 1.0084807833297353, + "p95Ms": 1.4116792833336906 + } + }, + "network": { + "totalBytesSent": 32210193, + "totalBytesReceived": 0, + "maxConnectedPlayers": 10, + "avgBytesSentPerSecond": 535222.3882152817, + "maxBytesSentPerSecond": 559391.8169551987, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 300.2943947891586, + "maxPacketsSentPerSecond": 310.75611660367906, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0.051713485794082574, + "compressionCountTotal": 2 + } + }, + "phases": [ + { + "name": "setup", + "durationMs": 510, + "collected": false + }, + { + "name": "churn-and-measure", + "durationMs": 59116, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/ai-memory/docs/perf-final-2026-03-05/results/blocks-10k-dense.json b/ai-memory/docs/perf-final-2026-03-05/results/blocks-10k-dense.json new file mode 100644 index 00000000..cef71ad6 --- /dev/null +++ b/ai-memory/docs/perf-final-2026-03-05/results/blocks-10k-dense.json @@ -0,0 +1,101 @@ +{ + "timestamp": "2026-03-05T11:47:34.121Z", + "scenario": "blocks-10k-dense", + "durationMs": 40964, + "baseline": { + "avgTickMs": 0.0768966646045384, + "maxTickMs": 3.521280000000843, + "p95TickMs": 0.2584854000000026, + "p99TickMs": 1.070374300000185, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 42.14106089274089, + "operations": { + "entities_tick": { + "avgMs": 0.0008413928855368853, + "p95Ms": 0.0011327000000164844 + }, + "physics_step": { + "avgMs": 0.016747402696686193, + "p95Ms": 0.02549669999971229 + }, + "physics_cleanup": { + "avgMs": 0.0019017873539182513, + "p95Ms": 0.0028274666673799706 + }, + "simulation_step": { + "avgMs": 0.02085331523004515, + "p95Ms": 0.03292626666655754 + }, + "entities_emit_updates": { + "avgMs": 0.0003370778280886417, + "p95Ms": 0.0005439999994753937 + }, + "send_all_packets": { + "avgMs": 0.07660242520250063, + "p95Ms": 0.42700786666658436 + }, + "network_synchronize_cleanup": { + "avgMs": 0.0027183606324976367, + "p95Ms": 0.004674099999950461 + }, + "network_synchronize": { + "avgMs": 0.09494421419603762, + "p95Ms": 0.4786903333333006 + }, + "world_tick": { + "avgMs": 0.07466210413063806, + "p95Ms": 0.25193186666753414 + }, + "ticker_tick": { + "avgMs": 0.10742549861440716, + "p95Ms": 0.293796333333133 + }, + "serialize_packets": { + "avgMs": 0.05803172000001357, + "p95Ms": 0.1558759999988979 + }, + "send_packets": { + "avgMs": 0.03413209769328384, + "p95Ms": 0.14359399999921152 + } + }, + "network": { + "totalBytesSent": 703270, + "totalBytesReceived": 0, + "maxConnectedPlayers": 50, + "avgBytesSentPerSecond": 8365.059304885883, + "maxBytesSentPerSecond": 250951.7791465765, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 11.727997034734143, + "maxPacketsSentPerSecond": 351.8399110420243, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0.05609737600015065, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "setup-world", + "durationMs": 147, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 3645, + "collected": false + }, + { + "name": "join-and-measure", + "durationMs": 30721, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 30, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/ai-memory/docs/perf-final-2026-03-05/results/blocks-10m-dense.json b/ai-memory/docs/perf-final-2026-03-05/results/blocks-10m-dense.json new file mode 100644 index 00000000..ae093e01 --- /dev/null +++ b/ai-memory/docs/perf-final-2026-03-05/results/blocks-10m-dense.json @@ -0,0 +1,101 @@ +{ + "timestamp": "2026-03-05T11:53:33.437Z", + "scenario": "blocks-10m-dense", + "durationMs": 78455, + "baseline": { + "avgTickMs": 0.14864898569747675, + "maxTickMs": 86.35801799999899, + "p95TickMs": 0.055144733333327166, + "p99TickMs": 1.5178473999989364, + "ticksOverBudgetPct": 0.05465775138010822, + "avgMemoryMb": 55.64536577860515, + "operations": { + "entities_tick": { + "avgMs": 0.0008150408151551575, + "p95Ms": 0.0011980499996449604 + }, + "physics_step": { + "avgMs": 0.016692221987835716, + "p95Ms": 0.026579683333693538 + }, + "physics_cleanup": { + "avgMs": 0.001609191176044596, + "p95Ms": 0.002377500000936076 + }, + "simulation_step": { + "avgMs": 0.0203698639435968, + "p95Ms": 0.033004716666679694 + }, + "entities_emit_updates": { + "avgMs": 0.0003852133996061862, + "p95Ms": 0.0006210500000330891 + }, + "world_tick": { + "avgMs": 0.14744808298771073, + "p95Ms": 0.051459149999755024 + }, + "ticker_tick": { + "avgMs": 0.17897410487854548, + "p95Ms": 0.09315956666638764 + }, + "send_all_packets": { + "avgMs": 0.19983364054329575, + "p95Ms": 0.005290416667169969 + }, + "network_synchronize_cleanup": { + "avgMs": 0.0021870705358342005, + "p95Ms": 0.0028079666664173903 + }, + "network_synchronize": { + "avgMs": 0.24119330498536626, + "p95Ms": 0.013638766666675413 + }, + "serialize_packets": { + "avgMs": 33.12660199999846, + "p95Ms": 65.62818299999708 + }, + "send_packets": { + "avgMs": 36.930843500000265, + "p95Ms": 72.85814300000129 + } + }, + "network": { + "totalBytesSent": 57477, + "totalBytesReceived": 0, + "maxConnectedPlayers": 1, + "avgBytesSentPerSecond": 890.4162553454629, + "maxBytesSentPerSecond": 53424.975320727775, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 0.09295018063003946, + "maxPacketsSentPerSecond": 5.5770108378023675, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 33.0944240000008, + "compressionCountTotal": 1 + } + }, + "phases": [ + { + "name": "setup-world", + "durationMs": 151, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 15012, + "collected": false + }, + { + "name": "join-and-measure", + "durationMs": 57607, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/ai-memory/docs/perf-final-2026-03-05/results/blocks-1m-dense.json b/ai-memory/docs/perf-final-2026-03-05/results/blocks-1m-dense.json new file mode 100644 index 00000000..91ec3279 --- /dev/null +++ b/ai-memory/docs/perf-final-2026-03-05/results/blocks-1m-dense.json @@ -0,0 +1,101 @@ +{ + "timestamp": "2026-03-05T11:52:07.868Z", + "scenario": "blocks-1m-dense", + "durationMs": 73802, + "baseline": { + "avgTickMs": 0.13430407329589647, + "maxTickMs": 24.70325799999955, + "p95TickMs": 0.2974614166668895, + "p99TickMs": 2.0355113833333536, + "ticksOverBudgetPct": 0.10762428362586213, + "avgMemoryMb": 54.630763880411784, + "operations": { + "entities_tick": { + "avgMs": 0.0006639741925769103, + "p95Ms": 0.0011167333337046633 + }, + "physics_step": { + "avgMs": 0.017991068591225012, + "p95Ms": 0.02801933333330453 + }, + "physics_cleanup": { + "avgMs": 0.0017506892953392905, + "p95Ms": 0.002781599998858534 + }, + "simulation_step": { + "avgMs": 0.021922096450537373, + "p95Ms": 0.03478115000013228 + }, + "entities_emit_updates": { + "avgMs": 0.00032803052653561355, + "p95Ms": 0.0004783166663097897 + }, + "send_all_packets": { + "avgMs": 0.17240404918693603, + "p95Ms": 0.4584230833332488 + }, + "network_synchronize_cleanup": { + "avgMs": 0.002145039332288079, + "p95Ms": 0.003171766666703964 + }, + "network_synchronize": { + "avgMs": 0.20826236343561627, + "p95Ms": 0.514821783333961 + }, + "world_tick": { + "avgMs": 0.1326782217833507, + "p95Ms": 0.2922409500009053 + }, + "ticker_tick": { + "avgMs": 0.16740631090351493, + "p95Ms": 0.3400438333335842 + }, + "serialize_packets": { + "avgMs": 2.6806935833329444, + "p95Ms": 8.010051000001113 + }, + "send_packets": { + "avgMs": 1.8336511000000408, + "p95Ms": 8.723202000001038 + } + }, + "network": { + "totalBytesSent": 63923, + "totalBytesReceived": 0, + "maxConnectedPlayers": 10, + "avgBytesSentPerSecond": 785.8555772119179, + "maxBytesSentPerSecond": 47151.33463271507, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 1.0941468074380223, + "maxPacketsSentPerSecond": 65.64880844628134, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 2.6758092916665337, + "compressionCountTotal": 10 + } + }, + "phases": [ + { + "name": "setup-world", + "durationMs": 152, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 10007, + "collected": false + }, + { + "name": "join-and-measure", + "durationMs": 57947, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/ai-memory/docs/perf-final-2026-03-05/results/blocks-1m-multi-world.json b/ai-memory/docs/perf-final-2026-03-05/results/blocks-1m-multi-world.json new file mode 100644 index 00000000..7dcda2fb --- /dev/null +++ b/ai-memory/docs/perf-final-2026-03-05/results/blocks-1m-multi-world.json @@ -0,0 +1,101 @@ +{ + "timestamp": "2026-03-05T11:55:42.705Z", + "scenario": "blocks-1m-multi-world", + "durationMs": 116960, + "baseline": { + "avgTickMs": 0.03945141325865762, + "maxTickMs": 28.31015599999955, + "p95TickMs": 0.03559149999980744, + "p99TickMs": 0.05915101111196337, + "ticksOverBudgetPct": 0.02001912939030629, + "avgMemoryMb": 39.942650519477, + "operations": { + "entities_tick": { + "avgMs": 0.0004684072115782438, + "p95Ms": 0.0006511222216229524 + }, + "physics_step": { + "avgMs": 0.009574272374033969, + "p95Ms": 0.01620499999985946 + }, + "physics_cleanup": { + "avgMs": 0.0009659701342487121, + "p95Ms": 0.0014869777783234085 + }, + "simulation_step": { + "avgMs": 0.011896159795362076, + "p95Ms": 0.019827333332634426 + }, + "entities_emit_updates": { + "avgMs": 0.00020322640959405248, + "p95Ms": 0.00029830000037489097 + }, + "send_all_packets": { + "avgMs": 0.04838580351200127, + "p95Ms": 0.0037267777780167913 + }, + "network_synchronize_cleanup": { + "avgMs": 0.0014979463776519976, + "p95Ms": 0.002417411110582179 + }, + "network_synchronize": { + "avgMs": 0.059486937447321045, + "p95Ms": 0.009280888888760172 + }, + "world_tick": { + "avgMs": 0.045497917225274076, + "p95Ms": 0.032147288889609625 + }, + "ticker_tick": { + "avgMs": 0.063352809372313, + "p95Ms": 0.06255761111145451 + }, + "serialize_packets": { + "avgMs": 2.1547827037037237, + "p95Ms": 5.93796199999997 + }, + "send_packets": { + "avgMs": 1.2724785467290045, + "p95Ms": 6.439845000000787 + } + }, + "network": { + "totalBytesSent": 261503, + "totalBytesReceived": 0, + "maxConnectedPlayers": 40, + "avgBytesSentPerSecond": 163.75788743479205, + "maxBytesSentPerSecond": 14738.209869131286, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 0.2586280368124615, + "maxPacketsSentPerSecond": 23.276523313121537, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 2.152255296296462, + "compressionCountTotal": 40 + } + }, + "phases": [ + { + "name": "setup-worlds", + "durationMs": 152, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 10007, + "collected": false + }, + { + "name": "joins-and-measure", + "durationMs": 102500, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 90, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/ai-memory/docs/perf-final-2026-03-05/results/blocks-500k-dense.json b/ai-memory/docs/perf-final-2026-03-05/results/blocks-500k-dense.json new file mode 100644 index 00000000..5f86d997 --- /dev/null +++ b/ai-memory/docs/perf-final-2026-03-05/results/blocks-500k-dense.json @@ -0,0 +1,101 @@ +{ + "timestamp": "2026-03-05T11:48:56.811Z", + "scenario": "blocks-500k-dense", + "durationMs": 74184, + "baseline": { + "avgTickMs": 0.14140416256201357, + "maxTickMs": 17.655299999998533, + "p95TickMs": 0.37285456666710765, + "p99TickMs": 2.4301144666666006, + "ticksOverBudgetPct": 0.053255225669018774, + "avgMemoryMb": 48.43299166361491, + "operations": { + "entities_tick": { + "avgMs": 0.0007039452957370873, + "p95Ms": 0.0010677333336388984 + }, + "physics_step": { + "avgMs": 0.01850606042002969, + "p95Ms": 0.028902466667204862 + }, + "physics_cleanup": { + "avgMs": 0.0017545602051715224, + "p95Ms": 0.0029788333332362526 + }, + "simulation_step": { + "avgMs": 0.022470014284345127, + "p95Ms": 0.03584373333363449 + }, + "entities_emit_updates": { + "avgMs": 0.0003423013221541184, + "p95Ms": 0.0005583499998768578 + }, + "world_tick": { + "avgMs": 0.13978336091750282, + "p95Ms": 0.3656250500001382 + }, + "ticker_tick": { + "avgMs": 0.17294758232436494, + "p95Ms": 0.4071851166677637 + }, + "send_all_packets": { + "avgMs": 0.1719061867245776, + "p95Ms": 0.6893992500004666 + }, + "network_synchronize_cleanup": { + "avgMs": 0.002408139111171516, + "p95Ms": 0.003817999999955646 + }, + "network_synchronize": { + "avgMs": 0.22191345575921226, + "p95Ms": 0.772945300000265 + }, + "serialize_packets": { + "avgMs": 1.198182509803827, + "p95Ms": 3.3448520000019926 + }, + "send_packets": { + "avgMs": 0.5832188880595591, + "p95Ms": 3.2410930000005465 + } + }, + "network": { + "totalBytesSent": 74510, + "totalBytesReceived": 0, + "maxConnectedPlayers": 20, + "avgBytesSentPerSecond": 733.0014737876572, + "maxBytesSentPerSecond": 43980.088427259434, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 2.292166734566154, + "maxPacketsSentPerSecond": 137.53000407396925, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 1.1953125294117746, + "compressionCountTotal": 20 + } + }, + "phases": [ + { + "name": "setup-world", + "durationMs": 148, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 10005, + "collected": false + }, + { + "name": "join-and-measure", + "durationMs": 58223, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/ai-memory/docs/perf-final-2026-03-05/results/combined.json b/ai-memory/docs/perf-final-2026-03-05/results/combined.json new file mode 100644 index 00000000..e62d5252 --- /dev/null +++ b/ai-memory/docs/perf-final-2026-03-05/results/combined.json @@ -0,0 +1,106 @@ +{ + "timestamp": "2026-03-05T10:19:41.066Z", + "scenario": "combined-stress", + "durationMs": 146434, + "baseline": { + "avgTickMs": 1.7669154759255734, + "maxTickMs": 119.1145120000001, + "p95TickMs": 1.315517916666431, + "p99TickMs": 4.684417033333982, + "ticksOverBudgetPct": 0.8499649331407365, + "avgMemoryMb": 63.636258125305176, + "operations": { + "entities_tick": { + "avgMs": 0.0728528046739081, + "p95Ms": 0.1221807416680349 + }, + "physics_step": { + "avgMs": 1.408065831620104, + "p95Ms": 0.6354630583336378 + }, + "physics_cleanup": { + "avgMs": 0.0030421108487570497, + "p95Ms": 0.00449195000016213 + }, + "simulation_step": { + "avgMs": 1.414739001908914, + "p95Ms": 0.645052208334073 + }, + "entities_emit_updates": { + "avgMs": 0.06617845378886872, + "p95Ms": 0.10611717500141822 + }, + "serialize_packets": { + "avgMs": 0.02305844670776388, + "p95Ms": 0.03494735000109964 + }, + "send_packets": { + "avgMs": 0.03796243016453969, + "p95Ms": 0.11168594166689824 + }, + "send_all_packets": { + "avgMs": 0.40120493518422384, + "p95Ms": 0.6376426833339792 + }, + "network_synchronize_cleanup": { + "avgMs": 0.00435136068161182, + "p95Ms": 0.00610366666696791 + }, + "network_synchronize": { + "avgMs": 0.4263879479175249, + "p95Ms": 0.6685033583338736 + }, + "world_tick": { + "avgMs": 1.7746799205055102, + "p95Ms": 1.3021827249979347 + }, + "ticker_tick": { + "avgMs": 1.8245079275256666, + "p95Ms": 1.3848611416666852 + } + }, + "network": { + "totalBytesSent": 32907550, + "totalBytesReceived": 0, + "maxConnectedPlayers": 10, + "avgBytesSentPerSecond": 266178.4396486531, + "maxBytesSentPerSecond": 396740.41631734296, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 294.4515047767222, + "maxPacketsSentPerSecond": 329.0251711324992, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0.02127565248229243, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "load-world", + "durationMs": 5110, + "collected": false + }, + { + "name": "spawn-all", + "durationMs": 760, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 10001, + "collected": false + }, + { + "name": "measure", + "durationMs": 119529, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 120, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/ai-memory/docs/perf-final-2026-03-05/results/entity-density.json b/ai-memory/docs/perf-final-2026-03-05/results/entity-density.json new file mode 100644 index 00000000..7c7c1f4c --- /dev/null +++ b/ai-memory/docs/perf-final-2026-03-05/results/entity-density.json @@ -0,0 +1,93 @@ +{ + "timestamp": "2026-03-05T10:28:40.896Z", + "scenario": "entity-density", + "durationMs": 74447, + "baseline": { + "avgTickMs": 0.35446141580684515, + "maxTickMs": 1.217609999992419, + "p95TickMs": 0.49880955000010846, + "p99TickMs": 0.6524828166667854, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 44.66063092549642, + "operations": { + "entities_tick": { + "avgMs": 0.016547232843148327, + "p95Ms": 0.025251083333538798 + }, + "physics_step": { + "avgMs": 0.13449901648284898, + "p95Ms": 0.18499565000129223 + }, + "physics_cleanup": { + "avgMs": 0.002322116057727884, + "p95Ms": 0.0033113166655918272 + }, + "simulation_step": { + "avgMs": 0.1396163369787168, + "p95Ms": 0.19289864999897569 + }, + "entities_emit_updates": { + "avgMs": 0.1810740421472613, + "p95Ms": 0.2781539166659362 + }, + "send_all_packets": { + "avgMs": 0.0023587110905412306, + "p95Ms": 0.0033070833336751094 + }, + "network_synchronize_cleanup": { + "avgMs": 0.004359490449905124, + "p95Ms": 0.005840849999397809 + }, + "network_synchronize": { + "avgMs": 0.01736202156468164, + "p95Ms": 0.024285166667020044 + }, + "world_tick": { + "avgMs": 0.35252660842303335, + "p95Ms": 0.4856061166657431 + }, + "ticker_tick": { + "avgMs": 0.3828609000750609, + "p95Ms": 0.5241929000011017 + } + }, + "network": { + "totalBytesSent": 0, + "totalBytesReceived": 0, + "maxConnectedPlayers": 0, + "avgBytesSentPerSecond": 0, + "maxBytesSentPerSecond": 0, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 0, + "maxPacketsSentPerSecond": 0, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "setup", + "durationMs": 181, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 10008, + "collected": false + }, + { + "name": "measure", + "durationMs": 58586, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/ai-memory/docs/perf-final-2026-03-05/results/idle.json b/ai-memory/docs/perf-final-2026-03-05/results/idle.json new file mode 100644 index 00000000..9e29d32c --- /dev/null +++ b/ai-memory/docs/perf-final-2026-03-05/results/idle.json @@ -0,0 +1,88 @@ +{ + "timestamp": "2026-03-05T10:12:51.999Z", + "scenario": "idle-baseline", + "durationMs": 34964, + "baseline": { + "avgTickMs": 0.04679685049276594, + "maxTickMs": 0.7618509999992966, + "p95TickMs": 0.08563006666624157, + "p99TickMs": 0.14175990000006397, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 40.66230392456055, + "operations": { + "entities_tick": { + "avgMs": 0.0013375285718636284, + "p95Ms": 0.0020908666665794347 + }, + "physics_step": { + "avgMs": 0.02362913038497697, + "p95Ms": 0.037648333333769796 + }, + "physics_cleanup": { + "avgMs": 0.0029344255942972772, + "p95Ms": 0.004569666666156991 + }, + "simulation_step": { + "avgMs": 0.02947926233473994, + "p95Ms": 0.04859916666643282 + }, + "entities_emit_updates": { + "avgMs": 0.0004343184789922846, + "p95Ms": 0.0007068999998409708 + }, + "world_tick": { + "avgMs": 0.04458446096775077, + "p95Ms": 0.07784126666668574 + }, + "ticker_tick": { + "avgMs": 0.07767732243819857, + "p95Ms": 0.125281199999669 + }, + "send_all_packets": { + "avgMs": 0.003018038009964449, + "p95Ms": 0.005092800000026424 + }, + "network_synchronize_cleanup": { + "avgMs": 0.0021036462653031196, + "p95Ms": 0.003053333333173214 + }, + "network_synchronize": { + "avgMs": 0.012486877813969597, + "p95Ms": 0.020697799999864704 + } + }, + "network": { + "totalBytesSent": 0, + "totalBytesReceived": 0, + "maxConnectedPlayers": 0, + "avgBytesSentPerSecond": 0, + "maxBytesSentPerSecond": 0, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 0, + "maxPacketsSentPerSecond": 0, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "warmup", + "durationMs": 5002, + "collected": false + }, + { + "name": "measure", + "durationMs": 29290, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 30, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/ai-memory/docs/perf-final-2026-03-05/results/join-storm.json b/ai-memory/docs/perf-final-2026-03-05/results/join-storm.json new file mode 100644 index 00000000..e04a1590 --- /dev/null +++ b/ai-memory/docs/perf-final-2026-03-05/results/join-storm.json @@ -0,0 +1,101 @@ +{ + "timestamp": "2026-03-05T10:09:09.199Z", + "scenario": "join-storm", + "durationMs": 91105, + "baseline": { + "avgTickMs": 3.8439880739882892, + "maxTickMs": 112.95012900000074, + "p95TickMs": 19.94523724999999, + "p99TickMs": 77.46513353333297, + "ticksOverBudgetPct": 3.7425877320890444, + "avgMemoryMb": 48.09654210408529, + "operations": { + "entities_tick": { + "avgMs": 0.0009194323091604177, + "p95Ms": 0.0014065166675209183 + }, + "physics_step": { + "avgMs": 0.1250473523586698, + "p95Ms": 0.16052406666658497 + }, + "physics_cleanup": { + "avgMs": 0.002340173183884654, + "p95Ms": 0.00359716666728976 + }, + "simulation_step": { + "avgMs": 0.1303260057996392, + "p95Ms": 0.16916449999947267 + }, + "entities_emit_updates": { + "avgMs": 0.0004420220462453582, + "p95Ms": 0.0007175666659046935 + }, + "send_all_packets": { + "avgMs": 6.925752767111098, + "p95Ms": 32.30038794999982 + }, + "network_synchronize_cleanup": { + "avgMs": 0.0034105134230800684, + "p95Ms": 0.0070909166668570835 + }, + "network_synchronize": { + "avgMs": 7.512317285671764, + "p95Ms": 35.00766645000058 + }, + "world_tick": { + "avgMs": 3.8963345294500464, + "p95Ms": 14.466868366666798 + }, + "ticker_tick": { + "avgMs": 4.646949241996682, + "p95Ms": 17.103415033332507 + }, + "serialize_packets": { + "avgMs": 14.845185557970833, + "p95Ms": 44.2485400000005 + }, + "send_packets": { + "avgMs": 1.0592896467176869, + "p95Ms": 0.037754999997559935 + } + }, + "network": { + "totalBytesSent": 37004083, + "totalBytesReceived": 0, + "maxConnectedPlayers": 100, + "avgBytesSentPerSecond": 47830.810050770735, + "maxBytesSentPerSecond": 2869848.603046244, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 6.217319217022814, + "maxPacketsSentPerSecond": 373.0391530213688, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 14.841901460144893, + "compressionCountTotal": 100 + } + }, + "phases": [ + { + "name": "preload-world", + "durationMs": 6441, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 10007, + "collected": false + }, + { + "name": "join-and-measure", + "durationMs": 68940, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/ai-memory/docs/perf-final-2026-03-05/results/large-world.json b/ai-memory/docs/perf-final-2026-03-05/results/large-world.json new file mode 100644 index 00000000..f020520d --- /dev/null +++ b/ai-memory/docs/perf-final-2026-03-05/results/large-world.json @@ -0,0 +1,98 @@ +{ + "timestamp": "2026-03-05T10:15:38.029Z", + "scenario": "large-world", + "durationMs": 83958, + "baseline": { + "avgTickMs": 0.27312384526383476, + "maxTickMs": 0.8103859999973793, + "p95TickMs": 0.3799384999991768, + "p99TickMs": 0.5230436666655199, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 88.8380968729655, + "operations": { + "entities_tick": { + "avgMs": 0.025279676413873416, + "p95Ms": 0.04658946666756189 + }, + "physics_step": { + "avgMs": 0.20503839756718564, + "p95Ms": 0.28973085000009935 + }, + "physics_cleanup": { + "avgMs": 0.002534760739664471, + "p95Ms": 0.004096933333312336 + }, + "simulation_step": { + "avgMs": 0.21045242650494736, + "p95Ms": 0.2990760833338451 + }, + "entities_emit_updates": { + "avgMs": 0.020149946157692927, + "p95Ms": 0.03197119999967981 + }, + "world_tick": { + "avgMs": 0.271353897934955, + "p95Ms": 0.3954721666663318 + }, + "ticker_tick": { + "avgMs": 0.3032136743891168, + "p95Ms": 0.4411740833342265 + }, + "send_all_packets": { + "avgMs": 0.0018746731203238406, + "p95Ms": 0.0033366166666382925 + }, + "network_synchronize_cleanup": { + "avgMs": 0.0035733975726565527, + "p95Ms": 0.00537756666635687 + }, + "network_synchronize": { + "avgMs": 0.017516659663546427, + "p95Ms": 0.026397666666161966 + } + }, + "network": { + "totalBytesSent": 0, + "totalBytesReceived": 0, + "maxConnectedPlayers": 0, + "avgBytesSentPerSecond": 0, + "maxBytesSentPerSecond": 0, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 0, + "maxPacketsSentPerSecond": 0, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "load-world", + "durationMs": 6114, + "collected": false + }, + { + "name": "spawn-bots", + "durationMs": 197, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 10006, + "collected": false + }, + { + "name": "measure", + "durationMs": 58006, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/ai-memory/docs/perf-final-2026-03-05/results/many-players.json b/ai-memory/docs/perf-final-2026-03-05/results/many-players.json new file mode 100644 index 00000000..66877308 --- /dev/null +++ b/ai-memory/docs/perf-final-2026-03-05/results/many-players.json @@ -0,0 +1,101 @@ +{ + "timestamp": "2026-03-05T10:17:05.112Z", + "scenario": "many-players", + "durationMs": 80745, + "baseline": { + "avgTickMs": 0.8746872841378011, + "maxTickMs": 4.222443999999086, + "p95TickMs": 2.5081412666680385, + "p99TickMs": 3.219694016666593, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 42.02790768941244, + "operations": { + "entities_tick": { + "avgMs": 0.05138953384694122, + "p95Ms": 0.09260073333268035 + }, + "physics_step": { + "avgMs": 0.041260081827029095, + "p95Ms": 0.0621942666659379 + }, + "physics_cleanup": { + "avgMs": 0.0019546326687081284, + "p95Ms": 0.0029245166662803966 + }, + "simulation_step": { + "avgMs": 0.045355334968446834, + "p95Ms": 0.06857985000048454 + }, + "entities_emit_updates": { + "avgMs": 0.04189781307972324, + "p95Ms": 0.06619103333323437 + }, + "world_tick": { + "avgMs": 0.8729773269334782, + "p95Ms": 2.462780899999537 + }, + "ticker_tick": { + "avgMs": 0.9051501613199358, + "p95Ms": 2.5023866833339223 + }, + "serialize_packets": { + "avgMs": 0.022628722083128678, + "p95Ms": 0.041281666666994475 + }, + "send_packets": { + "avgMs": 0.027797208580608634, + "p95Ms": 0.06443991666637885 + }, + "send_all_packets": { + "avgMs": 1.4330436181876212, + "p95Ms": 2.59864774999866 + }, + "network_synchronize_cleanup": { + "avgMs": 0.004993326950873297, + "p95Ms": 0.007568166666715115 + }, + "network_synchronize": { + "avgMs": 1.456699282547033, + "p95Ms": 2.6258489500005453 + } + }, + "network": { + "totalBytesSent": 60403050, + "totalBytesReceived": 0, + "maxConnectedPlayers": 50, + "avgBytesSentPerSecond": 1003501.3912498938, + "maxBytesSentPerSecond": 1417933.0762851927, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 1500.1696318543493, + "maxPacketsSentPerSecond": 1546.4201008697064, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0.020653985255427998, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "connect-clients", + "durationMs": 10004, + "collected": false + }, + { + "name": "spawn-bots", + "durationMs": 81, + "collected": false + }, + { + "name": "measure", + "durationMs": 57993, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/ai-memory/docs/perf-final-2026-03-05/results/multi-world.json b/ai-memory/docs/perf-final-2026-03-05/results/multi-world.json new file mode 100644 index 00000000..0f52515e --- /dev/null +++ b/ai-memory/docs/perf-final-2026-03-05/results/multi-world.json @@ -0,0 +1,88 @@ +{ + "timestamp": "2026-03-05T10:07:04.188Z", + "scenario": "multi-world", + "durationMs": 64049, + "baseline": { + "avgTickMs": 0.03324289287861414, + "maxTickMs": 1.2527769999996963, + "p95TickMs": 0.056213900000072196, + "p99TickMs": 0.09321036666650191, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 40.977216720581055, + "operations": { + "entities_tick": { + "avgMs": 0.000647179935009268, + "p95Ms": 0.001016316666058022 + }, + "physics_step": { + "avgMs": 0.01958448665007442, + "p95Ms": 0.03103574999987965 + }, + "physics_cleanup": { + "avgMs": 0.0014549152493084315, + "p95Ms": 0.002296966666881417 + }, + "simulation_step": { + "avgMs": 0.022994598524021994, + "p95Ms": 0.036961799999562575 + }, + "entities_emit_updates": { + "avgMs": 0.00029909848248830335, + "p95Ms": 0.0004827166661774148 + }, + "send_all_packets": { + "avgMs": 0.00160899185917926, + "p95Ms": 0.002542850000084703 + }, + "network_synchronize_cleanup": { + "avgMs": 0.0016132861723190263, + "p95Ms": 0.002282100000168915 + }, + "network_synchronize": { + "avgMs": 0.007937750397640869, + "p95Ms": 0.009497916667047928 + }, + "world_tick": { + "avgMs": 0.03205459396199441, + "p95Ms": 0.051640333333701466 + }, + "ticker_tick": { + "avgMs": 0.05589747594833992, + "p95Ms": 0.08802360000042124 + } + }, + "network": { + "totalBytesSent": 0, + "totalBytesReceived": 0, + "maxConnectedPlayers": 0, + "avgBytesSentPerSecond": 0, + "maxBytesSentPerSecond": 0, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 0, + "maxPacketsSentPerSecond": 0, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "setup", + "durationMs": 204, + "collected": false + }, + { + "name": "measure", + "durationMs": 58174, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/ai-memory/docs/perf-final-2026-03-05/results/stress.json b/ai-memory/docs/perf-final-2026-03-05/results/stress.json new file mode 100644 index 00000000..b132f65a --- /dev/null +++ b/ai-memory/docs/perf-final-2026-03-05/results/stress.json @@ -0,0 +1,93 @@ +{ + "timestamp": "2026-03-05T10:14:08.480Z", + "scenario": "stress-test", + "durationMs": 68941, + "baseline": { + "avgTickMs": 0.25662994207978596, + "maxTickMs": 2.328760999998849, + "p95TickMs": 0.43224216666612847, + "p99TickMs": 1.2698941333335219, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 46.99844512939453, + "operations": { + "entities_tick": { + "avgMs": 0.08646684851703983, + "p95Ms": 0.1446740333327701 + }, + "physics_step": { + "avgMs": 0.08340307568625353, + "p95Ms": 0.12420135000017278 + }, + "physics_cleanup": { + "avgMs": 0.00210539873287511, + "p95Ms": 0.0033580500001638334 + }, + "simulation_step": { + "avgMs": 0.08810355751048535, + "p95Ms": 0.13298310000024383 + }, + "entities_emit_updates": { + "avgMs": 0.05982602447784044, + "p95Ms": 0.09787540000033915 + }, + "send_all_packets": { + "avgMs": 0.0026767473524755537, + "p95Ms": 0.0041315499996623355 + }, + "network_synchronize_cleanup": { + "avgMs": 0.004177543891797372, + "p95Ms": 0.005747166666393847 + }, + "network_synchronize": { + "avgMs": 0.02764730996719078, + "p95Ms": 0.04289294999938041 + }, + "world_tick": { + "avgMs": 0.2544640216211446, + "p95Ms": 0.4325516666674048 + }, + "ticker_tick": { + "avgMs": 0.28816852412366506, + "p95Ms": 0.4896158333333915 + } + }, + "network": { + "totalBytesSent": 0, + "totalBytesReceived": 0, + "maxConnectedPlayers": 0, + "avgBytesSentPerSecond": 0, + "maxBytesSentPerSecond": 0, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 0, + "maxPacketsSentPerSecond": 0, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "spawn-entities", + "durationMs": 180, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 5001, + "collected": false + }, + { + "name": "measure", + "durationMs": 58086, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/ai-memory/docs/perf-framework-research-2026-03-05/SYNTHESIS-perf-framework-spec.md b/ai-memory/docs/perf-framework-research-2026-03-05/SYNTHESIS-perf-framework-spec.md new file mode 100644 index 00000000..f1edce75 --- /dev/null +++ b/ai-memory/docs/perf-framework-research-2026-03-05/SYNTHESIS-perf-framework-spec.md @@ -0,0 +1,965 @@ +# HYTOPIA Performance Framework - Research Synthesis & Specification + +**Date:** 2026-03-05 +**Sources:** 6 research documents covering HyFire2 (100+ perf branches, 63 curated PRs) and HYTOPIA SDK engine +**Scope:** Server profiling, client profiling, network monitoring, headless automation, mobile testing, CI/CD regression detection + +--- + +## Executive Summary + +### What Exists + +The HYTOPIA ecosystem already has substantial performance infrastructure, but it is **fragmented across repos, branches, and merge states**: + +- **HYTOPIA SDK (engine):** Built-in Sentry-based server telemetry (zero-overhead when disabled), client FPS/memory tracking via `PerformanceMetricsManager`, startup performance marks (`performance.mark`/`measure`), WebGL debug stats panel, automatic quality adjustment, and RTT measurement via SyncRequest/SyncResponse. The SDK provides the foundational hooks but no aggregation, no local dev profiling (Sentry-only), and no automated benchmarks. + +- **HyFire2 (game):** 63 curated performance PRs across 4 months (July-October 2025), 300+ hours of work. On master: a full profiling suite (`PerformanceManager`, `PerformanceMonitor`, `PerformanceProfiler`, `FrameBudgetMonitor`), 16+ analysis scripts, headless browser test scaffolding, baseline capture/comparison tools, YAML test scenarios, and 82 unique instrumentation points. Unmerged: 26 PRs including the most advanced monitoring (flame charts, spike detection with game state snapshots, Python trace analyzers, Excel analysis pipeline, particle stress tester). + +### What's Missing + +1. **No reusable performance framework** -- All tools are game-specific (HyFire2) or engine-specific (SDK). No shared toolkit exists for any HYTOPIA game developer. +2. **No local SDK profiling** -- `Telemetry.startSpan()` is a no-op without Sentry. Developers have zero built-in performance visibility during local development. +3. **No automated benchmarks** -- The `big-world` SDK example loads a large map but has no timing or measurement. No stress test example exists. +4. **No CI/CD performance gates** -- A 1,244-line infrastructure plan exists on an unmerged branch but nothing is built. +5. **No client frame breakdown** -- FPS is tracked but not frame-time components (render, JS, network, GC). +6. **No GPU profiling** -- WebGL draw calls and triangle counts are shown but no GPU millisecond timing. +7. **No cross-device pipeline** -- Chrome trace analysis scripts exist but require manual capture. No automated device testing. +8. **No server-to-client perf telemetry** -- No protocol packet carries server tick time or entity count to the client. + +### What We Need + +A **HYTOPIA Performance Framework** consisting of: +1. An SDK-level performance module (built into the engine, usable by any game) +2. A standalone benchmark runner (CLI tool for repeatable tests) +3. A headless device testing pipeline (Puppeteer/Playwright-based) +4. Trace analysis tools (Python/Node scripts for Chrome trace + CPU profile parsing) +5. A regression detector (CI/CD integration with baseline comparison) +6. A dashboard/reporter (HTML reports with historical comparisons) + +--- + +## Inventory of Existing Performance Code + +### Already Merged (Ready to Use) + +#### HYTOPIA SDK Engine (server + client) + +| Component | File | What It Does | +|-----------|------|-------------| +| Telemetry (Sentry spans) | `server/src/metrics/Telemetry.ts` | Wraps Sentry spans around tick subsystems. Zero-overhead no-op without Sentry. 12 span operations defined. | +| WorldLoop timing | `server/src/worlds/WorldLoop.ts` | Emits `TICK_END` with `tickDurationMs` every tick. SDK event any game can listen to. | +| Simulation timing | `server/src/worlds/physics/Simulation.ts` | Emits `STEP_END` with `stepDurationMs` for physics. | +| Ticker safeguards | `server/src/shared/classes/Ticker.ts` | `TICK_SLOW_UPDATE_CAP=2`, `MAX_ACCUMULATOR_TICK_MULTIPLE=3`. Prevents spiral-of-death. | +| IterationMap | `server/src/shared/classes/IterationMap.ts` | Custom Map+Array hybrid for ~2x faster iteration. Used in all sync queues. | +| Connection packet cache | `server/src/networking/Connection.ts` | Encode-once, send-to-N serialization cache. Gzip at level 1 for >64KB packets. | +| Network sync (30Hz) | `server/src/networking/NetworkSynchronizer.ts` | GC-aware queue clearing, reliable/unreliable packet splitting, lazy cache clearing. | +| SyncRequest/SyncResponse | `server/src/networking/`, protocol | RTT measurement every 2s. Server sends `r`, `s`, `p`, `n` timestamps. | +| PerformanceMetricsManager | `client/src/core/PerformanceMetricsManager.ts` | FPS (1s window), delta time, memory (Chrome-only `performance.memory`), refresh rate estimation. | +| DebugPanel | `client/src/core/DebugPanel.ts` | Stats.js FPS/MS/MB/RTT panels. lil-gui folders for WebGL, Entity, Chunk, GLTF, Audio, SceneUI, Arrow stats. | +| Stats classes | `client/src/entities/EntityStats.ts`, `chunks/ChunkStats.ts`, `gltf/GLTFStats.ts`, `audio/AudioStats.ts`, `arrows/ArrowStats.ts`, `ui/SceneUIStats.ts` | Per-subsystem static counters, reset each frame. | +| Startup marks | `client/src/network/NetworkManager.ts`, `client/src/chunks/ChunkManager.ts` | `performance.mark`/`measure` for connecting, connected, first-packet, first-chunk-batch, game-ready. | +| Quality auto-adjust | `client/src/settings/SettingsManager.ts` | ULTRA/HIGH/MEDIUM/LOW/POWER_SAVING presets. Auto-adjusts based on FPS with warmup, bounce protection, mobile cap. | +| View distance + frustum culling | `client/src/entities/Entity.ts`, `EntityManager.ts` | Squared distance checks, frustum culling, update skipping. | +| Renderer optimization | `client/src/core/Renderer.ts` | `matrixAutoUpdate=false` on all scenes, manual resets, FPS cap, custom transparent sort. | + +#### HyFire2 Game (on master) + +| Component | File | What It Does | +|-----------|------|-------------| +| PerformanceManager | `src/profiling/PerformanceManager.ts` | Spike detection (>50ms), auto CPU profiling on spike, heap snapshots at 800MB, SIGUSR1/SIGUSR2 signal handlers. Event loop lag detection. | +| InspectorCpuProfiler | `src/profiling/InspectorCpuProfiler.ts` | V8 Inspector API (`Profiler` domain) wrapper. Outputs `.cpuprofile` files compatible with Chrome DevTools. Signal-based profiling. | +| @Monitor decorator | `src/profiling/decorators.ts` | `@Monitor`, `@MonitorClass`, `monitorBlock()`, `monitorAsyncBlock()`. Wraps methods in `performanceManager.measure()`. | +| PerformanceMonitor | `src/utils/PerformanceMonitor.ts` | Sampling-based metrics with p50/p95/p99. Memory and event loop lag. 1s sampling, 10-min history. (Sampling currently DISABLED due to OOM.) | +| PerformanceProfiler | `src/utils/PerformanceProfiler.ts` | Manual call stack profiler. Exports collapsed stack format for flame graphs. 1ms sampling. | +| FrameBudgetMonitor | `src/utils/FrameBudgetMonitor.ts` | 16.66ms frame budget tracking. Spike threshold 8ms. Worst frame tracking. Top offenders. 82 unique `measure()` calls across codebase. | +| ClientPerformanceReporter | `src/utils/ClientPerformanceReporter.ts` | Server-side FPS report aggregator from clients. Issue detection (<30 FPS, >50ms frame time, >1GB memory). Performance tier classification. | +| ZoneVisibilityMonitor | `src/utils/ZoneVisibilityMonitor.ts` | Raycast skip rate tracking. 30s report interval. | +| benchmark-game.ts | `scripts/benchmark-game.ts` | Automated benchmarks: idle, 5v5_combat, 10v10_full, stress_test scenarios. JSON output. | +| profile-server-auto.ts | `scripts/profile-server-auto.ts` | Auto-saving ANSI dashboard. 30s auto-save to `performance-reports/`. | +| capture-baseline.sh | `scripts/capture-baseline.sh` | 5-min server run with bots, extracts perf.* events, calculates stats, saves JSON baseline. | +| compare-baselines.ts | `scripts/compare-baselines.ts` | Compares two baseline JSONs. Flags regressions >5%. Exit code 1 on regression. CI-ready. | +| analyze-performance.cjs | `scripts/analyze-performance.cjs` | Parses .cpuprofile, builds call tree, top 20 hot functions by self time. | +| analyze-full-profile.cjs | `scripts/analyze-full-profile.cjs` | Top 100 functions from CPU profile. Game vs system categorization. | +| map-profile-to-code.cjs | `scripts/map-profile-to-code.cjs` | Maps V8 profile functions to source code via CODEBASE_REF.md. | +| generate-flamegraph.ts | `scripts/generate-flamegraph.ts` | Interactive HTML flame graphs from collapsed stack format. | +| generate-flame-chart.cjs | `scripts/generate-flame-chart.cjs` | HTML flame chart from performance-reports JSON. | +| visualize-perf.js | `scripts/visualize-perf.js` | ASCII bar charts sorted by P99 latency. | +| analyze-bottleneck.js | `scripts/analyze-bottleneck.js` | Worst-frame JSON analysis. | +| analyze-entity-lookups.sh | `scripts/analyze-entity-lookups.sh` | Audits inefficient entity lookup patterns. | +| analyze-latest-session.ts | `scripts/analyze-latest-session.ts` | Latest session log analysis with full stat breakdown. | +| test-grenade-spike.ts | `scripts/test-grenade-spike.ts` | Automated Puppeteer test: buys grenades, dies, measures handleDeath. | +| headless-player.ts | `scripts/lib/headless-player.ts` | Puppeteer headless browser player for automated testing. | +| server-controller.ts | `scripts/lib/server-controller.ts` | Server lifecycle management for tests. | +| metrics-extractor.ts | `scripts/lib/metrics-extractor.ts` | NDJSON log parser with stat calculation. | +| scenario-types.ts | `scripts/lib/scenario-types.ts` | TypeScript types for YAML test scenarios. | +| grenade-death.yaml | `scenarios/grenade-death.yaml` | YAML scenario with thresholds and pass/fail criteria. | +| analyze-frame-budget.py | `analyze-frame-budget.py` | Chrome trace frame budget analysis. 60fps and 30fps targets. | +| analyze-recurring-blockers.py | `analyze-recurring-blockers.py` | Recurring frame blockers (>3 occurrences, skips startup). Impact scoring. | +| Debug UI config | `src/config/DebugUIConfig.ts` | Cached flag lookups for perf-relevant debug toggles. | + +### Unmerged (Needs Cherry-Picking) + +Ranked by value for a reusable framework, highest first. + +#### Tier 1: High Value, Ready to Adapt + +| Branch | Key Content | Lines Added | Why It Matters | +|--------|-------------|-------------|---------------| +| `feature/add-game-performance-monitoring` | FlameChartRecorder (Chrome Trace Event format), enhanced SpikeDetector with bot state snapshots, D3.js flame chart viewer, 12 analysis scripts | +10,306 | **Only source of Chrome Trace Event format output.** FlameChartRecorder is directly reusable for any HYTOPIA game. | +| `feature/performance-monitoring-improvements` | Enhanced FrameBudgetMonitor with hierarchical tracking, self-time calculation, interactive flamechart HTML export, performance context (strategy, zone, enemies) | +4,405 | **Self-time and hierarchy are critical for meaningful profiling.** Current FrameBudgetMonitor on master has flat operation tracking only. | +| `feature/performance-monitoring-ui` | PerformanceMetricsService, FunctionProfiler, SessionSpikeTracker, SystemProfiler, F9 client overlay | +2,311 | **The only real-time visual monitoring UI.** F9 overlay with Overview/Spikes/Logs/Operations tabs. | +| `feature/performance-analysis-combined` | 200+ per-function spike analyses, instrumentation guide with ROI ranking, actual code fixes with A/B validation report, 1.6M-line codebase call graph | +1,785,915 | **Most comprehensive analysis ever done.** The INSTRUMENTATION_GUIDE.md methodology is directly reusable. | +| `test/headless-browser-automation` | 6 Puppeteer scripts for automating game client through hytopia.com/play | +951 | **Foundation for all headless testing.** Working scripts with WSL2 WebGL workarounds documented. | +| `fix/memory-optimizations` | PathfindingCache (pool of 20 Maps/Sets), PlayerCache (pre-categorized, readonly), BotManager readonly array return | Unmerged PR | **Object pooling patterns** directly applicable to framework benchmarks. | + +#### Tier 2: Valuable, Needs Adaptation + +| Branch | Key Content | Lines Added | Why It Matters | +|--------|-------------|-------------|---------------| +| `feature/performance-analysis-tools` | Python CSV-to-Excel analysis pipeline. Generates XLSX with pivot tables, high-variance functions, spike details | +3,333 | **Structured analysis for non-developers.** Excel output is shareable with stakeholders. | +| `feature/particle-stress-tester` | ParticleStressTester class (8 scenarios: weapons, smoke, HE, molotov, flash, blood, stress, ramp), F2 menu | +831 | **Only particle stress testing tool.** Directly reusable pattern for framework stress scenarios. | +| `feature/performance-monitoring-hybrid` | PerformanceLagDetector (CPU polling every 50ms, spike snapshots), PerformanceMonitoringConfig interface | +698 | **Production-safe polling approach.** Alternative to function wrapping when overhead matters. | +| `feature/bot-cover-micro-profiler` | Prototype patching with Symbol guard, env-var gating (`BOT_COVER_PROF=1`), JSON report on exit | +1,269 | **Cleanest opt-in profiler design.** Symbol-guarded prototype patching prevents double-wrap. | +| `feature/mobile-performance-analysis` | 3 Python trace analyzers (TraceAnalyzer, FrameBudgetAnalyzer, RecurringBlockerAnalyzer) | +66,578 | **Mobile-specific analysis tools.** Chrome trace parsing for mobile device profiling. | +| `feature/baseline-lag-optimization` | Mobile frame skipping (30fps on mobile), viewmodel bob disable | +64,772 | **Mobile optimization patterns.** Frame skipping technique applicable to quality presets. | +| `investigation/perf-monitoring-analysis` | master-performance-analysis.cjs (768 lines, 6 analysis categories), WASM function mapping (6,684 Rapier functions), final-complete-analysis.cjs (3,374 lines) | +8,701 | **Deepest CPU profile analysis.** WASM mapping is unique -- only tool that can identify Rapier physics functions in profiles. | +| `sentry-testing` | SentryTelemetryService (1,593 lines), dual SDK integration, 30+ game-specific span operations | +941 | **Production monitoring.** Fully implemented, just needs merge and enable. | +| `fix/10v10-performance-analysis` | DistanceCullingService, OptimizedBroadcastService, tiered update rates by distance | Unmerged PR | **Network optimization patterns** for scaling to many players. | +| `test/arm64-simulation` | Docker ARM64 emulation matching AWS m7g.large | +3,228 | **Production environment simulation.** Good for compatibility (not performance) testing. | + +#### Tier 3: Documentation / Planning Only + +| Branch | Content | +|--------|---------| +| `docs/performance-testing-infrastructure` | 1,244-line roadmap for CI/CD perf testing. Phase 1-5 plan. GitHub Actions workflow template. | +| `docs/performance-monitoring-ultrathink-analysis` | 93KB technical deep-dive. Smart spike aggregation, tree filtering, Chrome Trace/Speedscope export research. | +| `analysis/performance-work-3mo` | 3-month retrospective. 63 curated PRs. 26 unmerged PR analysis. Quantified improvements. | +| `docs/performance-analysis-outputs` | CPU profile correlation with Trello cards. Dependency graph of 219 perf records. Baseline capture scripts. | +| `analysis/performance-monitoring-strategy` | 4-phase monitoring strategy. PerformanceLagDetector design. Sentry dual-SDK setup docs. | +| `analysis/code-hotspots-metrics` | Static hotspot analysis with before/after code examples. Spatial grid indexing proposal. | + +### Key Files & Locations + +#### HYTOPIA SDK Engine + +``` +/home/ab/GitHub/hytopia/work1/ +├── server/src/ +│ ├── metrics/Telemetry.ts # Sentry span wrapper (12 operations) +│ ├── worlds/WorldLoop.ts # TICK_START/TICK_END events +│ ├── worlds/physics/Simulation.ts # STEP_START/STEP_END events +│ ├── shared/classes/Ticker.ts # Fixed timestep with safeguards +│ ├── shared/classes/IterationMap.ts # Fast-iteration Map+Array +│ ├── networking/NetworkSynchronizer.ts # 30Hz sync, GC-aware clearing +│ ├── networking/Connection.ts # Packet cache, gzip, MTU handling +│ └── GameServer.ts # Start timing +├── client/src/ +│ ├── core/PerformanceMetricsManager.ts # FPS, memory, refresh rate +│ ├── core/DebugPanel.ts # Stats.js + lil-gui overlay +│ ├── core/Renderer.ts # WebGL stats, FPS cap, matrix opt +│ ├── settings/SettingsManager.ts # Quality auto-adjust +│ ├── network/NetworkManager.ts # RTT, performance marks +│ ├── chunks/ChunkManager.ts # Chunk performance marks +│ ├── entities/EntityStats.ts # Entity counters +│ ├── chunks/ChunkStats.ts # Chunk counters +│ ├── gltf/GLTFStats.ts # GLTF counters +│ ├── audio/AudioStats.ts # Audio counters +│ ├── arrows/ArrowStats.ts # Arrow counters +│ └── ui/SceneUIStats.ts # SceneUI counters +└── protocol/ + ├── packets/inbound/SyncRequest.ts # RTT request + ├── packets/outbound/SyncResponse.ts # RTT response + └── packets/inbound/DebugConfig.ts # Physics debug toggle +``` + +#### HyFire2 Game (master) + +``` +~/GitHub/games/hyfire2/ +├── src/profiling/ +│ ├── PerformanceManager.ts # Spike detection, auto CPU profiling +│ ├── InspectorCpuProfiler.ts # V8 Inspector API wrapper +│ └── decorators.ts # @Monitor, @MonitorClass, monitorBlock +├── src/utils/ +│ ├── PerformanceMonitor.ts # Sampling metrics, percentiles +│ ├── PerformanceProfiler.ts # Call stack profiler, flame graphs +│ ├── FrameBudgetMonitor.ts # Frame budget tracking (82 measure points) +│ ├── ClientPerformanceReporter.ts # Client FPS aggregation +│ └── ZoneVisibilityMonitor.ts # Raycast optimization monitoring +├── scripts/ +│ ├── benchmark-game.ts # Automated scenarios +│ ├── profile-server-auto.ts # ANSI dashboard + auto-save +│ ├── capture-baseline.sh # Baseline JSON capture +│ ├── compare-baselines.ts # Baseline regression comparison +│ ├── analyze-performance.cjs # CPU profile analysis +│ ├── analyze-full-profile.cjs # Top 100 functions +│ ├── map-profile-to-code.cjs # Profile-to-source mapping +│ ├── generate-flamegraph.ts # HTML flame graph +│ ├── generate-flame-chart.cjs # HTML flame chart +│ ├── visualize-perf.js # ASCII bar charts +│ ├── analyze-bottleneck.js # Worst-frame analysis +│ ├── analyze-entity-lookups.sh # Entity lookup audit +│ ├── analyze-latest-session.ts # Session log analysis +│ ├── test-grenade-spike.ts # Puppeteer perf test +│ └── lib/ +│ ├── headless-player.ts # Puppeteer game client +│ ├── server-controller.ts # Server lifecycle +│ ├── metrics-extractor.ts # NDJSON log parser +│ └── scenario-types.ts # YAML scenario types +├── scenarios/ +│ └── grenade-death.yaml # Test scenario definition +├── analyze-frame-budget.py # Chrome trace analysis +└── analyze-recurring-blockers.py # Recurring blocker analysis +``` + +--- + +## Techniques Catalog + +### Server-Side Profiling + +#### 1. V8 Inspector CPU Profiling +**Source:** HyFire2 `src/profiling/InspectorCpuProfiler.ts` + +Uses Node.js `inspector.Session` to capture V8 CPU profiles programmatically. Output is `.cpuprofile` JSON, loadable in Chrome DevTools or analyzable with custom scripts. + +```typescript +const session = new inspector.Session(); +session.connect(); +session.post('Profiler.enable'); +session.post('Profiler.start'); +// ... run for duration ... +session.post('Profiler.stop', (err, { profile }) => { + fs.writeFileSync('profile.cpuprofile', JSON.stringify(profile)); +}); +``` + +**Auto-trigger:** PerformanceManager triggers a 5s CPU profile capture (with 30s cooldown) when any operation exceeds the spike threshold. SIGUSR1 signal toggles manual profiling. + +#### 2. performance.now() Checkpoint Instrumentation +**Source:** HyFire2 `src/entities/GamePlayerEntity.ts` (50+ calls) + +The most common profiling pattern. Places `performance.now()` at section boundaries within a function, logs checkpoint data when total exceeds threshold. + +```typescript +function handleDeath() { + const perfStart = performance.now(); + const checkpoints: Record = {}; + + // Section 1 + const s1 = performance.now(); + doLogging(); + checkpoints.logging = performance.now() - s1; + + // Section 2 + const s2 = performance.now(); + calcHealth(); + checkpoints.healthCalc = performance.now() - s2; + + const total = performance.now() - perfStart; + if (total > 0.5) { + eventLogger.info('perf.handleDeath', { totalMs: total.toFixed(3), checkpoints }); + } +} +``` + +**Overhead:** Negligible (~0.001ms per `performance.now()` call). The conditional logging threshold prevents log spam. + +#### 3. @Monitor Decorators +**Source:** HyFire2 `src/profiling/decorators.ts` + +Zero-effort instrumentation via TypeScript decorators. Auto-detects sync vs async methods. + +```typescript +@MonitorClass() +class BotBrain { + // Every method automatically measured as "BotBrain.methodName" + think() { ... } + evaluateCombat() { ... } +} + +// Or per-method: +class GameManager { + @Monitor() + handleDeath() { ... } +} + +// Or inline: +monitorBlock('zone.lookup', () => findZoneForPosition(pos)); +``` + +#### 4. FrameBudgetMonitor.measure() +**Source:** HyFire2 `src/utils/FrameBudgetMonitor.ts` + +Wraps operations inline with frame-level budget tracking. 82 unique tracking points across HyFire2. + +```typescript +frameBudgetMonitor.startFrame(); +// ... per-operation: +frameBudgetMonitor.measure('brain.bombDetection.Alpha', () => { + detectBomb(); +}); +frameBudgetMonitor.endFrame(); +``` + +**Key metrics:** worst frame ever, top offenders (name + count + max + avg), recent spikes (last 50), frames over budget percentage. + +**Known issue:** The monitor itself causes 15.4% overhead (6.59% for measure() + 8.81% for SpikeDetector start/end). This is documented and needs to be addressed in the framework with adaptive sampling. + +#### 5. Sentry Telemetry Spans +**Source:** HYTOPIA SDK `server/src/metrics/Telemetry.ts`, HyFire2 `sentry-testing` branch + +Zero-overhead wrapping: `Telemetry.startSpan()` is a direct function call when Sentry is not initialized. When initialized, creates hierarchical spans filtered by tick time threshold. + +```typescript +Telemetry.startSpan({ op: TelemetrySpanOperation.ENTITIES_TICK }, () => { + entityManager.tickEntities(dt); +}); +``` + +**12 defined SDK span operations:** TICKER_TICK, WORLD_TICK, ENTITIES_TICK, SIMULATION_STEP, PHYSICS_STEP, PHYSICS_CLEANUP, ENTITIES_EMIT_UPDATES, NETWORK_SYNCHRONIZE, BUILD_PACKETS, SERIALIZE_PACKETS, SEND_PACKETS, SEND_ALL_PACKETS, NETWORK_SYNCHRONIZE_CLEANUP. + +**HyFire2 extends with 30+ game-specific operations** (in the unmerged SentryTelemetryService): GAME_TICK, BOT_TICK_ALL, BOT_BRAIN_THINK, BOT_NAVIGATION, BOT_COMBAT_SYSTEM, PLAYER_DAMAGE_CALC, WEAPON_FIRE, BOMB_PLANT, etc. + +#### 6. Chrome Trace Event Format Recording +**Source:** HyFire2 `feature/add-game-performance-monitoring` branch, `FlameChartRecorder.ts` + +Records operations in Chrome Trace Event format, loadable in `chrome://tracing`, Speedscope, or the custom D3.js viewer. + +```typescript +interface TraceEvent { + name: string; + cat: string; // category + ph: string; // 'B' (begin), 'E' (end), 'X' (complete) + ts: number; // timestamp in microseconds + pid: number; + tid: number; + dur?: number; + args?: any; +} +``` + +Uses logical thread IDs: MAIN=1, BOTS=2, PHYSICS=3, NETWORK=4. + +#### 7. Process Stats / Memory Monitoring +**Source:** HYTOPIA SDK `Telemetry.getProcessStats()`, HyFire2 `PerformanceManager` + +```typescript +// SDK: +Telemetry.getProcessStats(true) // { jsHeapSizeMb, jsHeapCapacityMb, rssSizeMb, ... } + +// HyFire2: +v8.writeHeapSnapshot() // Triggered when heap > 800MB +process.memoryUsage() // { heapUsed, heapTotal, rss, external } +``` + +**Known limitation:** Bun does not expose `process.cpuUsage()`. HyFire2's PerformanceMonitor uses a hardcoded 0.7/0.3 multiplier -- effectively fake CPU stats. + +#### 8. Prototype Patching with Symbol Guard +**Source:** HyFire2 `feature/bot-cover-micro-profiler` + +Opt-in, zero-setup profiling via prototype patching gated by environment variable. Uses Symbol to prevent double-wrapping. + +```typescript +const ENABLED = process.env.BOT_COVER_PROF === "1"; +if (ENABLED) { + const SYM = Symbol.for("PROFILER_INSTALLED"); + if (!proto[SYM]) { + proto[SYM] = true; + wrapMethod(proto, 'methodName', 'label'); + } +} +``` + +#### 9. Signal-Based Profiling +**Source:** HyFire2 `PerformanceManager` + +- `SIGUSR1` -- Toggle CPU profiling on/off +- `SIGUSR2` -- Generate performance report + +Useful for production environments where you cannot attach a debugger. + +### Client-Side Profiling + +#### 1. PerformanceMetricsManager +**Source:** HYTOPIA SDK `client/src/core/PerformanceMetricsManager.ts` + +- FPS: Averaged over 1-second windows via Three.js Clock +- Memory: `performance.memory.usedJSHeapSize` / `totalJSHeapSize` (Chrome-only) +- Refresh rate estimation: Samples 30 rAF deltas, trims 10% outliers, snaps to common rates (30/60/72/90/120/144/165/240/300/360) + +#### 2. Stats Classes (Per-Subsystem Counters) +**Source:** HYTOPIA SDK client, 6 stat classes + +All use static fields, reset per frame by their respective managers: + +| Class | Key Counters | +|-------|-------------| +| EntityStats | count, inViewDistanceCount, frustumCulledCount, updateSkipCount, animationPlayCount, localMatrixUpdateCount, worldMatrixUpdateCount | +| ChunkStats | count, visibleCount, blockCount, opaqueFaceCount, transparentFaceCount, liquidFaceCount | +| GLTFStats | fileCount, sourceMeshCount, clonedMeshCount, instancedMeshCount, drawCallsSaved | +| AudioStats | count, matrixUpdateCount, matrixUpdateSkipCount | +| ArrowStats | count, visibleCount | +| SceneUIStats | count, visibleCount | + +#### 3. WebGL Renderer Stats +**Source:** HYTOPIA SDK `client/src/core/Renderer.ts` + `DebugPanel.ts` + +Read from `renderer.info` (manual reset via `renderer.info.reset()` per frame): +- `render.calls` (draw calls) +- `render.triangles` +- `memory.geometries` +- `memory.textures` +- `programs.length` + +#### 4. Performance API Marks/Measures +**Source:** HYTOPIA SDK client + +Startup performance timeline (visible in browser DevTools): +- `NetworkManager:connecting` / `connected` / `connected-time` +- `NetworkManager:world-packet-received` / `connected-to-first-packet-time` +- `NetworkManager:game-ready-time` +- `ChunkManager:first-chunk-batch-built` / `first-chunk-batch-built-time` + +#### 5. Chrome DevTools Trace Parsing +**Source:** HyFire2 Python scripts (3 analyzers) + +Parses Chrome Performance tab JSON exports. Filters by `ph === 'X'` complete events with `dur` in microseconds. + +| Script | Focus | +|--------|-------| +| `analyze-trace.py` | Long tasks (>50ms), JS execution breakdown, rendering perf, frame time distribution | +| `analyze-frame-budget.py` | Per-call cost (not cumulative), 60fps/30fps budget violations, weapon animation deep-dive | +| `analyze-recurring-blockers.py` | Recurring issues (3+ occurrences, skips startup), impact score, periodic pattern detection | + +### Network Monitoring + +#### 1. RTT Tracking via SyncRequest/SyncResponse +**Source:** HYTOPIA SDK protocol + +Client sends SyncRequest every 2 seconds. Server responds with: +- `r`: server absolute time at request receipt +- `s`: server absolute time at response +- `p`: high-res processing time (ms) +- `n`: ms until next server tick + +Client calculates RTT: `clientReceiveTime - syncStartTime - serverProcessingTime`. Exponential moving average with smoothing factor 0.5. + +#### 2. Packet Size Monitoring +**Source:** HYTOPIA SDK `Connection.ts` + +Serialization telemetry records packet count, IDs, and serialized byte count in Sentry span attributes. Gzip compression triggers at 64KB. WebTransport unreliable datagrams capped at 1200 bytes (MTU). + +#### 3. WebTransport vs WebSocket Metrics +**Source:** HYTOPIA SDK `NetworkManager.ts` + +Client tracks send/receive protocol (ws/wt) shown in debug panel. WebTransport uses unreliable datagrams for entity position updates; WebSocket falls back for everything. No per-transport performance comparison is built in. + +### Optimization Patterns Found + +Ranked by measured impact: + +| Rank | Pattern | Impact | Where Used | +|------|---------|--------|-----------| +| 1 | **Death visibility deferred** | 99.6% faster (1.5ms -> 0.006ms) | `perf/optimize-death-visibility-check` | +| 2 | **Grenade disposal logging batched** | 97% faster (40-60ms -> 1.03ms) | `perf/cache-sceneui-takedamage` | +| 3 | **Stopping power require() cached** | 93% faster (3.45ms -> 0.24ms) | `perf/stopping-power-optimization` | +| 4 | **MVP tracking deferred** | 93% faster in 10v10 (0.7ms -> <0.05ms) | `perf/optimize-mvp-tracking` | +| 5 | **Debug logging removed from hot paths** | 90% overhead eliminated | `perf/remove-damage-debug-logging` | +| 6 | **Team elimination check cached** | 80% faster (0.98ms -> 0.2ms) | Various PRs | +| 7 | **Map draw calls reduced** | 70% reduction (1,597 -> 487) | Asset optimization | +| 8 | **setTimeout(fn, 0) deferral** | Spreads spikes across ticks | 5+ branches (audio, effects, physics, drops, MVP) | +| 9 | **handleDeath optimized** | 49.2% faster (3ms -> 1.5ms) | `perf/cache-sceneui-takedamage` | +| 10 | **Weapon drops sequential-deferred** | 42.8% faster | `perf/investigate-weapon-drop` | +| 11 | **Per-tick raycast cache (bidirectional)** | 39% raycast reduction | `perf/reduce-bot-combat-spike` | +| 12 | **Weapon attack optimized** | 35% faster (4.84ms -> 3.13ms) | `perf/reduce-weapon-fire-spike` | +| 13 | **Deferred evaluation (flag-based)** | 150-700x faster intel updates | `perf-monitoring-terrorist-approaches` | +| 14 | **Object/collection pooling** | 300+ allocations/sec eliminated | `fix/memory-optimizations` | +| 15 | **Set-based lookups** | O(n) -> O(1) | `perf/optimize-bot-updates` | +| 16 | **Squared distance comparison** | Eliminates Math.sqrt() | `perf/bomb-retrieval-optimization` | +| 17 | **Frame-duration caching** | Multiple-per-tick -> once-per-tick | `perf/optimize-recoil-offset-calls` | +| 18 | **Fire-and-forget logging** | Eliminates `await` in hot paths | `perf/bomb-retrieval-optimization` | +| 19 | **Rate-limited warnings** | 1/5s instead of 1/tick | `perf/rate-limit-bot-navigator-warnings` | +| 20 | **Web Audio API** | 5-20ms latency vs 100-200ms | `perf/web-audio-kill-sounds` | +| 21 | **Distance-based network LOD** | Tiered update rates (close/medium/far) | `fix/10v10-performance-analysis` | +| 22 | **Screen-space UI over SceneUI** | n^2 DOM layers -> single overlay | `fix/teammate-ui-performance-lag` | +| 23 | **Log spam rate limiting** | 99.7% reduction (3,600/min -> 12/min) | Various PRs | +| 24 | **Mobile frame skipping** | 30fps target on mobile | `feature/baseline-lag-optimization` | + +### Anti-Patterns Identified + +These recurring bad patterns were found across HyFire2 and should be detected by the framework's lint/audit tools: + +1. `await eventLogger.debug(...)` in loops -- blocks iteration on log I/O +2. `Array.from(map.values()).includes()` -- O(n) scan with intermediate array creation +3. `[...this._collection]` on every getter call -- constant array allocation +4. `require('module')` inside tick/damage handlers -- module resolution per call +5. `JSON.parse(JSON.stringify(obj))` for deep copy -- expensive serialization roundtrip +6. `sceneUIManager.getAllSceneUIs()` to find specific UI -- full collection scan +7. Verbose logging in per-tick, per-damage, per-entity paths -- string creation + object allocation overhead +8. Creating new Map/Set in pathfinding hot paths -- GC pressure at 300+/sec +9. Multiple raycasts to same target pair in same tick -- redundant physics work +10. Synchronous physics state changes on game events -- blocks main thread + +--- + +## Gaps & Requirements + +### What's Missing in the SDK + +| Gap | Severity | Description | +|-----|----------|-------------| +| No local profiling without Sentry | **Critical** | `Telemetry.startSpan()` is a no-op without Sentry DSN. Developers have zero built-in visibility during local dev. Need a lightweight local profiler that works without external services. | +| No client frame breakdown | **Critical** | Client tracks FPS but not per-frame time breakdown (render time, JS time, animation update time, network processing, GC). Cannot diagnose *why* frames are slow. | +| No per-entity cost attribution | **High** | `ENTITIES_TICK` is one span for all entities. No way to identify which entity's `tick()` callback is expensive. Need per-entity or per-entity-type timing. | +| No bandwidth metrics exposed | **High** | Serialized byte count recorded in Sentry span attributes but never aggregated or exposed to SDK consumers. No per-player bandwidth tracking, no packet-rate counters. | +| No server-to-client perf telemetry | **High** | No protocol packet carries server tick time, entity count, or any server-side metrics to the client. Debug panel cannot show server health. | +| No tick budget tracking | **High** | No system tracks percentage of 16.67ms tick budget consumed or warns when ticks consistently exceed budget. Ticker's catch-up cap is silent. | +| No GPU profiling | **Medium** | WebGL draw calls and triangles tracked but no GPU millisecond timing. `EXT_disjoint_timer_query_webgl2` not used. | +| No chunk meshing timing | **Medium** | ChunkWorker does greedy meshing in a Web Worker but has no timing instrumentation. Slow chunk builds are invisible. | +| No memory trend tracking | **Medium** | Memory sampled once per frame but no leak detection, no trend analysis, no growth warning. | +| No network jitter metrics | **Medium** | RTT tracked with exponential smoothing but no jitter calculation (variance), no packet loss counting, no out-of-order detection. | +| No GC monitoring | **Medium** | No GC event tracking. GC-aware clearing in NetworkSynchronizer is experience-based, not measured. | +| No entity count budget warnings | **Low** | Nothing warns when entity or chunk counts approach degradation thresholds. | +| Chrome-only memory tracking | **Low** | `performance.memory` is Chrome-only. Firefox/Safari users get no memory data. | +| Stats classes not time-series | **Low** | All Stats classes are instantaneous counters reset each frame. No historical data, min/max/avg, or percentiles. | + +### What's Missing in Tooling + +| Gap | Severity | Description | +|-----|----------|-------------| +| No CI/CD performance gates | **Critical** | A 1,244-line plan exists on a branch but nothing is built. No GitHub Actions for perf testing. No merge-blocking on regression. | +| No automated regression detection | **Critical** | `compare-baselines.ts` exists on HyFire2 master but is not wired into any CI pipeline and is game-specific. | +| No cross-device testing pipeline | **High** | Chrome trace analysis scripts exist but require manual capture from physical devices. No automated device farm. | +| No headless stress testing | **High** | Puppeteer scripts exist on unmerged branches. No merged headless test infrastructure. | +| No benchmark suite for SDK | **High** | The `big-world` example loads a large map but has no measurement. No standardized benchmark scenarios. | +| No deterministic replay | **Medium** | No way to replay exact game state for A/B comparisons. Tests depend on bot AI randomness. | +| No network condition simulation | **Medium** | Puppeteer CDP network throttling documented but not implemented in any test. | +| No shared performance results format | **Low** | Each tool outputs different formats (JSON, markdown, CSV, XLSX). No unified schema for cross-tool comparison. | + +--- + +## Framework Architecture Proposal + +### Core Components + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ HYTOPIA Performance Framework │ +│ │ +│ ┌──────────────────────┐ ┌──────────────────────┐ │ +│ │ 1. SDK Perf Module │ │ 2. Benchmark Runner │ │ +│ │ (built into engine) │ │ (standalone CLI) │ │ +│ │ │ │ │ │ +│ │ - Local profiler │ │ - Scenario loader │ │ +│ │ - Tick budget track │ │ - Bot spawner │ │ +│ │ - Entity cost attr. │ │ - Metric collector │ │ +│ │ - Network metrics │ │ - Baseline compare │ │ +│ │ - Client frame break │ │ - JSON/HTML output │ │ +│ │ - Perf telemetry pkt │ │ - YAML scenarios │ │ +│ └──────────┬───────────┘ └──────────┬───────────┘ │ +│ │ │ │ +│ ┌──────────┴───────────┐ ┌──────────┴───────────┐ │ +│ │ 3. Device Pipeline │ │ 4. Trace Analyzer │ │ +│ │ (Puppeteer/Playwright)│ │ (Python/Node) │ │ +│ │ │ │ │ │ +│ │ - Headless clients │ │ - Chrome trace parse │ │ +│ │ - Network throttle │ │ - CPU profile parse │ │ +│ │ - CDP metrics pull │ │ - WASM fn mapping │ │ +│ │ - GPU timing (ext) │ │ - Flame graph gen │ │ +│ │ - Mobile emulation │ │ - Spike aggregation │ │ +│ └──────────┬───────────┘ └──────────┬───────────┘ │ +│ │ │ │ +│ ┌──────────┴───────────┐ ┌──────────┴───────────┐ │ +│ │ 5. Regression Gate │ │ 6. Dashboard/Report │ │ +│ │ (CI/CD integration) │ │ (HTML reports) │ │ +│ │ │ │ │ │ +│ │ - GH Actions workflow│ │ - Historical trends │ │ +│ │ - Baseline capture │ │ - Before/after diffs │ │ +│ │ - Threshold gates │ │ - Flame charts │ │ +│ │ - PR comments │ │ - Device comparison │ │ +│ │ - Branch comparison │ │ - Exportable (JSON) │ │ +│ └──────────────────────┘ └──────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### Component 1: SDK Performance Module + +**Built into the HYTOPIA engine** as an opt-in module. Available to every game developer. + +**Server-side additions:** +- `PerfProfiler` class (replaces need for Sentry): Local span tracking with configurable output (console, JSON file, Chrome Trace Event format). Zero-overhead when disabled (same pattern as Telemetry). +- `TickBudgetTracker`: Tracks percentage of 16.67ms budget consumed. Emits warnings when avg exceeds configurable threshold (default 80%). Exposes to SDK event system. +- `EntityCostTracker`: Optional per-entity timing via `entity.tick()` wrapping. Activated per-world with `world.enableEntityProfiling()`. Groups by entity type. +- `NetworkMetrics`: Aggregates bandwidth per player, packet rate, compression ratio, queue depths. Exposed via SDK event and optional protocol packet. +- `PerfTelemetryPacket` (new protocol packet): Server sends tick time, entity count, physics step time, network sync time to client every N ticks (configurable, default 10 = every 333ms). + +**Client-side additions:** +- `FrameTimeBreakdown`: Splits frame time into render, JS, network deserialization, animation update, GC (via `PerformanceObserver` for longtask). Displayed in DebugPanel. +- `MemoryTrendTracker`: Rolling window of memory samples. Detects sustained growth (possible leak). Warns via console. +- `NetworkJitterTracker`: RTT variance calculation. Packet loss estimation (missed SyncResponses). Out-of-order detection via WorldTick sequence. +- `Stats classes upgrade`: Add rolling min/max/avg/p95 windows (last 60 seconds) to all Stats classes. + +### Component 2: Benchmark Runner + +**Standalone CLI tool** (`@hytopia.com/perf-bench`) that any game developer can install. + +**Features:** +- Loads YAML scenario files defining test conditions (entity count, player count, world size, duration, metric thresholds) +- Starts game server with `AUTO_START_WITH_BOTS=true` or custom game init +- Spawns headless browser clients via Puppeteer +- Collects metrics from both server events and client CDP +- Compares against baseline JSON +- Outputs JSON results + HTML report + +**Built-in scenarios (shipped with the tool):** +```yaml +# idle-server.yaml +name: Idle Server Baseline +duration: 60s +setup: + world_size: default + entities: 0 + players: 0 +metrics: + server_tick_avg: { max: 0.5ms } + server_memory_mb: { max: 200 } + +# entity-stress.yaml +name: Entity Stress Test +duration: 120s +setup: + entities: [100, 500, 1000, 5000] + ramp_interval: 30s +metrics: + server_tick_avg: { max: 8ms } + server_tick_p95: { max: 14ms } + +# player-stress.yaml +name: Player Stress Test +duration: 120s +setup: + headless_clients: [10, 25, 50, 100] + ramp_interval: 30s +metrics: + server_tick_avg: { max: 10ms } + client_fps_avg: { min: 30 } + network_rtt_p95: { max: 100ms } +``` + +### Component 3: Device Testing Pipeline + +**Puppeteer/Playwright-based** headless game client automation. + +**Capabilities:** +- Connect headless browser to game server (handling hytopia.com/play connection flow) +- Extract `renderer.info` stats via CDP +- Extract `PerformanceMetricsManager` data (FPS, memory) +- Capture Chrome DevTools traces programmatically +- Apply network throttling (3G, 4G, WiFi presets) +- Apply CPU throttling (2x, 4x, 6x) +- Apply device emulation (mobile screen sizes, touch, device scale factor) +- Multi-client spawning for stress tests +- Screenshot capture at configurable intervals + +**Adapted from:** HyFire2 `test/headless-browser-automation` branch scripts, with WSL2 WebGL workarounds. + +### Component 4: Trace Analyzer + +**Python/Node scripts** for offline analysis of captured traces and profiles. + +**Chrome DevTools Traces:** +- Long task detection (>50ms) +- Frame budget violation analysis (60fps and 30fps targets) +- Recurring blocker detection (3+ occurrences, impact scoring) +- Rendering breakdown (Layout, Paint, Composite) +- Game loop analysis (FunctionCall, EvaluateScript) + +**V8 CPU Profiles:** +- Call tree construction with self/total time +- Hot function identification (top N by self time) +- Game vs SDK vs system categorization +- WASM function mapping (6,684 Rapier physics operations) +- Spike cascade analysis +- "Death by 1000 cuts" analysis (frequent low-cost functions) + +**Output formats:** Markdown reports, interactive HTML flame charts, JSON data, CSV for spreadsheet analysis. + +### Component 5: Regression Detector + +**CI/CD integration** via GitHub Actions. + +```yaml +# .github/workflows/perf-gate.yml +name: Performance Gate +on: + pull_request: + branches: [master] + +jobs: + perf-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: master + - name: Capture baseline + run: hytopia-perf-bench run scenarios/baseline.yaml --output baseline.json + + - uses: actions/checkout@v4 + - name: Capture PR metrics + run: hytopia-perf-bench run scenarios/baseline.yaml --output pr.json + + - name: Compare + run: hytopia-perf-bench compare baseline.json pr.json --threshold 10 --fail-on-regression + + - name: Comment PR + if: always() + run: hytopia-perf-bench report baseline.json pr.json --format github-comment | gh pr comment $PR_NUMBER --body-file - +``` + +### Component 6: Dashboard/Reporter + +**Static HTML reports** generated from benchmark JSON results. + +**Features:** +- Historical trend charts (tick time, FPS, memory over runs) +- Before/after comparison tables with color-coded deltas +- Interactive flame charts (from Chrome Trace Event data) +- Per-device comparison matrix +- Exportable raw JSON for custom analysis +- GitHub PR comment summary format + +### Benchmark Scenarios + +| Category | Scenario | Parameters | Key Metrics | +|----------|----------|-----------|-------------| +| Baseline | Idle server | 0 entities, 0 players | tick time, memory, event loop lag | +| Entity stress | Progressive entity load | 100, 500, 1000, 5000 entities | tick time, memory, GC frequency | +| Player stress | Progressive player load | 10, 25, 50, 100 headless clients | tick time, network sync time, bandwidth, RTT | +| Physics stress | Many dynamic bodies | 100, 500, 1000 physics bodies in motion | physics step time, simulation step ms | +| Network stress | High packet rate | 50 players, all moving, 30Hz updates | serialize time, send time, bandwidth per player | +| Chunk loading | Large world | 750x750 block area, player teleporting | chunk build time (worker), main thread stall | +| Particle stress | Many emitters | All effects simultaneously (smoke, HE, blood, etc.) | client FPS, draw calls, GPU time | +| Combined | Real-game simulation | 10v10 with bots, combat, grenades, bomb plant/defuse | all metrics simultaneously | +| Startup | Cold start to gameplay | Connect, load, render first frame | time-to-connected, time-to-first-packet, time-to-first-chunk, time-to-game-ready | +| Mobile | Device emulation | CPU throttle 4x, mobile viewport, touch input | client FPS, frame budget violations, quality auto-adjust behavior | + +### Metrics to Capture + +#### Server Metrics + +| Metric | Source | Unit | Collection Rate | +|--------|--------|------|----------------| +| Tick time | WorldLoop TICK_END event | ms | Every tick (60Hz) | +| Physics step time | Simulation STEP_END event | ms | Every tick | +| Entity update time | New: EntityCostTracker | ms | Every tick (opt-in) | +| Network sync time | New: PerfProfiler span | ms | Every 2nd tick (30Hz) | +| Serialize time | New: PerfProfiler span | ms | Every sync | +| Send time per player | New: PerfProfiler span | ms | Every sync | +| Tick budget % | New: TickBudgetTracker | % | Every tick | +| Memory (heap) | process.memoryUsage() | MB | Every 1s | +| Memory (RSS) | process.memoryUsage() | MB | Every 1s | +| Entity count | EntityManager | count | Every 1s | +| Player count | PlayerManager | count | Every 1s | +| GC pauses | PerformanceObserver (if available) | ms | On GC event | +| Event loop lag | Interval jitter measurement | ms | Every 100ms | +| Bandwidth per player | New: NetworkMetrics | KB/s | Every 1s | +| Packet rate | New: NetworkMetrics | packets/s | Every 1s | +| Compression ratio | New: NetworkMetrics | ratio | Every sync | + +#### Client Metrics + +| Metric | Source | Unit | Collection Rate | +|--------|--------|------|----------------| +| FPS | PerformanceMetricsManager | fps | Every 1s | +| Frame time | PerformanceMetricsManager | ms | Every frame | +| Frame time breakdown | New: FrameTimeBreakdown | ms per component | Every frame | +| Draw calls | renderer.info.render.calls | count | Every frame | +| Triangles | renderer.info.render.triangles | count | Every frame | +| Geometries | renderer.info.memory.geometries | count | Every frame | +| Textures | renderer.info.memory.textures | count | Every frame | +| Shader programs | renderer.info.programs.length | count | Every frame | +| JS heap memory | performance.memory (Chrome) | MB | Every 1s | +| Entity count | EntityStats.count | count | Every frame | +| Visible entities | EntityStats.inViewDistanceCount | count | Every frame | +| Frustum-culled entities | EntityStats.frustumCulledCount | count | Every frame | +| Visible chunks | ChunkStats.visibleCount | count | Every frame | +| SceneUI count | SceneUIStats.count | count | Every frame | +| RTT | NetworkManager SyncResponse | ms | Every 2s | +| Jitter | New: NetworkJitterTracker | ms | Every 2s | +| GPU time | New: EXT_disjoint_timer_query | ms | Every frame (if available) | +| Quality preset | SettingsManager | enum | On change | + +#### Network Metrics + +| Metric | Source | Unit | Collection Rate | +|--------|--------|------|----------------| +| RTT | SyncRequest/SyncResponse | ms | Every 2s | +| RTT jitter | New: variance of RTT | ms | Every 2s | +| Packet rate (outbound) | New: NetworkMetrics | packets/s | Every 1s | +| Bandwidth (outbound) | New: NetworkMetrics | KB/s | Every 1s | +| Compression ratio | New: NetworkMetrics | ratio | Per compressed packet | +| Transport type | NetworkManager | ws/wt | On connect | +| Reliable vs unreliable split | New: NetworkMetrics | % | Every 1s | +| Missed SyncResponses | New: NetworkJitterTracker | count | Rolling window | + +### Repeatability Requirements + +1. **Deterministic bot scenarios:** Use seeded random for bot decisions. Record and replay action sequences. Fixed spawn positions. +2. **Bot-driven tests:** No human input required. Headless browser clients connect automatically, select team, and idle (or follow scripted actions). +3. **Scenario configuration via YAML/JSON:** All test parameters (entity count, duration, thresholds, bot behavior) defined in declarative files. No code changes needed per scenario. +4. **Baseline capture and comparison:** Every test run produces a JSON artifact. Compare any two runs with percentage-change calculations and configurable regression thresholds. +5. **Environment isolation:** Tests specify required server config (tick rate, physics params, network sync rate). Tests validate environment before running. +6. **Warmup period:** All scenarios include a configurable warmup period (default 10s) before metric collection begins, to exclude startup costs. + +--- + +## Priority Implementation Roadmap + +### Phase 1: Core Metrics (Weeks 1-3) + +**Goal:** Every HYTOPIA game developer can see performance data during local development without Sentry. + +**Tasks:** +1. **Add `PerfProfiler` to SDK server** -- Lightweight local span tracker. Same API as `Telemetry.startSpan()` but writes to console/file instead of Sentry. Enabled via `HYTOPIA_PERF=1` env var. +2. **Add `TickBudgetTracker` to SDK server** -- Wraps WorldLoop tick. Calculates budget percentage. Emits warning event when avg >80% over 5s window. Emits critical event when any tick >32ms (2x budget). +3. **Add `FrameTimeBreakdown` to SDK client** -- Uses `PerformanceObserver` for longtask, manual marks around render/network/animation phases. Adds 4 new Stats.js panels to DebugPanel. +4. **Add `PerfTelemetryPacket` to protocol** -- New outbound packet: `{ tickMs, entityCount, physicsMs, networkMs, memoryMb }`. Sent every 10 ticks. Client displays in DebugPanel. +5. **Upgrade Stats classes** -- Add rolling 60s windows with min/max/avg/p95 to all 6 Stats classes. Expose in DebugPanel. +6. **Extract HyFire2 tools** -- Pull `compare-baselines.ts`, `capture-baseline.sh`, `metrics-extractor.ts`, and `scenario-types.ts` from HyFire2 into a standalone `@hytopia.com/perf-tools` package. Make game-agnostic. + +### Phase 2: Automation (Weeks 4-6) + +**Goal:** Automated, repeatable performance tests that run without human interaction. + +**Tasks:** +1. **Build benchmark runner CLI** -- `hytopia-perf-bench` command. Loads YAML scenarios, starts server, runs test, collects metrics, outputs JSON. Adapts from HyFire2 `benchmark-game.ts`. +2. **Merge and adapt headless browser scripts** -- Clean up HyFire2's Puppeteer scripts into a reusable `HeadlessGameClient` class. Handle connection flow, team selection, idle state. +3. **Implement 4 core scenarios** -- Idle baseline, entity stress (100/500/1000), player stress (10/25/50 headless clients), startup timing. +4. **CI/CD GitHub Actions workflow** -- Capture baseline on master, compare against PR. Block merge on >10% regression. Post comparison table as PR comment. +5. **Adapt FlameChartRecorder** -- Pull from HyFire2 `feature/add-game-performance-monitoring` branch. Make SDK-native (opt-in). Output Chrome Trace Event JSON. + +### Phase 3: Cross-Device Testing (Weeks 7-9) + +**Goal:** Automated performance testing across different device profiles. + +**Tasks:** +1. **Device profile presets** -- YAML-defined profiles: desktop-high, desktop-low, mobile-flagship, mobile-midrange, tablet. Each specifies CPU throttle, viewport, network conditions. +2. **CDP metric collection** -- Automated extraction of `renderer.info`, `PerformanceMetricsManager` data, `performance.memory` from headless clients. Time-series collection over test duration. +3. **Chrome trace capture automation** -- Programmatic trace capture via CDP `Tracing` domain. Auto-analyze with Python scripts. +4. **Network condition simulation** -- Integrate Puppeteer CDP `Network.emulateNetworkConditions` with benchmark scenarios. Presets: perfect, broadband, 4G, 3G, lossy. +5. **HTML report generator** -- Comparative HTML report showing performance across device profiles. Trend charts, comparison matrix. + +### Phase 4: Advanced (Weeks 10-12+) + +**Goal:** Deep profiling capabilities for performance engineers. + +**Tasks:** +1. **GPU timing** -- Implement `EXT_disjoint_timer_query_webgl2` in SDK client for actual GPU millisecond measurement. Add to FrameTimeBreakdown. +2. **Per-entity cost attribution** -- Optional wrapping of `entity.tick()` with per-entity-type aggregation. Exposed via PerfProfiler. +3. **Network bandwidth dashboard** -- Per-player bandwidth tracking, packet-rate monitoring, reliable/unreliable split visualization. +4. **Memory leak detection** -- Automated heap growth detection in client. Alert when JS heap grows >X MB/minute sustained over 5 minutes. +5. **Deterministic replay system** -- Record entity state + player inputs. Replay for exact A/B comparison. Seeded random for bot AI. +6. **WASM function mapping** -- Adapt HyFire2's `wasm-mappings-report.json` and `update-wasm-mappings.cjs` for SDK-level Rapier physics profiling. Auto-discover WASM functions from CPU profiles. +7. **Speedscope/Chrome Trace export from SDK** -- Native export from PerfProfiler to industry-standard formats. Research complete (HyFire2 `docs/performance-monitoring-ultrathink-analysis`), implementation estimated at 2-4 hours for Speedscope, 6-10 hours for Chrome Trace. + +--- + +## Appendix: Branch Reference + +### HyFire2 Performance Branches (Complete Inventory) + +#### Analysis/Documentation Branches (11 branches) + +| Branch | Status | Key Content | Research Doc | +|--------|--------|-------------|-------------| +| `docs-performance` | Unmerged | PERFORMANCE_GUIDE.md, benchmark-game.ts, profile-server-auto.ts, generate-flamegraph.ts | Doc 1 | +| `analysis/performance-monitoring-strategy` | Unmerged | 4-phase monitoring strategy, PerformanceLagDetector, Sentry dual-SDK, bottleneck analysis scripts | Doc 1 | +| `analysis/code-hotspots-metrics` | Unmerged | PERFORMANCE_FIX_ANALYSIS.md (5 hotspots with before/after code), bug pattern analysis | Doc 1 | +| `docs/performance-analysis-oct-5` | Unmerged | handleDeath 49.2% improvement analysis, grenade-death.yaml scenario | Doc 1 | +| `docs/performance-analysis-outputs` | Unmerged | CPU profile correlation (42,725 functions, 7.5M samples), dependency graph (219 records), baseline capture/compare scripts | Doc 1 | +| `docs/performance-automation-playbook` | Unmerged | Profiling-to-fix workflow docs, AI memory files for perf fixes | Doc 1 | +| `docs/performance-monitoring-ultrathink-analysis` | Unmerged | 93KB deep-dive: spike aggregation, tree filtering, Speedscope/Chrome Trace export research, monitoring overhead analysis (15.4%) | Doc 1 | +| `docs/performance-testing-infrastructure` | Unmerged | 1,244-line CI/CD roadmap (5 phases), GitHub Actions template | Doc 1, 6 | +| `docs/perf-hotspots-20251004` | Unmerged | Top 10 hotspot baseline (10-min profile), monitoring overhead quantified | Doc 1 | +| `docs/top-10-performance-analysis` | Unmerged | Real player session analysis (6,900 events), 9 PRs merged Oct 5 | Doc 1 | +| `analysis/performance-work-3mo` | Unmerged | 3-month retrospective, 63 curated PRs, 26 unmerged PR analysis | Doc 1 | + +#### Feature/Monitoring Branches (18 branches) + +| Branch | Status | Commits Ahead | Key Content | Research Doc | +|--------|--------|--------------|-------------|-------------| +| `feature/performance-monitoring` | **Merged** | 0 | PerformanceProfiler, profile-server-auto.ts | Doc 3 | +| `feature/performance-monitoring-system` | **Merged** | 0 | Early monitoring attempt | Doc 3 | +| `feature/performance-monitoring-ui` | Unmerged | 12 | PerformanceMetricsService, FunctionProfiler, SessionSpikeTracker, SystemProfiler, F9 client overlay | Doc 3 | +| `feature/performance-monitoring-improvements` | Unmerged | 29 | Enhanced FrameBudgetMonitor (hierarchy, self-time), flamechart export, performance context | Doc 3 | +| `feature/performance-monitoring-hybrid` | Unmerged | 2 | PerformanceLagDetector (CPU polling), PerformanceMonitoringConfig | Doc 3 | +| `feature/performance-monitoring-ui-merge-master` | Unmerged | 19 | Rebased monitoring UI | Doc 3 | +| `feature/add-game-performance-monitoring` | Unmerged | 44 | FlameChartRecorder, SpikeDetector v2, D3.js viewer, 12 scripts | Doc 3 | +| `feature/perf-monitoring-terrorist-approaches` | **Merged** | 0 | Deferred evaluation pattern (150-700x speedup) | Doc 3 | +| `feature/performance-analysis-tools` | Unmerged | 4 | Python CSV-to-Excel pipeline | Doc 3 | +| `feature/performance-analysis-combined` | Unmerged | 216 | 200+ function analyses, instrumentation guide, code fixes, validation | Doc 3 | +| `feature/performance-analysis-reports` | Unmerged | 187 | Per-function analysis reports (subset of combined) | Doc 3 | +| `feature/performance-optimizations` | **Merged** | 0 | Early optimization pass | Doc 3 | +| `feature/baseline-lag-optimization` | Unmerged | 4 | Mobile frame skipping, viewmodel bob disable | Doc 3 | +| `feature/comprehensive-mobile-performance` | **Merged** | 0 | Blood particles, A14 analysis, periodic ops audit | Doc 3 | +| `feature/mobile-performance-analysis` | Unmerged | 4 | 3 Python trace analyzers | Doc 3 | +| `feature/investigate-mobile-viewmodel-performance` | **Merged** | 0 | Ambient-only lighting on mobile | Doc 3 | +| `feature/particle-stress-tester` | Unmerged | 16 | ParticleStressTester (8 scenarios), F2 menu | Doc 3 | +| `feature/bot-cover-micro-profiler` | Unmerged | 5 | Prototype patching profiler, Symbol guard, env-var gating | Doc 3 | + +#### perf/* Branches (13 merged + 4 open) + +| Branch | Status | PR # | Key Optimization | Impact | Research Doc | +|--------|--------|------|-----------------|--------|-------------| +| `perf/cache-sceneui-takedamage` | **Merged** | #1446 | Cache SceneUI array per player | Death: 50% faster | Doc 4 | +| `perf/stopping-power-optimization` | **Merged** | #1451 | Cache require(), early exit on empty | 93% faster | Doc 4 | +| `perf/reduce-weapon-fire-spike` | **Merged** | #1457 | setTimeout projectile creation, throttle recoil UI | 35% faster | Doc 4 | +| `perf/reduce-bot-combat-spike` | **Merged** | #1458 | RaycastCacheService (bidirectional) | 39% raycast reduction | Doc 4 | +| `perf/fix-damage-ui-spikes` | **Merged** | #1462 | Defer audio/blood/damage-direction UI | Spike elimination | Doc 4 | +| `perf/optimize-death-visibility-check` | **Merged** | #1463 | Defer physics updates on death | 99.6% faster | Doc 4 | +| `perf/investigate-weapon-drop` | **Merged** | #1465 | Sequential-deferred item drops | 42.8% faster | Doc 4 | +| `perf/optimize-mvp-tracking` | **Merged** | #1469 | Defer MVP tracking, string prefix bot check | 93% faster | Doc 4 | +| `perf/remove-damage-debug-logging` | **Merged** | #1470 | Remove debug logging from damage path | 90% overhead eliminated | Doc 4 | +| `perf/rate-limit-bot-navigator-warnings` | **Merged** | #1471 | 5s rate limit on per-tick warnings | Log spam eliminated | Doc 4 | +| `perf/remove-grenade-logging-overhead` | **Merged** | #1515 | Remove verbose grenade attack logging | 44 lines removed | Doc 4 | +| `perf/optimize-recoil-offset-calls` | **Merged** | #1255 | Frame-duration recoil cache | Multi-call -> once | Doc 4 | +| `perf/web-audio-kill-sounds` | **Merged** | #1678 | Web Audio API replaces HTML5 Audio | 5-20ms vs 100-200ms latency | Doc 4 | +| `perf/add-bot-combat-instrumentation` | Open | - | Instrumentation only (no optimization) | N/A | Doc 4 | +| `perf/bomb-retrieval-optimization` | Open | - | Fire-and-forget logging, squared distance | Unquantified | Doc 4 | +| `perf/optimize-bot-updates` | Open | - | Set-based lookups, extended caches | O(n^2) -> O(1) | Doc 4 | +| `perf/optimize-logging-overhead` | Open | - | Env-gated logging, EventLogger early exit | Unquantified | Doc 4 | + +#### fix/* Performance Branches (3 open) + +| Branch | Status | Key Content | Research Doc | +|--------|--------|-------------|-------------| +| `fix/10v10-performance-analysis` | Open | DistanceCullingService, OptimizedBroadcastService, tiered update rates | Doc 4 | +| `fix/teammate-ui-performance-lag` | Open | Screen-space UI replacing n^2 SceneUI nametags | Doc 4 | +| `fix/memory-optimizations` | Open | PathfindingCache pool, PlayerCache singleton, readonly array returns | Doc 4 | + +#### Testing/Infrastructure Branches + +| Branch | Status | Key Content | Research Doc | +|--------|--------|-------------|-------------| +| `test/headless-browser-automation` | Unmerged | 6 Puppeteer scripts for WSL2 | Doc 6 | +| `test/arm64-simulation` | Unmerged | Docker ARM64 emulation | Doc 6 | +| `sentry-testing` | Unmerged | SentryTelemetryService (1,593 lines) | Doc 6 | +| `investigation/perf-monitoring-analysis` | Unmerged | CPU profile analysis (6 scripts, WASM mapping) | Doc 6 | +| `feature/mobile-debug-ui` | Unmerged | Mobile controls debug (F8 overlay) | Doc 6 | +| `feature/sentry-telemetry` | Unmerged | Earlier Sentry attempt | Doc 6 | +| `feature/sentry-review` | Unmerged | Sentry usage guide | Doc 6 | + +### Summary Statistics + +- **Total performance branches analyzed:** 58+ +- **Total performance PRs (curated):** 63 +- **Merged to master:** 17 branches (13 perf/* + 4 feature/*) +- **Unmerged (valuable):** 26+ branches +- **Estimated total development time:** 300+ hours +- **Overall improvement:** Average game tick from 10ms+ to 1.11ms (89% reduction) +- **Frame budget utilization:** ~6.7% of 16.67ms budget (down from 60%+) +- **Unique instrumentation points:** 82 frameBudgetMonitor.measure() calls +- **Peak sprint:** 9 PRs merged on October 5, 2025 diff --git a/ai-memory/docs/perf-framework-research-2026-03-05/headless-automation-research.md b/ai-memory/docs/perf-framework-research-2026-03-05/headless-automation-research.md new file mode 100644 index 00000000..18b3bc8d --- /dev/null +++ b/ai-memory/docs/perf-framework-research-2026-03-05/headless-automation-research.md @@ -0,0 +1,687 @@ +# Headless Testing, Browser Automation & Performance Infrastructure Research + +**Date:** 2026-03-05 +**Scope:** HyFire2 (all branches + worktrees) and HYTOPIA SDK engine repo +**Researcher:** AI agent reviewing all branches and codebases + +--- + +## Table of Contents + +1. [Executive Summary](#executive-summary) +2. [HyFire2 Branch Inventory](#hyfire2-branch-inventory) +3. [Headless Browser Automation (Puppeteer)](#headless-browser-automation-puppeteer) +4. [Chrome DevTools Trace Analysis](#chrome-devtools-trace-analysis) +5. [CPU Profile Analysis Scripts](#cpu-profile-analysis-scripts) +6. [Sentry Performance Monitoring](#sentry-performance-monitoring) +7. [Server-Side Performance Monitoring](#server-side-performance-monitoring) +8. [Mobile Device Testing](#mobile-device-testing) +9. [ARM64 Production Simulation](#arm64-production-simulation) +10. [HYTOPIA SDK Engine Performance Code](#hytopia-sdk-engine-performance-code) +11. [CI/CD Performance Regression Plans](#cicd-performance-regression-plans) +12. [General Techniques Reference](#general-techniques-reference) +13. [Gaps and Recommendations](#gaps-and-recommendations) + +--- + +## Executive Summary + +HyFire2 has an extensive but fragmented performance testing ecosystem spread across 15+ branches. The work includes: + +- **Puppeteer headless browser scripts** (6 scripts, `test/headless-browser-automation` branch) for automating game client connections through hytopia.com/play +- **Chrome DevTools trace analysis** (3 Python scripts, `feature/mobile-performance-analysis` branch) for parsing Performance tab JSON exports from mobile Chrome +- **CPU profile analysis** (6+ Node.js scripts, `investigation/perf-monitoring-analysis` branch) for parsing `--cpu-prof` V8 profiles with WASM function mapping +- **Sentry telemetry** (1593-line service, `sentry-testing` branch) with dual Hytopia SDK + direct Sentry integration for production error/performance monitoring +- **Server-side profiling** (`PerformanceMonitor.ts`, `PerformanceProfiler.ts`, `PerformanceManager.ts`) for real-time metrics, flame graphs, and spike detection +- **Mobile debug UI** (`feature/mobile-debug-ui` branch) with drag-to-reposition mobile controls tester +- **ARM64 Docker simulation** (`test/arm64-simulation` branch) for testing on AWS Graviton3-equivalent environment +- **Performance testing infrastructure doc** (1244-line planning document, `docs/performance-testing-infrastructure` branch) with full roadmap + +**HYTOPIA SDK** has built-in `performance.mark`/`performance.measure` calls in the client (NetworkManager, ChunkManager), a `PerformanceMetricsManager` for FPS/memory tracking, a server-side `Telemetry` class wrapping Sentry spans, and a `DebugPanel` exposing WebGL draw calls, entity counts, chunk stats, and GLTF stats. It has NO headless browser tests, NO Puppeteer/Playwright, and NO GitHub Actions for performance testing. + +**Critical gap:** None of the headless browser work has been merged to master. The scripts exist only on feature branches. There is no CI/CD integration for automated performance regression detection. + +--- + +## HyFire2 Branch Inventory + +### Branches with performance/testing code (vs master) + +| Branch | Key Files | Status | +|--------|-----------|--------| +| `test/headless-browser-automation` | 6 Puppeteer scripts + server log | +951 lines, unmerged | +| `feature/mobile-performance-analysis` | 3 Python trace analyzers, analysis docs | +66,578 lines, unmerged | +| `feature/comprehensive-mobile-performance` | Per-frame budget analysis docs | Already merged to master | +| `feature/mobile-debug-ui` | Mobile controls debug JS/CSS/HTML tester | +1,322 lines, unmerged | +| `test/arm64-simulation` | ARM64 Docker runner + docs | +3,228 lines, unmerged | +| `sentry-testing` | SentryTelemetryService rewrite + analysis | +941 lines, unmerged | +| `investigation/perf-monitoring-analysis` | CPU profile scripts, WASM mappings, guides | +8,701 lines, unmerged | +| `docs/performance-testing-infrastructure` | 1244-line infrastructure planning doc | +1,244 lines, unmerged | +| `feature/sentry-telemetry` | Earlier Sentry integration attempt | Unmerged | +| `feature/sentry-review` | Sentry usage guide + config fixes | +130 lines, unmerged | +| `feature/player-stats-sentry-logging` | Player stats backup via Sentry | +64 lines, unmerged | +| `fix/device-info-from-ui-load` | Device info handshake for mobile detection | Already merged to master | + +### Branches that were merged/identical to master +- `test/performance-testing` (merged) +- `feat/performance-testing-infrastructure` (merged) +- `feature/comprehensive-mobile-performance` (merged) +- `fix/device-info-from-ui-load` (merged) +- `merge/performance-testing-with-master` (merged) + +### Worktrees (all on unrelated feature branches) +- `work1`: `test/sdk015-mapcomp` +- `work2`: `feature/gun-game-mode` +- `work3`: `feature/multi-world-investigation` +- `work4`: `feature/arena-queue-mode` +- `work5`: `feature/update-hytopia` +- `work6`: `fix/practice-arena-popup-investigation` +- `work7`: `fix/investigate-sensitivity-override` +- `work8`: `feature/gungame-deathmatch-20250905` + +None of the worktrees contain performance testing code. + +--- + +## Headless Browser Automation (Puppeteer) + +### Branch: `test/headless-browser-automation` + +Six TypeScript scripts using Puppeteer to automate connecting a browser client to HyFire2 through hytopia.com/play. + +#### Scripts + +**`scripts/working-headless-test.ts`** (245 lines) -- The most complete script. +- Launches Puppeteer with WebGL flags for WSL2 +- Navigates to `https://hytopia.com/play?localhost:8081` +- Handles connection dialogs (clicks OK buttons, types server address) +- Waits for game to load (checks `window.localPlayer`, `window.world`) +- Takes screenshots at each step +- 30-second observation window + +Key Puppeteer launch args for WebGL in WSL2: +``` +--use-gl=egl +--use-angle=swiftshader-webgl +--override-use-software-gl-for-headless +--enable-unsafe-swiftshader +--enable-webgl +--enable-webgl2 +``` + +Important: Uses `headless: false` (visible mode), not true headless. True headless Chrome has WebGL issues in WSL2. + +**`scripts/diagnose-browser.ts`** (148 lines) -- Diagnostic tool. +- Captures ALL console messages, network requests, page errors +- Saves diagnostics to `/tmp/browser-diagnostics.json` +- Reports element counts (buttons, inputs, canvases, divs) +- Useful for debugging why game client fails to load + +**`scripts/interactive-test.ts`** (165 lines) -- Button interaction test. +- Searches for buttons by text content (Play, Connect, OK, Continue, Join, Start) +- Searches for input fields and types server address +- Takes screenshots after each interaction step +- Progressive screenshot capture every 5 seconds + +**`scripts/working-automated-test.ts`** (108 lines) -- Keyboard-driven automation. +- Uses `page.keyboard.press('Enter')` instead of DOM clicks +- Handles first OK dialog, server input dialog, skip intro, team selection +- Simpler approach that works when button selectors are unreliable + +**`scripts/simple-headless-test.ts`** (79 lines) -- Minimal test. +- True `headless: true` mode (no WebGL flags) +- Navigates, waits 10 seconds, takes screenshot +- Checks `window.localPlayer`, `window.world`, `[data-team]`, `.buy-menu` +- Good for testing if basic connection works + +**`scripts/simple-wait-and-screenshot.ts`** (72 lines) -- Screenshot-only test. +- No page evaluation, no clicking +- Takes screenshots at 5, 10, 15, 20, 30, 40, 50, 60, 70, 80, 90 second intervals +- Useful for visual debugging of loading progress + +#### Server Test Log +`test-logs/server-2025-10-05T22-22-25-688Z.log` confirms the server successfully started during a headless test session, loaded 182 models, initialized WebRTC. + +#### Limitations Found +1. WSL2 requires `headless: false` with SwiftShader for WebGL +2. hytopia.com/play has connection dialogs that need automated interaction +3. Certificate issues with localhost:8081 (HTTPS self-signed) +4. No performance metric extraction yet -- scripts only test connectivity +5. No CDP protocol usage beyond Puppeteer's high-level API + +--- + +## Chrome DevTools Trace Analysis + +### Branch: `feature/mobile-performance-analysis` + +Three Python scripts for analyzing Chrome DevTools Performance tab trace JSON exports. + +#### `analyze-trace.py` (429 lines) -- `TraceAnalyzer` class +Parses Chrome trace JSON (`traceEvents` array) and analyzes: +- **Long tasks** (>50ms) that block the main thread +- **JavaScript execution** by category (EvaluateScript, v8.compile, v8.run, FunctionCall) +- **Rendering performance** (Layout, UpdateLayoutTree, Paint, CompositeLayers) +- **Frame times and jank detection** + +Key technique: Chrome trace events use `ph: 'X'` (complete events) with `dur` in microseconds. The script groups by `name` and `cat` fields. + +#### `analyze-frame-budget.py` (386 lines) -- `FrameBudgetAnalyzer` class +Per-frame analysis focused on individual calls that exceed the 16ms budget: +- Groups expensive calls (>16ms) by function name: count, min, max, avg +- Finds DrawFrame/Frame/BeginMainThreadFrame events for actual frame timing +- Compares against 60fps (16.67ms) and 30fps (33.33ms) targets +- Identifies which operations are present in slow frames + +#### `analyze-recurring-blockers.py` (372 lines) -- `RecurringBlockerAnalyzer` class +Focused on RECURRING issues (not one-offs): +- Skips first 5 seconds (startup/profiler initialization) +- Filters to operations >5ms that happen 3+ times during gameplay +- Calculates impact score: `count * avg_duration` +- Identifies periodic patterns (e.g., GC every N seconds) + +#### Supporting Data +- `mobile-a14-trace.json` -- Actual Chrome trace from Apple A14 device +- `recurring_blockers_raw_data.json` (61,007 lines) -- Raw extracted data from trace +- Several analysis markdown docs (MOBILE_PERFORMANCE_ANALYSIS.md, ACTUAL_CODE_ANALYSIS.md, etc.) + +#### Key Findings from Analysis +- 418 long tasks blocking main thread (>50ms each) +- Weapon viewmodel rendering: 17.7s total, 32.8ms average (2x frame budget) +- Blood effects NOT disabled on mobile (50-300 particles per hit) +- GPU tasks taking 30-73ms in worst frames +- Major GC pauses up to 528ms during gameplay + +--- + +## CPU Profile Analysis Scripts + +### Branch: `investigation/perf-monitoring-analysis` + +Node.js scripts for analyzing V8 `--cpu-prof` output (`.cpuprofile` JSON files) from the game server. + +#### `scripts/master-performance-analysis.cjs` (768 lines) -- Primary tool +Six analysis categories: +1. **Single Worst Spikes** -- Absolute worst individual function calls +2. **Spike Cascade Analysis** -- Full call stack breakdown when a function spikes +3. **Frequent Medium Spikes** -- Regular 0.2-3ms offenders (impact = avg * count) +4. **Death by 1000 Cuts** -- Functions called >5% of samples with <0.5ms individual cost +5. **Correlated Tick Overruns** -- Ticks exceeding 16ms budget with contributor breakdown +6. **Code Classification** -- Categorizes into game-hyfire, hytopia-sdk, physics-wasm, node, gc, idle + +Loads `CODEBASE_REF.md` for function-to-class mapping. Loads `wasm-mappings-report.json` for Rapier physics WASM function names. + +#### `scripts/final-complete-analysis.cjs` (3,374 lines) -- CSV-oriented analysis +Complete analysis with built-in WASM mapping of 6,684 Rapier physics operations. Outputs CSV file for external analysis. Advanced categorization with parent context for anonymous functions. + +#### `scripts/update-wasm-mappings.cjs` (363 lines) +Auto-discovers and maps WASM functions from CPU profiles. Uses context patterns to identify physics operations (collision, broad-phase, narrow-phase, joint, body, shape, solver, etc.). Updates mapping JSON. + +#### `scripts/export-spikes-csv.cjs` (90 lines) +Simple CSV export of all CPU spikes with tick number, duration, category, function name. + +#### `scripts/profile-full-report.cjs` (303 lines) +Generates markdown report from CPU profile. Includes tick window analysis (busy time per tick, spike count, largest spike). + +#### `wasm-mappings-report.json` (2,977 lines) +Pre-computed mapping of WASM function IDs to human-readable Rapier physics operation names. + +#### Usage Pattern +```bash +# Generate CPU profile +NODE_OPTIONS="--cpu-prof --cpu-prof-interval=100" hytopia start +# Let run 3-5 minutes with activity, then Ctrl+C + +# Analyze +node scripts/master-performance-analysis.cjs CPU.*.cpuprofile 16 +``` + +--- + +## Sentry Performance Monitoring + +### Branch: `sentry-testing` (primary), also `feature/sentry-telemetry`, `feature/sentry-review` + +#### `src/services/SentryTelemetryService.ts` (1,593 lines) + +Dual integration approach: +1. **Hytopia's Telemetry class** -- Auto tick monitoring, slow-tick filtering (>17-50ms threshold) +2. **Direct Sentry SDK v10.10.0** -- Custom transactions, enriched context, manual spans + +Key capabilities: +- `measurePerformance(name, callback)` -- Wraps functions in Sentry spans +- `trackGameOperation(op, callback, options)` -- Auto-warns when exceeding expected duration +- `startProfiling(name)` / `stopProfiling(name)` -- Manual span control with memory delta tracking +- `backupPlayerDataIfNeeded()` -- Sends player stats to Sentry as backup +- `backupGlobalLeaderboardIfNeeded()` -- Raw data dump of leaderboard state +- Periodic metrics reporting (every 60s) +- Memory tracking (every 30s) with heap baseline comparison + +Game-specific span operations: +``` +GAME_TICK, ROUND_PROCESS, BOT_TICK_ALL, BOT_BRAIN_THINK, +BOT_NAVIGATION, BOT_PATHFINDING, BOT_COMBAT_SYSTEM, +BOT_STRATEGY_SELECTION, PLAYER_DAMAGE_CALC, PLAYER_SPAWN, +PLAYER_DEATH, WEAPON_FIRE, ECONOMY_PURCHASE, UI_UPDATE, +BOMB_PLANT, BOMB_DEFUSE, BOMB_EXPLOSION, +ZONE_NAVIGATION_PATH, ZONE_PATHFIND_ALGORITHM, +AUDIO_PRIORITY_CALC, REPLAY_FRAME_CAPTURE, etc. +``` + +Configuration via `game-features.yaml` or environment variables: +- `SENTRY_DSN`, `SENTRY_ENABLED`, `FORCE_SENTRY_ENABLED` +- Sample rate, threshold, environment selection +- Toggle player action capture, bot decision capture, memory metrics + +#### SENTRY_USAGE.md (from `feature/sentry-review`) +- Sentry disabled by default in development +- Enable via `npm run start:sentry` or `FORCE_SENTRY_ENABLED=true` +- Production: always enabled with 1% transaction sampling + +#### SENTRY_INTEGRATION_ANALYSIS.md +- Documents the dual-init approach (Hytopia Telemetry + direct Sentry) +- Sentry queries: `transaction:"game.tick"`, `op:"metrics.report"`, `message:"Performance Metrics Report"` +- Files: `index.ts` (init), `SentryTelemetryService.ts` (implementation), `BotTickService.ts` (game tick wrapping) + +--- + +## Server-Side Performance Monitoring + +### On HyFire2 master (merged) + +#### `src/utils/PerformanceMonitor.ts` +- `process.hrtime.bigint()` high-precision timing +- Memory tracking (heap, RSS, external) +- Event loop lag detection with rolling averages +- Per-operation stats (p50, p95, p99) +- 1-second sampling interval, 10-minute history + +#### `src/utils/PerformanceProfiler.ts` +- Manual call stack tracking for flame graphs +- Function wrapping for automatic profiling +- 1ms sampling profiler +- Exports collapsed stack format for external tools (speedscope, flamegraph.pl) + +#### `src/profiling/PerformanceManager.ts` +- Spike threshold detection (default 50ms) +- Auto-triggers CPU profiling on spike +- Heap snapshots at 800MB threshold +- Signal handlers: SIGUSR1 (CPU profile), SIGUSR2 (perf report) + +#### `scripts/profile-server-auto.ts` +- Auto-saving performance monitor for WSL/non-interactive terminals +- Saves reports every 30 seconds to `performance-reports/` directory + +#### `scripts/benchmark-game.ts` +- Automated performance benchmarking using Bun-specific APIs +- Runs scenarios, collects metrics (avgTickTime, maxTickTime, memory, event loop lag) +- Outputs JSON results + +--- + +## Mobile Device Testing + +### Branch: `feature/mobile-debug-ui` + +#### `assets/ui/components/mobile-controls-debug.js` (537 lines) +In-game debug UI for mobile controls: +- Only active in Deathmatch FFA mode +- F8 key toggles debug mode +- Drag-to-reposition all mobile control elements +- Slider-based resizing of buttons/joystick +- Tracks all control elements: joystick, fire, jump, crouch, reload, drop, buy, bomb, scope, etc. +- Saves/restores custom positions + +#### `assets/ui/components/mobile-controls-debug.css` (376 lines) +CSS for the debug panel overlay. + +#### `mobile-controls-tester.html` (349 lines) +Standalone HTML page for testing mobile controls outside the game. Mock game background with all mobile control elements rendered. + +#### `serve-mobile-tester.sh` (26 lines) +Simple HTTP server to serve the mobile tester HTML locally. + +### Branch: `feature/mobile-performance-analysis` +The mobile performance analysis (Chrome traces from A14 device) is covered in the trace analysis section above. + +### Branch: `fix/device-info-from-ui-load` (merged to master) +Device detection handshake: +- Loads `device-detector.js` and `team-config.js` from CDN +- Client sends `device_detector_ready`, responds to `request-device-info` +- Server starts 5s timeout on ready, sends single request +- Anchored to UI LOAD event (no postMessage/fallbacks) + +--- + +## ARM64 Production Simulation + +### Branch: `test/arm64-simulation` + +#### `run-arm64-server.sh` (87 lines) +Docker-based ARM64 emulation matching AWS m7g.large: +- `--platform linux/arm64` with QEMU emulation +- `--cpus="2.0" --memory="8g"` resource limits +- Base image: `arm64v8/node:20` +- Installs Bun with 3-attempt retry logic +- Caches Bun in Docker volume +- Maps port 8080 + +#### `ARM64_PRODUCTION_TESTING.md` (139 lines) +Documentation covering: +- Expected 10-50x slower than native due to emulation overhead +- Good for compatibility testing, NOT for performance testing +- Quick start, monitoring commands, troubleshooting + +--- + +## HYTOPIA SDK Engine Performance Code + +### Client-Side (`/home/ab/GitHub/hytopia/work1/client/src/`) + +#### `core/PerformanceMetricsManager.ts` +- FPS measurement using Three.js Clock +- Refresh rate estimation (samples 30 frames, trims outliers, snaps to common rates) +- Memory tracking via `performance.memory` (Chrome only) +- Common refresh rates: 30, 60, 72, 90, 120, 144, 165, 240, 300, 360 + +#### `core/DebugPanel.ts` +Exposes via lil-gui: +- Player/camera position +- Server protocol info +- WebGL stats: drawCalls, geometries, programs, textures, triangles +- Entity stats: count, frustumCulled, animationPlay, lightLevelUpdate, etc. +- Chunk stats: visible, blockCount, opaque/transparent/liquid faces +- GLTF stats: fileCount, sourceMesh, clonedMesh, instancedMesh, drawCallsSaved +- SceneUI, Arrow, Audio stats + +#### `network/NetworkManager.ts` -- Performance marks +```typescript +performance.mark('NetworkManager:connecting'); +performance.mark('NetworkManager:connected'); +performance.measure('NetworkManager:connected-time', ...); +performance.mark('NetworkManager:world-packet-received'); +performance.measure('NetworkManager:connected-to-first-packet-time', ...); +performance.measure('NetworkManager:game-ready-time', ...); +``` + +#### `chunks/ChunkManager.ts` -- Performance marks +```typescript +performance.mark('ChunkManager:first-chunk-batch-built'); +performance.measure('ChunkManager:first-chunk-batch-built-time', 'NetworkManager:connected', ...); +``` + +### Server-Side (`/home/ab/GitHub/hytopia/work1/server/src/`) + +#### `metrics/Telemetry.ts` (252 lines) +- `TelemetrySpanOperation` enum: BUILD_PACKETS, ENTITIES_TICK, PHYSICS_STEP, NETWORK_SYNCHRONIZE, WORLD_TICK, etc. +- `Telemetry.initializeSentry(dsn, threshold)` -- Initializes Sentry with tick-time filtering +- `Telemetry.startSpan(options, callback)` -- Zero-overhead span wrapping (no-op without Sentry) +- `Telemetry.getProcessStats()` -- Heap, RSS, usage percentage +- `Telemetry.sentry()` -- Direct Sentry SDK access +- `beforeSendTransaction` filters to only send TICKER_TICK spans exceeding threshold + +### No Headless Testing +- `server/test/_setup.ts` contains only a placeholder comment +- No `client/test/` directory exists +- No Puppeteer/Playwright in any `package.json` +- No GitHub Actions workflows in the engine repo + +--- + +## CI/CD Performance Regression Plans + +### Branch: `docs/performance-testing-infrastructure` + +A 1,244-line planning document (`docs/PERFORMANCE_TESTING_INFRASTRUCTURE.md`) outlines a 5-phase roadmap: + +#### Phase 1: Foundation +- Install Puppeteer +- Create headless browser test scaffold +- Add test hooks to server (`ENABLE_TEST_HOOKS` env var) +- Baseline capture and comparison scripts + +#### Phase 2: Specific Fix Validation +- Test grenade death spike with automated scenario +- Before/after comparison with metrics proof + +#### Phase 3: Automated Testing Suite +- YAML scenario definitions (spawn bots, force loadout, kill player, measure) +- Scenario runner with action executors +- 5 core test scenarios planned + +#### Phase 4: CI/CD Integration +Proposed GitHub Actions workflow: +```yaml +# .github/workflows/performance-test.yml +on: + pull_request: + branches: [master] + +steps: + - Checkout master baseline + - Capture baseline metrics + - Checkout PR branch + - Capture PR metrics + - Compare (block merge if >10% regression) +``` + +#### Phase 5: Continuous Monitoring +- Production spike alerting +- Weekly performance reports +- Automatic profiling triggers + +**Current status:** Planning only. None of this CI/CD infrastructure has been built. + +--- + +## General Techniques Reference + +### Parsing Chrome DevTools Performance Traces + +Chrome Performance tab exports a JSON file with `traceEvents` array. Each event has: +- `name`: Operation name (FunctionCall, Paint, Layout, etc.) +- `cat`: Category (devtools.timeline, v8, blink, etc.) +- `ph`: Phase (`X` = complete, `B`/`E` = begin/end, `I` = instant) +- `ts`: Timestamp in microseconds +- `dur`: Duration in microseconds (for `ph: 'X'`) +- `args`: Event-specific data (URL, function name, etc.) +- `tid`: Thread ID +- `pid`: Process ID + +To find frame budget violations: filter for `ph === 'X'` events where `dur / 1000 > 16.67`. + +### Capturing Performance Metrics from Three.js with Puppeteer + +```javascript +const page = await browser.newPage(); +await page.goto(gameUrl); + +// Extract Three.js renderer stats via CDP +const stats = await page.evaluate(() => { + // Access Three.js renderer info + const renderer = /* get renderer reference */; + return { + drawCalls: renderer.info.render.calls, + triangles: renderer.info.render.triangles, + geometries: renderer.info.memory.geometries, + textures: renderer.info.memory.textures, + programs: renderer.info.programs?.length || 0, + fps: /* from PerformanceMetricsManager */, + memory: performance.memory ? { + usedHeap: performance.memory.usedJSHeapSize, + totalHeap: performance.memory.totalJSHeapSize, + limit: performance.memory.jsHeapSizeLimit + } : null + }; +}); +``` + +For continuous monitoring, use `setInterval` inside `page.evaluate` and collect data via `page.exposeFunction`. + +### Measuring WebTransport/WebSocket Latency + +Client-side approach using the existing heartbeat packet: +```typescript +// The HYTOPIA protocol already has bidirectional Heartbeat packets +// Client sends heartbeat, server echoes, measure round-trip + +const t0 = performance.now(); +sendHeartbeat(); +onHeartbeatResponse(() => { + const rtt = performance.now() - t0; + // rtt is the round-trip latency +}); +``` + +For automated testing, inject timing via Puppeteer: +```javascript +await page.evaluate(() => { + const originalSend = WebSocket.prototype.send; + WebSocket.prototype.send = function(data) { + performance.mark('ws-send-' + Date.now()); + return originalSend.call(this, data); + }; +}); +``` + +### Running Headless Game Clients for Stress Testing + +Pattern for multi-client stress testing: +```javascript +const browsers = await Promise.all( + Array(clientCount).fill(null).map(() => + puppeteer.launch({ + headless: 'new', // New headless mode + args: ['--no-sandbox', '--disable-gpu', '--use-gl=swiftshader'] + }) + ) +); + +// Each browser connects to the game server +for (const browser of browsers) { + const page = await browser.newPage(); + await page.goto(gameUrl); + // Automate connection flow... +} + +// Monitor server-side metrics while clients are connected +``` + +Note: In WSL2, SwiftShader is required for WebGL. Each headless Chrome uses 300-400MB RAM. For 10+ clients, consider running on a machine with ample memory. + +### Collecting GPU/Renderer Stats from Three.js + +Available via `renderer.info`: +```javascript +{ + render: { + calls: number, // Draw calls per frame + triangles: number, // Triangles rendered + points: number, + lines: number, + frame: number // Frame counter + }, + memory: { + geometries: number, // Active geometries + textures: number // Active textures + }, + programs: WebGLProgram[] // Active shader programs +} +``` + +Reset per frame with `renderer.info.reset()` to get per-frame stats. + +For GPU timing (Chrome-only, requires `EXT_disjoint_timer_query_webgl2`): +```javascript +const ext = gl.getExtension('EXT_disjoint_timer_query_webgl2'); +if (ext) { + const query = gl.createQuery(); + gl.beginQuery(ext.TIME_ELAPSED_EXT, query); + // render... + gl.endQuery(ext.TIME_ELAPSED_EXT); + // Read result next frame (async) + const elapsed = gl.getQueryParameter(query, gl.QUERY_RESULT); + const gpuTimeMs = elapsed / 1e6; +} +``` + +### Network Throttling with Puppeteer CDP + +```javascript +const client = await page.target().createCDPSession(); + +// Simulate 3G network +await client.send('Network.emulateNetworkConditions', { + offline: false, + downloadThroughput: 1.5 * 1024 * 1024 / 8, // 1.5 Mbps + uploadThroughput: 750 * 1024 / 8, // 750 Kbps + latency: 40 // 40ms RTT +}); + +// Device emulation +await client.send('Emulation.setDeviceMetricsOverride', { + width: 375, + height: 812, + deviceScaleFactor: 3, + mobile: true +}); + +// CPU throttling (4x slowdown) +await client.send('Emulation.setCPUThrottlingRate', { rate: 4 }); +``` + +### Lighthouse Integration + +```javascript +const lighthouse = require('lighthouse'); +const chromeLauncher = require('chrome-launcher'); + +const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] }); +const result = await lighthouse(gameUrl, { + port: chrome.port, + onlyCategories: ['performance'], + throttling: { + rttMs: 40, + throughputKbps: 10240, + cpuSlowdownMultiplier: 4 + } +}); +// result.lhr.categories.performance.score +``` + +Note: Lighthouse is designed for page load, not persistent game sessions. Use for initial load metrics only. + +--- + +## Gaps and Recommendations + +### Critical Gaps + +1. **No merged headless testing** -- All 6 Puppeteer scripts are on unmerged branches. No automated way to test client behavior. + +2. **No CI/CD performance gates** -- The `docs/performance-testing-infrastructure` branch has a full plan but zero implementation. + +3. **No client-side automated testing** -- HYTOPIA SDK has `server/test/_setup.ts` (empty placeholder) and no client test directory at all. + +4. **No WebTransport/WebSocket latency monitoring** -- The protocol has heartbeat packets but no automated latency measurement. + +5. **No GPU profiling pipeline** -- The DebugPanel exposes WebGL stats but there's no automated way to collect them over time. + +6. **Sentry not merged** -- The comprehensive SentryTelemetryService (1,593 lines) is still on `sentry-testing` branch. + +### What Exists and Works + +1. **Server CPU profiling** -- Mature toolchain with WASM mapping (6,684 Rapier functions), 6 analysis categories +2. **Server real-time monitoring** -- PerformanceMonitor, PerformanceProfiler, PerformanceManager all on master +3. **Client performance marks** -- NetworkManager and ChunkManager use `performance.mark`/`measure` +4. **Mobile trace analysis** -- Complete Python toolchain for Chrome DevTools trace JSON +5. **Sentry integration** -- Fully implemented, just needs to be merged and enabled + +### Recommended Next Steps + +1. Merge `sentry-testing` branch -- Production monitoring with zero effort +2. Merge `test/headless-browser-automation` -- Foundation for all future automation +3. Build on Puppeteer scripts to extract `renderer.info` stats over time +4. Implement the Phase 1 items from `docs/performance-testing-infrastructure` +5. Add `performance.measure` calls around entity rendering, chunk meshing, and network deserialization in the client +6. Create a simple CI action that runs headless browser connection test on every PR diff --git a/ai-memory/docs/perf-framework-research-2026-03-05/hyfire2-feature-perf-branches.md b/ai-memory/docs/perf-framework-research-2026-03-05/hyfire2-feature-perf-branches.md new file mode 100644 index 00000000..ca58aef1 --- /dev/null +++ b/ai-memory/docs/perf-framework-research-2026-03-05/hyfire2-feature-perf-branches.md @@ -0,0 +1,962 @@ +# HyFire2 Feature Performance Branches - Complete Research + +**Repo:** `~/GitHub/games/hyfire2` +**Date:** 2026-03-05 +**Total branches researched:** 18 + +--- + +## Table of Contents + +1. [BATCH 1 - Monitoring System Branches](#batch-1---monitoring-system-branches) + - [feature/performance-monitoring](#featureperformance-monitoring) (merged) + - [feature/performance-monitoring-system](#featureperformance-monitoring-system) (merged) + - [feature/performance-monitoring-ui](#featureperformance-monitoring-ui) (12 commits ahead) + - [feature/performance-monitoring-improvements](#featureperformance-monitoring-improvements) (29 commits ahead) + - [feature/performance-monitoring-hybrid](#featureperformance-monitoring-hybrid) (2 commits ahead) + - [feature/performance-monitoring-ui-merge-master](#featureperformance-monitoring-ui-merge-master) (19 commits ahead) + - [feature/add-game-performance-monitoring](#featureadd-game-performance-monitoring) (44 commits ahead) + - [feature/perf-monitoring-terrorist-approaches](#featureperf-monitoring-terrorist-approaches) (merged) + - [feature/performance-analysis-tools](#featureperformance-analysis-tools) (4 commits ahead) + - [feature/performance-analysis-combined](#featureperformance-analysis-combined) (216 commits ahead) + - [feature/performance-analysis-reports](#featureperformance-analysis-reports) (187 commits ahead) +2. [BATCH 2 - Optimization Branches](#batch-2---optimization-branches) + - [feature/performance-optimizations](#featureperformance-optimizations) (merged) + - [feature/baseline-lag-optimization](#featurebaseline-lag-optimization) (4 commits ahead) + - [feature/comprehensive-mobile-performance](#featurecomprehensive-mobile-performance) (merged) + - [feature/mobile-performance-analysis](#featuremobile-performance-analysis) (4 commits ahead) + - [feature/investigate-mobile-viewmodel-performance](#featureinvestigate-mobile-viewmodel-performance) (merged) + - [feature/particle-stress-tester](#featureparticle-stress-tester) (16 commits ahead) + - [feature/bot-cover-micro-profiler](#featurebot-cover-micro-profiler) (5 commits ahead) +3. [Cross-Cutting Architecture Summary](#cross-cutting-architecture-summary) +4. [Key Techniques Catalog](#key-techniques-catalog) + +--- + +## BATCH 1 - Monitoring System Branches + +--- + +### feature/performance-monitoring + +**Status:** Fully merged into master +**Commits:** 5 unique (all merged) +**Key commits:** +- `f54b510a6` docs: add comprehensive performance guide +- `16e22bd70` feat: add comprehensive performance monitoring system + +**What it added (now in master):** +- `PerformanceProfiler` - server-side profiling with `Bun.nanoseconds()` +- `profile-server-auto.ts` script for automated profiling in WSL terminals +- Performance monitoring documentation + +**Significance:** This was the first performance monitoring system. It established the baseline pattern of wrapping functions with `performance.now()` timing and saving JSON reports. + +--- + +### feature/performance-monitoring-system + +**Status:** Fully merged into master +**Commits:** 0 ahead (identical to master) + +Likely an earlier/duplicate branch that was merged before `feature/performance-monitoring`. + +--- + +### feature/performance-monitoring-ui + +**Status:** 12 commits ahead of master (NOT merged) +**Files changed:** 13 files, +2,311 / -40 lines + +**New files added:** +- `assets/ui/components/performance-monitor.js` (788 lines) - Client-side UI overlay +- `src/services/FunctionProfiler.ts` (236 lines) - Function-level profiling +- `src/services/PerformanceMetricsService.ts` (543 lines) - Centralized metrics collection +- `src/services/SessionSpikeTracker.ts` (218 lines) - Session-wide spike tracking +- `src/services/SystemProfiler.ts` (275 lines) - Per-system tick profiling + +**Architecture:** + +``` +Client UI (F9 toggle) Server Services ++-------------------+ +-------------------------------+ +| performance- | HTTP | PerformanceMetricsService | +| monitor.js |<------->| - collectSnapshot() (1/sec) | +| - Overview tab | | - getUIMetrics() | +| - Spikes tab | | - spikeThresholds | +| - Logs tab | +-------------------------------+ +| - Operations tab | | ++-------------------+ +----------+----------+ + | | + +-------------------+ +-------------------+ + | FunctionProfiler | | SessionSpikeTracker| + | - startFunction() | | - allTimeWorst[] | + | - endFunction() | | - recentSpikes[] | + | - wrap(fn) | | - spikePatterns | + +-------------------+ +-------------------+ + | + +-------------------+ + | SystemProfiler | + | - startTick() | + | - measureSystem() | + | - endTick() | + +-------------------+ +``` + +**Key code - PerformanceMetricsService singleton:** +```typescript +export class PerformanceMetricsService { + private spikes: PerformanceSpike[] = []; + private spikeThresholds = { + tick: 1, // 1ms - catch EVERYTHING + memory: 1000, // 1GB + eventloop: 1, // 1ms - super sensitive + operation: 5 // 5ms for operations + }; + private metricsHistory: MetricsSnapshot[] = []; + private maxHistorySize: number = 300; // 5 minutes at 1/sec + + public startCollection(): void { + this.updateInterval = setInterval(() => { + this.collectSnapshot(); + }, 1000); + } +} +``` + +**Key code - FunctionProfiler wrap pattern:** +```typescript +public wrap any>(fn: T, name: string, fileName: string = ''): T { + const profiler = this; + return function(this: any, ...args: any[]) { + profiler.startFunction(name, fileName); + try { + const result = fn.apply(this, args); + if (result instanceof Promise) { + return result.finally(() => profiler.endFunction()); + } + profiler.endFunction(); + return result; + } catch (error) { + profiler.endFunction(); + throw error; + } + } as T; +} +``` + +**Key code - SessionSpikeTracker:** +```typescript +interface DetailedSpike { + id: string; + timestamp: number; + duration: number; + functionName: string; + callStack: string[]; + context: { + botName?: string; + team?: string; + round?: string; + players?: number; + bots?: number; + }; +} +``` + +**UI Features:** +- F9 hotkey toggles full-screen overlay (90% width, 90vh height) +- Tabs: Overview, Spikes, Logs, Operations +- Green-on-black terminal aesthetic +- Pause/resume button for freezing data +- 500ms update rate +- Auto-scroll with lock + +**How to use:** Import and call `metricsService.startCollection()` in GameManager. Open client and press F9. + +--- + +### feature/performance-monitoring-improvements + +**Status:** 29 commits ahead of master (NOT merged) +**Files changed:** 14 files, +4,405 / -116 lines +**Relationship:** Superset of `performance-monitoring-ui` (contains all 12 commits + 17 more) + +**What it adds beyond the UI branch:** + +1. **Enhanced FrameBudgetMonitor** (272+ lines rewritten): + - Hierarchical operation tracking with parent-child relationships + - `selfTime` calculation (time in function excluding children) + - Concurrent load metrics per frame + - Rich `PerformanceContext` with strategy, zone, enemies, health, weapon + - Call stack capture for spikes + - Correlated log entries (before/during/after spike) + +2. **Interactive Flamechart Export** (`docs/FLAMECHART_EXPORT.md`, 345 lines): + - Generates standalone HTML with interactive flame chart visualization + - Color coding by self-time: Red (>80% = bottleneck), Orange (>50%), Yellow (>30%), Green (<30%) + - Search panel to find specific operations + - Zoom/pan controls + - Shows worst frame tree with drill-down + +3. **Markdown Report Export:** + - Removed heavy UI rendering in favor of lightweight MD export + - Phase A: Hierarchical tracking and call stack capture + - Phase B: Rich context and spike-log correlation + - Phase C: Drill-down modal for detailed spike analysis + +4. **Performance optimizations of the monitor itself:** + - Cached DOM elements to eliminate UI lag + - Stopped rebuilding DOM every second + - Reduced update rate from 2x/sec to 1x/sec + - Limited spikes window from 30s to 5s (max 20 items) + - 75% CPU reduction when debug UI is open + +**Key code - Enhanced FrameBudgetMonitor:** +```typescript +interface FrameOperation { + name: string; + duration: number; + timestamp: number; + parent: string | null; // Hierarchical tracking + depth: number; + selfTime: number; // Time excluding children + children: FrameOperation[]; + context?: PerformanceContext; +} + +interface SlowFrame { + totalDuration: number; + timestamp: number; + operations: FrameOperation[]; + exceedsBudget: boolean; + concurrentOperationsCount: number; + totalCPUTime: number; + heaviestOperations: FrameOperation[]; +} +``` + +**Key code - Flamechart color logic (in exported HTML):** +```javascript +function getFlameColor(selfTimePct) { + if (selfTimePct > 80) return '#ff4444'; // BOTTLENECK + if (selfTimePct > 50) return '#ff8800'; // Heavy + if (selfTimePct > 30) return '#ffaa00'; // Medium + return '#00ff88'; // Lightweight +} +``` + +--- + +### feature/performance-monitoring-hybrid + +**Status:** 2 commits ahead of master (NOT merged) +**Files changed:** 5 files, +698 / -25 lines + +**New files added:** +- `src/services/PerformanceLagDetector.ts` (427 lines) + +**Modified:** +- `src/config/gameConfig.ts` - Added `PerformanceMonitoringConfig` interface +- `src/services/SentryTelemetryService.ts` - Added 50-transaction cap per session +- `docs/PERFORMANCE_MONITORING.md` - Full documentation + +**Architecture - Different approach from the UI branch:** +Instead of wrapping individual functions, this uses a polling model: +- Checks CPU usage via `Telemetry.getProcessStats()` every 50ms +- Captures detailed snapshots (player states, bot states, strategies) during spikes +- Sends critical spikes to Sentry telemetry +- Generates periodic 30-second summary reports + +**Key code - PerformanceLagDetector:** +```typescript +export class PerformanceLagDetector { + private spikeThreshold = 40; // 40% CPU threshold + private consecutiveSpikes = 0; + + public initialize(): void { + this.checkInterval = setInterval(() => this.checkForSpikes(), 50); + this.reportInterval = setInterval(() => this.logPerformanceSnapshot(), 30000); + } + + private checkForSpikes(): void { + const stats = Telemetry.getProcessStats(false); + if (stats.cpuUsage > this.spikeThreshold) { + this.consecutiveSpikes++; + // Capture player/bot/round state snapshot + // Send to Sentry if critical + } + } + + public profile(name: string, fn: () => T): T { + const start = performance.now(); + try { return fn(); } + finally { this.trackOperation(name, performance.now() - start); } + } +} +``` + +**Config structure:** +```typescript +performanceMonitoring: { + enabled: false, // Master switch + lagDetection: true, + cpuThreshold: 40, // % CPU for spike detection + memoryThreshold: 500, // MB for warnings + reportInterval: 30, // seconds between reports + sentryTraceCap: 50, // max Sentry transactions/session + tickTimeThreshold: 50, // ms for slow tick warning + checkInterval: 50 // ms polling interval +} +``` + +**How to use:** Set `performanceMonitoring.enabled = true` in `gameConfig.ts`. System auto-starts on server boot. + +--- + +### feature/performance-monitoring-ui-merge-master + +**Status:** 19 commits ahead of master (NOT merged) +**Relationship:** This is `performance-monitoring-ui` rebased/merged onto a newer master, plus the `performance-monitoring-improvements` Phase 1-3 optimizations (DOM caching, reduced update rate, etc). Subset of `performance-monitoring-improvements`. + +No unique content beyond what's in the improvements branch. + +--- + +### feature/add-game-performance-monitoring + +**Status:** 44 commits ahead of master (NOT merged) +**Files changed:** 38 files, +10,306 / -70,940 lines (large diff due to `index.mjs` rebuild) + +**The most comprehensive monitoring branch.** Adds flame charts, spike detection with bot state capture, and many analysis scripts. + +**New files added:** +- `src/utils/FlameChartRecorder.ts` (550 lines) - Chrome Trace Event format recording +- `src/utils/SpikeDetector.ts` (676 lines, rewritten) - Spike detection with bot state snapshots +- `performance-reports/flame-chart-viewer.html` (905 lines) - D3.js-based flame chart viewer +- `debug-flame-chart.html` (41 lines) +- `PERFORMANCE_FIXES.md` (359 lines) +- 12 analysis scripts in `scripts/` + +**Analysis scripts:** +| Script | Purpose | +|--------|---------| +| `analyze-performance-spikes.ts` | Analyze stats JSON files, generate markdown reports | +| `analyze-all-8k-spikes.ts` | Process 8000+ spike events | +| `analyze-latest-run.ts` | Quick analysis of most recent game run | +| `analyze-latest-spikes.ts` | Recent spike summary | +| `analyze-spike-situations.ts` | Correlate spikes with game situations | +| `cross-reference-all-data.ts` | Cross-reference flame charts with spike logs | +| `dump-frame-monitor-data.ts` | Raw frame monitor data extraction | +| `export-spike-data-comprehensive.ts` | Full spike data export with all context | +| `export-spike-data-fast.ts` | Quick spike data export | +| `extract-all-spikes.ts` | Extract all spike events from logs | +| `extract-granular-operations.ts` | Per-operation timing extraction | +| `extract-spike-logs.ts` | Pull spike-related log entries | + +**Key code - FlameChartRecorder (Chrome Trace Events):** +```typescript +interface TraceEvent { + name: string; + cat: string; // category + ph: string; // phase: 'B' (begin), 'E' (end), 'X' (complete) + ts: number; // timestamp in microseconds + pid: number; + tid: number; + dur?: number; + args?: any; +} + +export class FlameChartRecorder { + private events: TraceEvent[] = []; + private readonly THREAD_MAIN = 1; + private readonly THREAD_BOTS = 2; + private readonly THREAD_PHYSICS = 3; + private readonly THREAD_NETWORK = 4; + + beginOperation(name: string, category: string, threadId: number = this.THREAD_MAIN): void { + this.events.push({ + name, cat: category, ph: 'B', + ts: (performance.now() - this.startTimeMs) * 1000, + pid: 1, tid: threadId + }); + } + + endOperation(name: string, category: string, threadId: number = this.THREAD_MAIN): void { + this.events.push({ + name, cat: category, ph: 'E', + ts: (performance.now() - this.startTimeMs) * 1000, + pid: 1, tid: threadId + }); + } + + // Output: loadable in chrome://tracing or Speedscope + saveToFile(): void { + const output = JSON.stringify({ traceEvents: this.events }); + fs.writeFileSync(path.join(this.config.outputPath, filename), output); + } +} +``` + +**Key code - SpikeDetector with bot state capture:** +```typescript +interface SpikeEvent { + timestamp: number; + operation: string; + durationMs: number; + percentOfBudget: number; + botStates: BotStateSnapshot[]; // Full state of every bot + gameContext: { + roundNumber: number; + roundTime: number; + bombPlanted: boolean; + bombTime: number | null; + teamScores: { ct: number; t: number }; + playersAlive: { ct: number; t: number }; + }; + stateChanges?: StateChange[]; // What changed since last spike +} +``` + +**Flame chart viewer** uses D3.js with: +- Dark theme (1e1e1e background) +- File upload for JSON trace files +- Pan/zoom controls +- Fixed Y-axis showing operation names +- Color-coded bars by duration + +**How to use:** +1. Enable flame chart: `flameChartRecorder.setEnabled(true)` or import `profile-server-auto.ts` +2. Run game for desired duration +3. Find output in `performance-reports/flame-chart-cumulative-*.json` +4. Open `flame-chart-viewer.html` and load the JSON file +5. Or load in `chrome://tracing` + +--- + +### feature/perf-monitoring-terrorist-approaches + +**Status:** Fully merged into master +**Commits:** 4 unique (all merged) + +**What it fixed:** +Found that `CTRotationManager.checkTerroristApproaches` was calling `evaluateRotationNeed()` on every terrorist intel update within the same tick. If 5 terrorists rushed B site in one tick, evaluation ran 5 times. + +**Solution: Deferred evaluation pattern:** +```typescript +// BEFORE: Each update triggers evaluation +updateTerroristIntel(botName, position) { + this.evaluateRotationNeed(); // Called 5x in one tick! +} + +// AFTER: Flag-based deferred evaluation +updateTerroristIntel(botName, position) { + this.pendingEvaluation = true; // Just set flag +} + +// GameManager tick loop: +for (bot of terrorists) checkApproach(bot); +if (rotationManager.hasPendingEvaluation()) { + rotationManager.evaluateRotationNeedDeferred(); // Called 1x per tick +} +``` + +**Result:** 150-700x faster intel updates, no spikes >2ms + +--- + +### feature/performance-analysis-tools + +**Status:** 4 commits ahead of master (NOT merged) +**Files changed:** 11 files, +3,333 lines (mostly new) + +**New files:** +- `scripts/performance-analysis/create_complete_analysis.py` (442 lines) +- `scripts/performance-analysis/create_pivot_analysis.py` (362 lines) +- `scripts/performance-analysis/extract_full_pivot.py` (128 lines) +- `scripts/performance-analysis/extract_issues.py` (261 lines) +- `scripts/performance-analysis/show_pivot_table.py` (61 lines) +- `scripts/performance-analysis/check_sheets.py` (31 lines) +- `scripts/performance-analysis/README.md` (186 lines) +- Output Excel files (COMPLETE_PERFORMANCE_ANALYSIS.xlsx, PIVOT_ANALYSIS.xlsx) + +**Python analysis pipeline:** +``` +Input CSVs (from Chrome DevTools CPU profiling): + profile_FunctionSummary.csv + profile_TickSummary.csv + profile_FunctionByTick_MAPPED.csv + | + v +create_complete_analysis.py + -> COMPLETE_PERFORMANCE_ANALYSIS.xlsx + Sheet 1: Overview + Sheet 2: HighVarianceFunctions (max/median > 5x) + Sheet 3: FunctionSpikeDetails (individual spike occurrences) + Sheet 4: FunctionGroups (correlated functions) + Sheet 5: HighTicks (all ticks > 3ms) + Sheet 6: GroupTickBreakdown + | + v +create_pivot_analysis.py + -> PIVOT_ANALYSIS.xlsx + Sheet 1: PivotTable (HyFire2 code only, ticks >= 1000) + Sheet 2: GroupsWithFunctionMS (per-function ms contributions) + Sheet 3: GroupSummary (one row per group) +``` + +**Key metrics to focus on (from README):** +- Variance Ratio (Max/Median): >10x = SPIKY +- TickNonIdle > 3ms: identifies problematic ticks +- Function Groups: identify event cascades (bomb plant -> retake logic) + +**How to use:** +```bash +# 1. Run game with CPU profiling +node --cpu-prof --cpu-prof-interval=100 server.js + +# 2. Convert .cpuprofile to CSVs + +# 3. Run analysis +cd /path/to/csv/data +python3 scripts/performance-analysis/create_complete_analysis.py +python3 scripts/performance-analysis/create_pivot_analysis.py +``` + +--- + +### feature/performance-analysis-combined + +**Status:** 216 commits ahead of master (NOT merged) +**Files changed:** 274 files, +1,785,915 lines +**Relationship:** Superset of `performance-analysis-tools` + `performance-analysis-reports` + +**The largest analysis branch.** Contains: + +1. **200+ individual function spike analyses** in `performance-reports/` and `spike-analysis/` directories + - Each is a markdown file analyzing one function + - Includes: metrics (max, p95, median, spike ratio), root cause, proposed fix with code + +2. **Instrumentation guide** (`INSTRUMENTATION_GUIDE.md`, 598 lines): + - Step-by-step deployment guide for performance instrumentation + - Recommended fix order by ROI: + 1. `setMovementInputs` - easiest, 60-80% improvement, 30 min + 2. `_isKnownWeaponClass` - replace require(), 70-90% improvement, 15 min + 3. `checkTerroristApproach` - cache bot lookups, 70-90% improvement, 45 min + 4. `handlePlayerInput` - cache debug config, 50-70% improvement, 30 min + 5. `isPlayerInAir` - fix Rust aliasing, 70-85% improvement, 15 min + +3. **Final spike analysis** (`FINAL_SPIKE_ANALYSIS.md`): + - 88 gameplay spikes ranked by severity + - Top offenders: pointInZone (36x spike), _handlePlayerInput (31x), getCurrentRecoilOffset (24x) + - Severity formula: spike_ratio * duration + +4. **Actual code optimizations applied** (in src/): + - `GamePlayerEntity.ts`: +260/-68 lines - optimized _handlePlayerInput, reduced grenade logging + - `MomentumPlayerController.ts`: optimized setMovementInputs + - `RecoilSystem.ts`: eliminated Vector3.clone() allocation + - `BotController.ts`: reduced string concatenation + - `CTRotationManager.ts`: cached bot lookups + - `PointInPolygon.ts`: optimized polygon checks + - `MovementAccuracySystem.ts`: throttled accuracy recalculation + +5. **Validation report** (`PERFORMANCE_VALIDATION_REPORT.md`): + - A/B test results proving fixes work + - 36% less log data with fixes applied + - 7% fewer errors + +6. **Codebase mapping** (`codebase-map.json`, 1.6M lines): + - Complete call graph analysis + - Function relationship mapping + - Used to trace spike cascades + +**Key instrumentation pattern:** +```typescript +private setMovementInputs(w, a, s, d): void { + const perfStart = performance.now(); + const perfCheckpoints: Record = {}; + + const stringStart = performance.now(); + const currentInputState = `${w}${a}${s}${d}`; + perfCheckpoints.stringConcat = performance.now() - stringStart; + + // ... rest of function with checkpoints ... + + const totalMs = performance.now() - perfStart; + if (totalMs > 0.5) { + eventLogger.info('perf.setMovementInputs', 'Performance breakdown', { + totalMs: totalMs.toFixed(3), + checkpoints: { /* ... */ }, + context: { botName: this._entity.getBotName() } + }); + } +} +``` + +--- + +### feature/performance-analysis-reports + +**Status:** 187 commits ahead of master (NOT merged) +**Relationship:** Subset of `performance-analysis-combined` (all commits present in combined) + +Contains the per-function analysis reports and codebase mapping tools but without the actual code fixes. The `combined` branch is the superset. + +--- + +## BATCH 2 - Optimization Branches + +--- + +### feature/performance-optimizations + +**Status:** Fully merged into master +**Commits:** 0 ahead + +Earlier optimization work that was fully merged. No unique content remaining. + +--- + +### feature/baseline-lag-optimization + +**Status:** 4 commits ahead of master (NOT merged) +**Files changed:** 8 files, +64,772 / -5 lines (mostly raw JSON trace data) + +**Focus:** Mobile client frame budget analysis based on real Chrome trace data. + +**Key findings (from `REAL_BASELINE_LAG_FIX.md`):** +- 59.2% of animation frames fail 60fps budget (>16ms) +- 34.2% fail 30fps budget (>33ms) +- Average frame time 38ms (238% over budget) +- Root cause: `weapon-viewmodel-dual-renderer.js` runs unconditionally at 60fps + +**Fixes applied:** +1. **Frame skipping on mobile** (`weapon-viewmodel-dual-renderer.js`): +```javascript +let frameCounter = 0; +function renderFrame() { + frameCounter++; + if (isMobile && frameCounter % 2 === 0) { + animationFrameId = requestAnimationFrame(renderFrame); + return; // Skip every other frame = 30fps on mobile + } + // ... rest of rendering +} +``` + +2. **Disable weapon bob on mobile** - removes per-frame sine/cosine calculations +3. **Reduce setInterval aggression** in mobile controls joystick + +**Analysis data:** Contains `recurring_blockers_raw_data.json` (61K lines) - full trace data for offline analysis. + +--- + +### feature/comprehensive-mobile-performance + +**Status:** Fully merged into master +**Commits:** 5 unique (all merged) + +**What was merged:** +- Per-frame analysis of what kills frame budget on mobile A14 chip +- Reduced blood particle count on mobile (instead of disabling entirely) +- Complete breakdown of mobile A14 performance characteristics +- Audit of all periodic operations and their frame budget impact +- Previously merged optimizations: antialiasing disabled, pixel ratio clamped to 1.5, shader precision lowered, simplified lighting, shell casings disabled, smoke disabled on mobile + +--- + +### feature/mobile-performance-analysis + +**Status:** 4 commits ahead of master (NOT merged) +**Files changed:** 12 files, +66,578 lines + +**New Python analysis scripts:** +- `analyze-trace.py` (429 lines) - Chrome Performance Trace Analyzer + - Analyzes long tasks (>50ms) + - JavaScript execution time breakdown + - Rendering performance (Layout, Paint, Composite) + - Frame time distribution +- `analyze-recurring-blockers.py` (372 lines) - Recurring Frame Blocker Analysis + - Filters to gameplay only (skips first 5s startup) + - Finds operations that repeatedly block frames + - Tracks call frequency and duration distribution +- `analyze-frame-budget.py` (386 lines) - Frame budget analysis + +**Key code - TraceAnalyzer:** +```python +class TraceAnalyzer: + def analyze_long_tasks(self, threshold_ms=50): + for event in self.events: + if event.get('ph') == 'X' and 'dur' in event: + duration_ms = event['dur'] / 1000 + if duration_ms > threshold_ms: + self.long_tasks.append({ + 'name': event.get('name'), + 'duration_ms': duration_ms, + 'timestamp': event.get('ts', 0) / 1000 + }) +``` + +**How to use:** +```bash +# 1. Capture Chrome trace on mobile device +# 2. Download trace JSON +python3 analyze-trace.py trace.json +python3 analyze-recurring-blockers.py trace.json +python3 analyze-frame-budget.py trace.json +``` + +--- + +### feature/investigate-mobile-viewmodel-performance + +**Status:** Fully merged into master +**Commits:** 3 unique (all merged) + +**What was merged:** +- **Ambient-only lighting on mobile** for weapon view model: + - Mobile: single ambient light (intensity 1.2) instead of ambient (0.65) + directional (0.8) + - Desktop unchanged: ambient (0.5) + directional (1.0) + fill (0.3) + - Estimated 3-5% FPS improvement on mobile +- Comprehensive mobile view model optimizations +- Analysis documentation + +--- + +### feature/particle-stress-tester + +**Status:** 16 commits ahead of master (NOT merged) +**Files changed:** 5 files, +831 / -1 lines + +**New files:** +- `src/test/ParticleStressTester.ts` (425 lines) +- `assets/ui/components/particle-test-menu.js` (255 lines) + +**Test scenarios:** +| Test | What it does | +|------|-------------| +| `weapons` | All players fire weapons simultaneously | +| `smoke` | Spawn multiple smoke grenades | +| `he` | Spawn multiple HE grenades | +| `molotov` | Spawn multiple molotov grenades | +| `flash` | Spawn multiple flashbangs | +| `blood` | Damage all players for blood effects | +| `stress` | ALL effects combined at maximum intensity | +| `ramp` | Gradually increase particle intensity | + +**Key code:** +```typescript +export class ParticleStressTester { + constructor(gameManager: GameManager) { + this.initializeScenarios(); + // Auto-start test sequence after 5 seconds + setTimeout(() => this.runAutoTestSequence(), 5000); + } + + public async runTest(scenario: string): Promise { + this.isRunning = true; + const test = this.scenarios.get(scenario); + await test.execute(); + this.isRunning = false; + } +} +``` + +**UI:** F2 hotkey toggles a floating menu with orange-on-black theme. Also adds a permanent "PARTICLE TESTS (F2)" button. + +**How to use:** The stress tester auto-runs a sequence 5 seconds after construction. Or use the F2 menu in the client to trigger individual tests. + +--- + +### feature/bot-cover-micro-profiler + +**Status:** 5 commits ahead of master (NOT merged) +**Files changed:** 10 files, +1,269 / -25 lines + +**New files:** +- `src/profiling/BotCoverServiceProfiler.ts` (181 lines) +- `docs/micro-profiler-notes.md` (35 lines) + +**Technique: Opt-in prototype patching via environment variable:** +```typescript +const ENABLED = process.env.BOT_COVER_PROF === "1"; + +if (ENABLED) { + const INSTALLED_SYMBOL = Symbol.for("BOT_COVER_PROF_INSTALLED"); + + if (!(botCoverProto as any)[INSTALLED_SYMBOL]) { + (botCoverProto as any)[INSTALLED_SYMBOL] = true; + + function wrapMethod(prototype, methodName, label) { + const original = prototype[methodName]; + prototype[methodName] = function(...args) { + const start = performance.now(); + try { + const result = original.apply(this, args); + if (result?.then) { + return result.then(v => { recordSample(label, performance.now() - start); return v; }); + } + recordSample(label, performance.now() - start); + return result; + } catch (error) { + recordSample(label, performance.now() - start); + throw error; + } + }; + } + + // Wrap specific methods + wrapMethod(botCoverProto, "assignBotsToGroups", "BotCover.assignBotsToGroups"); + wrapMethod(botCoverProto, "navigateBotToCoverPoint", "BotCover.navigateBotToCoverPoint"); + wrapMethod(botCoverProto, "updateBotPatrols", "BotCover.updateBotPatrols"); + wrapMethod(eventLoggerProto, "debug", "EventLogger.debug"); + wrapMethod(eventLoggerProto, "logToFile", "EventLogger.logToFile"); + + // Auto-emit on exit + process.once("exit", emitReport); + } +} +``` + +**Output:** JSON file at `profiles/bot-cover-prof-{timestamp}.json` with per-method stats (count, totalMs, avgMs, maxMs, minMs). + +**How to use:** +```bash +BOT_COVER_PROF=1 BOT_COVER_PROF_OUTPUT=profiles/run1.json bun run dev +``` + +**Key design principles (from micro-profiler-notes.md):** +1. Patch prototypes safely (use Symbol to prevent double-wrap) +2. Keep probe < 0.01ms overhead per call +3. Run A/B comparisons with identical workloads +4. Remove or disable behind strict opt-in flag when done + +--- + +## Cross-Cutting Architecture Summary + +### Evolution of Monitoring Approaches + +``` +Generation 1 (merged): + performance-monitoring -> Basic profiler with Bun.nanoseconds() + performance-monitoring-system -> Duplicate/early attempt + perf-monitoring-terrorist -> Targeted fix for CT rotation + performance-optimizations -> Early optimization pass + +Generation 2 (unmerged, UI-focused): + performance-monitoring-ui -> Full client overlay + server services + performance-monitoring-improvements -> Enhanced with flamecharts + hierarchy + performance-monitoring-ui-merge-master -> Rebased version + +Generation 3 (unmerged, production-focused): + performance-monitoring-hybrid -> Polling-based lag detection + Sentry + config + +Generation 4 (unmerged, deep analysis): + add-game-performance-monitoring -> Flame charts + spike detector + 12 scripts + performance-analysis-tools -> Python Excel analysis pipeline + performance-analysis-reports -> 200+ function analyses + performance-analysis-combined -> Everything + actual code fixes + validation + +Generation 5 (unmerged, targeted profilers): + bot-cover-micro-profiler -> Env-var-gated prototype patching + particle-stress-tester -> Grenade/particle load testing + +Mobile-specific (mix of merged/unmerged): + comprehensive-mobile-performance -> Merged: blood particles, A14 analysis + investigate-mobile-viewmodel -> Merged: ambient-only lighting + mobile-performance-analysis -> Unmerged: Python trace analyzers + baseline-lag-optimization -> Unmerged: frame skipping fix +``` + +### Common Metrics Tracked + +| Metric | Where | Threshold | +|--------|-------|-----------| +| Frame/tick duration | FrameBudgetMonitor | 16.66ms (60 FPS) | +| Spike detection | SpikeDetector | 0.5ms (3% of budget) | +| CPU usage | PerformanceLagDetector | 40% | +| Memory (heap) | PerformanceMetricsService | 1GB | +| Event loop lag | PerformanceMetricsService | 1ms | +| Individual operation | FunctionProfiler | 10ms | +| Sentry transactions | SentryTelemetryService | 50 per session | + +--- + +## Key Techniques Catalog + +### 1. Function Wrapping (FunctionProfiler pattern) +```typescript +const wrapped = function(...args) { + const start = performance.now(); + try { return original.apply(this, args); } + finally { record(performance.now() - start); } +}; +``` +**Used in:** performance-monitoring-ui, bot-cover-micro-profiler + +### 2. Deferred Evaluation (batch processing) +```typescript +// Flag instead of immediate execution +this.pendingEvaluation = true; +// Single evaluation at end of tick +if (hasPending()) evaluateDeferred(); +``` +**Used in:** perf-monitoring-terrorist-approaches (150-700x speedup) + +### 3. Chrome Trace Event Format +```typescript +{ name, cat, ph: 'X', ts: microseconds, pid: 1, tid: threadId, dur: microseconds } +``` +**Used in:** add-game-performance-monitoring (FlameChartRecorder) +**Viewable in:** chrome://tracing, Speedscope, custom D3.js viewer + +### 4. Prototype Patching with Symbol Guard +```typescript +const SYM = Symbol.for("PROFILER_INSTALLED"); +if (!proto[SYM]) { proto[SYM] = true; /* wrap methods */ } +``` +**Used in:** bot-cover-micro-profiler + +### 5. CPU Polling (production-safe) +```typescript +setInterval(() => { + const stats = Telemetry.getProcessStats(false); + if (stats.cpuUsage > threshold) captureSnapshot(); +}, 50); +``` +**Used in:** performance-monitoring-hybrid + +### 6. Checkpoint-based Instrumentation +```typescript +const checkpoints = {}; +checkpoints.step1 = performance.now() - stepStart; +// ... more steps ... +if (total > threshold) log({ checkpoints }); +``` +**Used in:** performance-analysis-combined (INSTRUMENTATION_GUIDE.md) + +### 7. Python CSV-to-Excel Analysis Pipeline +``` +Chrome CPU profile -> CSV -> openpyxl -> XLSX with pivot tables +``` +**Used in:** performance-analysis-tools + +### 8. Mobile Frame Skipping +```javascript +if (isMobile && frameCounter % 2 === 0) { return; } // 30fps on mobile +``` +**Used in:** baseline-lag-optimization + +### 9. Self-Time Calculation for Flamecharts +```typescript +selfTime = totalDuration - sum(children.map(c => c.duration)); +``` +**Used in:** performance-monitoring-improvements (FrameBudgetMonitor) + +### 10. A/B Validation Protocol +``` +Run 1: with fixes (5 min, AUTO_START_WITH_BOTS) +Run 2: without fixes (5 min, same config) +Compare: log volume, error count, frame timing +``` +**Used in:** performance-analysis-combined (PERFORMANCE_VALIDATION_REPORT.md) diff --git a/ai-memory/docs/perf-framework-research-2026-03-05/hyfire2-master-perf-code.md b/ai-memory/docs/perf-framework-research-2026-03-05/hyfire2-master-perf-code.md new file mode 100644 index 00000000..cac342a0 --- /dev/null +++ b/ai-memory/docs/perf-framework-research-2026-03-05/hyfire2-master-perf-code.md @@ -0,0 +1,666 @@ +# HyFire2 Master Branch - Performance Code Inventory + +Comprehensive inventory of ALL performance monitoring, profiling, benchmarking, and optimization code found on the `master` branch of `~/GitHub/games/hyfire2` as of 2026-03-05. + +--- + +## 1. Core Profiling System (`src/profiling/`) + +### 1.1 `src/profiling/PerformanceManager.ts` +**What it does:** Comprehensive performance monitoring singleton with spike detection, auto-profiling, memory monitoring, and report generation. + +**Key techniques:** +- `process.hrtime.bigint()` for high-precision timing +- Spike detection: any operation exceeding configurable threshold (default 50ms) triggers alerts +- Auto CPU profiling on spike via `InspectorCpuProfiler` (5s capture, 30s cooldown) +- Heap snapshots via `v8.writeHeapSnapshot()` when memory exceeds 800MB +- Event loop lag detection (interval-based) +- Signal handlers: `SIGUSR1` = toggle CPU profiling, `SIGUSR2` = generate report +- Operation stats: count, total, avg, max, min, spike count, recent history (last 100) + +**Current state:** Event loop monitoring and periodic reporting are **DISABLED** (commented out) because they were causing performance overhead and memory issues themselves. The core operation timing and spike detection remain active. + +**Key metrics:** operation duration (ms), spike count, heap used/total/RSS, event loop lag, hot paths (top by total time), spiky operations (top by spike count) + +**How to run:** Imported as singleton in `index.ts`. Configured at startup with `performanceManager.configure({...})`. + +**Dependencies:** `node:inspector`, `node:v8`, `node:fs`, `InspectorCpuProfiler`, `EventLogger` + +### 1.2 `src/profiling/InspectorCpuProfiler.ts` +**What it does:** Wraps the Node.js Inspector API (`Profiler` domain) to capture V8 CPU profiles. + +**Key techniques:** +- `inspector.Session` + `Profiler.enable` / `Profiler.start` / `Profiler.stop` +- Profile output as `.cpuprofile` JSON (Chrome DevTools compatible) +- `profileForMilliseconds(duration, outputPath)` for timed captures +- Signal-based profiling: `installSignalHandlers()` listens on configurable signals + +**How to run:** Used by PerformanceManager for auto-profiling. Can also be used standalone via signals or direct API. + +**Output:** `.cpuprofile` files in `./profiles/` directory + +### 1.3 `src/profiling/decorators.ts` +**What it does:** TypeScript decorators for zero-effort performance instrumentation. + +**Key techniques:** +- `@Monitor(captureStack?)` - method decorator, wraps sync/async methods in `performanceManager.measure()` +- `@MonitorClass(captureStack?)` - class decorator, monitors ALL methods +- `monitorBlock(name, fn, captureStack?)` - inline sync block measurement +- `monitorAsyncBlock(name, fn, captureStack?)` - inline async block measurement +- Auto-detects async vs sync methods via `constructor.name === 'AsyncFunction'` + +**How to run:** Import decorator, apply to class or method. Operation name auto-generated as `ClassName.methodName`. + +--- + +## 2. Utility Performance Monitors (`src/utils/`) + +### 2.1 `src/utils/PerformanceMonitor.ts` +**What it does:** Second performance monitoring singleton focused on sampling-based metrics collection with percentile calculations. + +**Key techniques:** +- `process.hrtime.bigint()` for timing +- Percentile calculations: p50, p95, p99 +- Memory tracking: heap used/total, RSS, external +- CPU approximation (Bun lacks `process.cpuUsage`) +- Event loop lag via interval jitter +- Auto-sampling at configurable interval (default 1s) +- Metrics history with configurable retention (default 600 samples = 10 min) + +**Current state:** Sampling is **DISABLED** in constructor (was causing memory overhead with no benefit). Timing storage also **DISABLED** (unbounded memory growth causing OOM). The `startTiming`/`endTiming` API still works but doesn't store history. + +**Key metrics:** memory (heapUsed, heapTotal, RSS, external in MB), CPU (user, system), event loop (lag, avgLag), per-operation timing stats (count, total, avg, min, max, p50, p95, p99) + +### 2.2 `src/utils/PerformanceProfiler.ts` +**What it does:** Manual call stack profiler for flame graph generation. + +**Key techniques:** +- Manual call stack tracking with `enter(name)` / `exit(name)` +- Tree-based profile data structure (ProfileNode with self time, total time, children) +- Statistical sampling: takes stack samples at configurable interval (default 1ms) +- `wrap(name, fn)` - wraps a function for automatic profiling +- Export to collapsed stack format (Brendan Gregg format) for flame graph generation +- Human-readable report with top functions by total time + +**How to run:** `profiler.enable()` then use `profiler.profile(name, fn)` or `profiler.enter()`/`profiler.exit()`. Generate output with `profiler.generateCollapsedStacks()` or `profiler.saveProfile(path)`. + +### 2.3 `src/utils/FrameBudgetMonitor.ts` +**What it does:** Tracks frame budget violations against 60 FPS target (16.66ms). + +**Key techniques:** +- Frame-level tracking: `startFrame()` / `endFrame()` with per-operation breakdown +- Spike threshold: 8ms (operations > 8ms tracked as concerning) +- Tracks worst frame ever, top offenders, recent spikes (last 50) +- Per-operation stats: name, duration, percent of budget +- `measure(name, fn)` for inline operation tracking + +**Current state:** Frame budget console logging is **DISABLED** (was causing performance overhead). Stats collection still active. + +**Key metrics:** total frames, frames over budget, percent over budget, worst frame (total duration + operation breakdown), top offenders (name, count, max, avg), recent spikes (operation, duration, % of budget) + +**Active usage:** Heavily used in `BotBrain.ts` - every bot decision path is wrapped in `frameBudgetMonitor.measure()` calls with per-bot naming (e.g., `brain.bombDetection.Alpha`, `brain.terroristStrategy.Zulu`). + +### 2.4 `src/utils/ClientPerformanceReporter.ts` +**What it does:** Server-side aggregator for client-side FPS reports. + +**Key techniques:** +- Receives `ClientPerformanceReport` from game clients (fps, frameTime, memory, performance tier) +- Per-player metric tracking with history (default 300 samples = 5 min at 1 report/sec) +- Issue detection: low FPS (<30), high frame time (>50ms), critical FPS (<20), high memory (>1GB) +- Running averages per player +- Periodic summary logging every 60 seconds +- Persistent issue detection: flags players with >30% problematic reports +- Performance tier classification: excellent/good/fair/poor/critical + +**Key metrics:** per-player FPS avg, frame time avg, memory avg, issue counts (lowFps, highFrameTime, criticalEvents), global summary (total players, players with issues, critical players, average FPS, lowest FPS player) + +### 2.5 `src/utils/ZoneVisibilityMonitor.ts` +**What it does:** Monitors zone visibility optimization performance (raycast skip rate). + +**Key techniques:** +- Periodic reporting at configurable interval (default 30s) +- Tracks checks skipped vs performed via `ZoneVisibilityService` +- Calculates estimated time saved (4ms per skipped raycast) +- Uses EventLogger for structured output + +**Key metrics:** checksSkipped, checksPerformed, skipRate, estimatedTimeSavedMs + +### 2.6 `src/utils/NameplateOptimizationConfig.ts` +**What it does:** Configuration constants for nameplate rendering optimization. + +**Key values:** +- MAX_VISIBLE_NAMEPLATES: 32 +- MAX_NAMEPLATE_DISTANCE: 75 +- CLOSE_RANGE_DISTANCE: 30 +- Update intervals: close 100ms, medium 250ms, far 500ms +- Feature flags: pooling, distance culling, batched updates, LOD + +--- + +## 3. Debug Systems + +### 3.1 `src/config/DebugUIConfig.ts` + `src/config/debugUIConfigData.ts` +**What it does:** Centralized debug UI configuration with cached flag lookups. + +**Perf-relevant flags:** +- `scene_debug_ui.show_performance_metrics` (default: false) +- `development.performance_profiling` (default: false) +- `development.verbose_logging` (default: false) +- `player_debug_ui.show_fps` / `show_ping` (default: false) + +**Perf optimization:** Flag values cached in static fields to avoid function call overhead (100+ calls/sec with 10 bots). + +### 3.2 `src/commands/DebugCommands.ts` +**What it does:** In-game debug command handler (admin/dev only). + +**Perf-relevant:** Bot debug UI toggle (shows names, roles, actions). Uses cached config flags for gate checks. + +### 3.3 `src/debug/AudioDebugConfig.ts` +**What it does:** Audio parameter tuning config singleton. + +**Perf-relevant:** Controls distance-based audio culling parameters (reference/cutoff distances). + +### 3.4 `src/entities/bot/debug/BotDebugUI.ts` + `BotDecisionTracer.ts` +**What it does:** Bot state visualization and decision tracing. + +### 3.5 `src/managers/GrenadeDebugManager.ts` +**What it does:** Debug visualization for grenade trajectories. + +### 3.6 `src/navigation/ZoneDebugManager.ts` + `ZoneDebugVisualizer.ts` +**What it does:** Navigation zone debug visualization. + +--- + +## 4. Performance Scripts (`scripts/`) + +### 4.1 `scripts/benchmark-game.ts` +**What it does:** Automated game performance benchmarking. + +**How to run:** +```bash +bun scripts/benchmark-game.ts [-d duration] [-s scenario] [-o output.json] +# npm run benchmark / benchmark:quick / benchmark:full +``` + +**Scenarios:** idle, 5v5_combat, 10v10_full, stress_test + +**Techniques:** Starts game server, measures tick times, memory, event loop lag per scenario. Reports avg/max tick time, memory, operation timings (bot.decision, pathfinding, combat.damage, network.send). + +**Dependencies:** PerformanceMonitor, PerformanceProfiler, game server + +### 4.2 `scripts/profile-server-auto.ts` +**What it does:** Auto-saving real-time performance dashboard for WSL/non-interactive terminals. + +**How to run:** +```bash +npm run profile:auto +# or: bun scripts/profile-server-auto.ts +``` + +**Techniques:** +- ANSI-colored terminal dashboard (refreshes every 1s) +- Auto-saves reports every 30s to `performance-reports/` +- Frame budget status with color coding (green/yellow/red) +- Worst frame breakdown with top operations +- Performance bar visualization +- Final comprehensive report on SIGINT (Ctrl+C) +- Saves JSON stats, text frame budget report, and performance summary + +**Dependencies:** PerformanceMonitor, PerformanceProfiler, FrameBudgetMonitor + +### 4.3 `scripts/analyze-performance.cjs` +**What it does:** Analyzes `.cpuprofile` files and generates human-readable reports. + +**How to run:** +```bash +node scripts/analyze-performance.cjs +``` + +**Techniques:** Builds call tree from V8 profile nodes, calculates self/total times, identifies hot functions (top 20 by self time, top 10 by total time), analyzes V8 engine overhead, flags slow functions (>50ms), identifies hot game paths (BotBrain, GameManager, RoundSystem, Navigation). + +### 4.4 `scripts/analyze-full-profile.cjs` +**What it does:** Complete CPU profile analysis showing top 100 functions. + +**How to run:** +```bash +node scripts/analyze-full-profile.cjs +``` + +**Techniques:** Categorizes functions as game code vs system/SDK, shows coverage percentage. + +### 4.5 `scripts/map-profile-to-code.cjs` +**What it does:** Maps V8 CPU profile functions back to game source code. + +**How to run:** +```bash +node scripts/map-profile-to-code.cjs +``` + +**Techniques:** Reads `CODEBASE_REF.md` to identify game functions, categorizes profile entries as YOUR CODE vs HYTOPIA SERVER vs UNKNOWN. Shows per-tick time contribution. Clusters unknown functions by line number ranges to identify game systems. + +### 4.6 `scripts/extract-game-performance.cjs` +**What it does:** Extracts game-specific functions from CPU profiles using CODEBASE_REF.md mapping. + +**How to run:** +```bash +node scripts/extract-game-performance.cjs +``` + +### 4.7 `scripts/generate-flamegraph.ts` +**What it does:** Generates interactive HTML flame graphs from collapsed stack format. + +**How to run:** +```bash +bun scripts/generate-flamegraph.ts [output.html] +# npm run flamegraph +``` + +**Techniques:** Parses collapsed stack format, builds tree structure, generates self-contained HTML with SVG visualization, click-to-zoom, search, tooltips. Color-coded by function name hash. + +### 4.8 `scripts/generate-flame-chart.cjs` +**What it does:** Generates text-based and HTML flame charts from performance-reports JSON stats. + +**How to run:** +```bash +node scripts/generate-flame-chart.cjs +``` + +**Techniques:** Reads latest `performance-reports/stats-*.json`, creates ASCII flame chart for terminal and HTML visualization with color-coded bars (hot/warm/cool by duration). + +### 4.9 `scripts/visualize-perf.js` +**What it does:** ASCII performance visualization and timing breakdown from stats JSON. + +**How to run:** +```bash +node scripts/visualize-perf.js +``` + +**Techniques:** Reads latest stats file, sorts by P99 latency, ASCII bar charts for avg/p99, cumulative time analysis, frame budget violation summary. + +### 4.10 `scripts/capture-baseline.sh` +**What it does:** Captures performance baseline by running the server with bots for a configurable duration. + +**How to run:** +```bash +./scripts/capture-baseline.sh [duration_seconds] [output_file] +# npm run perf:baseline +# Default: 300s (5 minutes), output to baseline.json +``` + +**Techniques:** Starts server with `AUTO_START_WITH_BOTS=true`, extracts `perf.*` events from game.log, calculates per-operation stats (avg, min, max, p50, p95, p99), saves as JSON. + +### 4.11 `scripts/compare-baselines.ts` +**What it does:** Compares two performance baselines to detect regressions or improvements. + +**How to run:** +```bash +node scripts/compare-baselines.ts [threshold%] +# npm run perf:compare before.json after.json 10 +``` + +**Techniques:** Compares avg/max/p95 for each operation, calculates percentage change, classifies as improved (>5% better), regressed (>5% worse), or stable. Exit code 1 on regression. Optional improvement threshold gate. + +### 4.12 `scripts/analyze-bottleneck.js` +**What it does:** Analyzes a specific worst-frame JSON snapshot to identify bottlenecks. + +**How to run:** +```bash +node scripts/analyze-bottleneck.js +``` + +**Techniques:** Groups operations by name, sorts by total time, creates ASCII bar chart, breaks down combat system specifically, identifies anomalies (e.g., specific bot's bomb detection taking 22ms). + +### 4.13 `scripts/add-granular-perf-tracking.sh` +**What it does:** Identifies gaps in performance instrumentation. + +**How to run:** +```bash +bash scripts/add-granular-perf-tracking.sh +``` + +**Techniques:** Greps source for `frameBudgetMonitor.measure()` calls, identifies methods with >5ms spikes that lack internal instrumentation, recommends specific measurement points. + +### 4.14 `scripts/analyze-entity-lookups.sh` +**What it does:** Audits inefficient entity lookup patterns. + +**How to run:** +```bash +bash scripts/analyze-entity-lookups.sh +``` + +**Techniques:** Counts `getAllEntities()`, `getAllPlayerEntities()`, `getEntitiesByTag()` calls across codebase, identifies bomb/player lookups that could use tags, suggests optimization phases. + +### 4.15 `scripts/analyze-latest-session.ts` +**What it does:** Analyzes performance metrics from the latest game session log. + +**How to run:** +```bash +bun scripts/analyze-latest-session.ts +``` + +**Techniques:** Extracts all `perf.*` events from `logs/latest/game.log`, calculates stats per operation (count, mean, median, min, max, p25, p75, p90, p95, p99). + +### 4.16 `scripts/comprehensive-analysis.cjs` +**What it does:** Analyzes replay files for tactical anomalies (bomb abandonment, CT defuse failures, terrorist no-plant rounds). + +**Perf-relevant:** Processes compressed replay data (gzip JSON), not primarily performance but relates to game quality analysis. + +--- + +## 5. Performance Testing Framework + +### 5.1 `scripts/test-grenade-spike.ts` +**What it does:** Automated performance regression test for grenade drop death spike. + +**How to run:** +```bash +PORT=8081 npm run perf:test:grenade +``` + +**Techniques:** +- Starts server with test hooks +- Spawns headless browser player via Puppeteer +- Buys 4 grenades, triggers death +- Extracts `perf.handleDeath` metrics from logs +- Validates against thresholds: total <50ms, weapon drop <25ms + +**Dependencies:** Puppeteer, HeadlessPlayer, ServerController, MetricsExtractor + +### 5.2 `scripts/test-grenade-performance.sh` +**What it does:** Long-running grenade performance test (3 minutes). + +**How to run:** +```bash +bash scripts/test-grenade-performance.sh +``` + +**Techniques:** Starts server with bots for 180s, analyzes death events from logs with Python, calculates avg/min/max/p95, validates thresholds. + +### 5.3 `scripts/lib/headless-player.ts` +**What it does:** Puppeteer-based headless browser player for automated testing. + +**Key API:** `connect(address)`, `selectTeam()`, `buyGrenades()`, `executeCommand()`, `screenshot()`, `getPosition()`, `isAlive()` + +**Dependencies:** puppeteer (devDependency) + +### 5.4 `scripts/lib/server-controller.ts` +**What it does:** Server lifecycle management for testing. + +**Key API:** `startServer(options)`, `stopServer(server)`, workspace port mapping (work1=8081, work2=8082, work3=8083) + +### 5.5 `scripts/lib/metrics-extractor.ts` +**What it does:** Extracts and calculates statistics from structured log files. + +**Key API:** `extractMetrics(logPath, options)`, `calculateStats(metrics)`, `formatStats(stats)` + +**Techniques:** Parses NDJSON log lines, filters by `perf.*` events, calculates count/avg/min/max/p50/p95/p99. + +### 5.6 `scripts/lib/scenario-types.ts` +**What it does:** TypeScript type definitions for YAML-based test scenarios. + +**Key types:** Scenario, ScenarioAction, ThresholdConfig, ScenarioResult, OperationResult + +### 5.7 `scenarios/grenade-death.yaml` +**What it does:** Declarative performance test scenario for grenade death spike. + +**Thresholds:** +- `perf.handleDeath`: avg <20ms, max <50ms, p95 <35ms +- `perf.weaponDrop`: avg <12ms, max <25ms, p95 <20ms +- Min 5 samples required, failure action: report_and_exit + +--- + +## 6. Python Analysis Tools (Root) + +### 6.1 `analyze-frame-budget.py` +**What it does:** Analyzes Chrome trace files for frame budget violations. + +**How to run:** +```bash +python3 analyze-frame-budget.py [trace-file.json] +``` + +**Techniques:** +- Parses Chrome trace events (ph='X' complete events with duration) +- Per-call cost analysis (not cumulative) with >16ms threshold +- Frame budget violation counting (60fps and 30fps targets) +- Weapon animation deep dive +- JavaScript game loop analysis (FunctionCall, EvaluateScript events) +- Generates `FRAME_BUDGET_ANALYSIS.md` report + +### 6.2 `analyze-recurring-blockers.py` +**What it does:** Finds RECURRING frame blockers during gameplay (excludes startup, one-offs). + +**How to run:** +```bash +python3 analyze-recurring-blockers.py [trace-file.json] +``` + +**Techniques:** +- Filters to gameplay only (skips first 5 seconds) +- Requires 3+ occurrences to qualify as "recurring" +- Impact score: frequency x average duration +- Animation frame pattern analysis (degradation over time detection) +- Periodic spike detection (checks for regular cadence) +- Full data dump with per-operation histograms +- Generates `RECURRING_BLOCKERS_FULL_DATA.md` and `recurring_blockers_raw_data.json` + +--- + +## 7. Package.json Performance Scripts + +``` +start:profile - node --cpu-prof --cpu-prof-dir=./profiles index.js +start:profile:inspect - node --inspect index.js +profile:server - node scripts/profile-server.ts +profile:enhanced - node scripts/profile-server-improved.ts +profile:auto - node scripts/profile-server-auto.ts +benchmark - bun scripts/benchmark-game.ts +benchmark:quick - bun scripts/benchmark-game.ts -d 10 +benchmark:full - bun scripts/benchmark-game.ts -d 60 -o benchmark-results.json +flamegraph - node scripts/generate-flamegraph.ts +perf:analyze - ls + tail logs/latest/performance.log +perf:test:grenade - npx ts-node scripts/test-grenade-spike.ts +perf:baseline - ./scripts/capture-baseline.sh +perf:compare - npx ts-node scripts/compare-baselines.ts +``` + +--- + +## 8. In-Game Performance Instrumentation + +### 8.1 `src/entities/bot/BotBrain.ts` - Frame Budget Instrumentation +**~30 `frameBudgetMonitor.measure()` calls** wrapping every bot decision path: +- `brain.getRoundState.{botName}`, `brain.buyPhase.{botName}` +- `brain.bombDetection.{botName}`, `brain.terroristStrategy.{botName}` +- `brain.ctRetake.{botName}`, `brain.ctRotation.{botName}` +- `brain.combatCheck.{botName}`, `brain.defaultBehavior.{botName}` +- `brain.bombRetrievalCheck.{botName}`, `brain.getNavigator.{botName}` +- `brain.bombCarrierExecute.{botName}`, `brain.supportExecute.{botName}` +- `bombDetect.getEntities.{botName}`, `bombDetect.loop.{botName}` +- `bombDetect.checkPlanted.{botName}`, `bombDetect.getPos.{botName}` + +### 8.2 `src/entities/GamePlayerEntity.ts` - Inline `performance.now()` Timing +**~50 `performance.now()` calls** with checkpoint tracking for: +- `handleDeath()` - comprehensive checkpoints: logging, healthCalc, nametag, mvpTracking, sourceTracking, audioAndEffects, healthUI, damageEffectUI, total +- Death item drops: botCacheInvalidate, audioCleanup, deathEffects, bombCancellation, broadcast, uiNotify, bombDrop, kitDrop, weaponDrop, deathEvent, inputCleanup, visibilityCheck +- Nameplate updates: timing for search, update operations +- Performance data logged via EventLogger as `perf.handleDeath` with checkpoint data + +### 8.3 `src/entities/bot/combat/BotCombatSystem.ts` - Raycast Timing +**Inline `performance.now()`** around physics raycasts with duration logging when >5ms. + +### 8.4 `src/managers/AudioManager.ts` - Audio Timing +**Inline `performance.now()`** for audio operation timing. + +--- + +## 9. Tools (`tools/`) + +### 9.1 `tools/replay-viewer/` +**What it does:** Browser-based 2D/3D replay viewer with analysis tools. + +**Perf-relevant components:** +- `js/AnalysisTools.js` - Analysis tools for replay data +- `js/BotAnomalyDetector.js` - Detects anomalous bot behavior +- `js/BotInspector.js` - Bot state inspection + +### 9.2 `src/tools/VisibilityDataCollector.ts` +**What it does:** Collects zone visibility data during gameplay to build visibility matrix. + +**Perf-relevant:** Runs raycasts during gameplay (rate-limited to `testsPerTick=50`), used to pre-compute zone visibility for optimization. + +--- + +## 10. Analysis Documents on Master + +### Root-level performance analysis docs: +- `CONSOLE_PERFORMANCE_ANALYSIS.md` - Console.log performance impact on low-end devices +- `FINAL_OPTIMIZATIONS_SUMMARY.md` - Summary of all optimizations applied +- `FRAME_SKIPPING_ANALYSIS.md` - Frame skipping patterns analysis +- `PERIODIC_STUTTER_ANALYSIS.md` - Periodic stutter investigation +- `PER_FRAME_BUDGET_KILLERS.md` - Per-frame analysis of budget violations +- `ALL_LAG_SOURCES_ACTION_PLAN.md` - Comprehensive lag source action plan +- `BASELINE_LAG_NEXT_STEPS.md` - Post-baseline lag reduction plan +- `COMPLETE_INTERVAL_AUDIT.md` - Audit of all setInterval/setTimeout calls +- `FINAL_CLIENT_SIDE_ISSUES.md` - Client-side performance issues +- `MOBILE_A14_COMPLETE_BREAKDOWN.md` - iPhone A14 performance breakdown +- `MOBILE_A14_VS_PC_6X_COMPARISON.md` - Mobile vs PC performance comparison +- `MOBILE_VIEWMODEL_PERFORMANCE_ANALYSIS.md` - Viewmodel renderer analysis +- `RECURRING_BLOCKERS_FULL_DATA.md` - Full recurring blocker data +- `WHATS_FIXED_ON_MASTER.md` - Summary of performance fixes on master +- `CLEANUP_ANALYSIS.md` - Code cleanup analysis + +### Docs directory guides: +- `docs/01-guides/performance-optimization-guide.md` - Top 10 bottlenecks, monitoring setup, root cause analysis +- `docs/01-guides/performance-testing-guide.md` - Complete testing workflow: server hooks, headless testing, baseline comparison + +### AI Memory performance folders: +- `ai-memory/feature/performance-monitoring-system-*` (4 instances: 1ead992, 4c5ae85, 695757e, 70b1968) +- `ai-memory/feature/performance-optimizations-5ac318d/` + +--- + +## 11. Saved Profile Data + +### `profiles/human-perf-20251011.log` +**What it is:** Server log from a human performance testing session on 2025-10-11. + +**Content:** Full server startup + gameplay log including performance monitoring initialization message: +``` +system.performance : Performance monitoring ENABLED + mode: automatic + spikeThreshold: 30ms + autoProfile: on spikes + reportInterval: 60s + outputDir: ./profiles +``` + +### `mobile-a14-trace.json` +**What it is:** Chrome trace file from an iPhone A14 device (used by `analyze-frame-budget.py` and `analyze-recurring-blockers.py`). + +--- + +## 12. Build-Time Optimization + +### `build-production-mode.sh` +**What it does:** Production build pipeline that strips all dev/profiling artifacts. + +**Perf-relevant removals:** +- Removes `performance-reports/`, `profiles/`, `scripts/`, `tools/`, `docs/` +- Removes all `.md` files, test files, dev configs +- Smart asset cleanup: removes unused audio/models/textures +- Creates mode-specific builds (casual vs deathmatch) + +### `scripts/optimize-map.js` +**What it does:** Removes interior blocks from the voxel map that are completely surrounded on all 6 sides. + +**Perf impact:** Reduces memory and processing for blocks never visible to players. + +### `.optimized/models/` +**What it is:** Pre-optimized GLTF model cache (regenerated with each SDK version). + +--- + +## 13. Performance-Related Configuration + +### `.env.example` +Only contains HYTOPIA API keys. No perf-specific env vars listed, but the codebase uses: +- `AUTO_START_WITH_BOTS` - Start with AI bots (for baseline capture) +- `ENABLE_TEST_HOOKS` - Enable test commands (for perf testing) +- `PORT` - Server port (workspace-specific) +- `NODE_ENV` - production/development + +### `index.ts` (entry point) +PerformanceManager configured at startup: +```typescript +performanceManager.configure({ + spikeThresholdMs: 30, // 30ms spike threshold + autoProfileOnSpike: true, + profileDurationMs: 5000, + memorySnapshotThreshold: 800, + eventLoopLagThreshold: 100, + reportIntervalMs: 60000, +}); +``` + +Periodic report generation (5-minute interval) is **DISABLED** in recent commits. + +--- + +## 14. Perf-Related Git Commit History (Recent) + +Key commits on master showing performance work: +- `932afe8c0` - perf: disable 5-minute periodic profiling interval +- `43b7d6c7a` - perf: fully disable silent PerformanceMonitor overhead +- `b6fc8934c` - perf: disable heavy performance monitoring that caused OOM crashes +- `91972d37d` - perf: use Web Audio API for low-latency kill sounds +- `4d6e2a1f6` - perf: remove 2 critical console.log calls from flashbang handler +- `d53c88039` - perf: remove 61 commented console statements from index.html +- `f7ad73521` - perf: optimize FFA scoreboard updates to reduce lag +- `dc9666220` - perf: use only ambient lighting on mobile for weapon view model +- `a805ef353` - perf: implement comprehensive mobile view model optimizations +- `1d264e14d` - perf: optimize tutorial animation size and timing +- `9f77e2c6f` - perf: refactor deathmatch podium for mobile performance +- `53fb8fe47` - perf: optimize EventLogger to skip work for filtered logs + +--- + +## 15. Summary of Techniques Used + +| Technique | Files | +|-----------|-------| +| `process.hrtime.bigint()` | PerformanceManager, PerformanceMonitor, PerformanceProfiler | +| `performance.now()` | GamePlayerEntity, BotCombatSystem, AudioManager, FrameBudgetMonitor | +| V8 Inspector CPU profiling | InspectorCpuProfiler, PerformanceManager | +| V8 heap snapshots | PerformanceManager | +| Chrome trace analysis | analyze-frame-budget.py, analyze-recurring-blockers.py | +| Flame graph generation | generate-flamegraph.ts, generate-flame-chart.cjs | +| Signal-based profiling (SIGUSR1/2) | InspectorCpuProfiler, PerformanceManager | +| Decorator instrumentation | profiling/decorators.ts | +| Frame budget tracking | FrameBudgetMonitor, BotBrain | +| Percentile calculations | PerformanceMonitor, metrics-extractor | +| Baseline comparison | capture-baseline.sh, compare-baselines.ts | +| Headless browser testing | headless-player.ts (Puppeteer) | +| YAML test scenarios | scenarios/grenade-death.yaml | +| Event loop lag detection | PerformanceManager, PerformanceMonitor | +| Memory monitoring | PerformanceManager, PerformanceMonitor | +| Entity lookup auditing | analyze-entity-lookups.sh | +| Map optimization | optimize-map.js | +| Model optimization | .optimized/ cache | +| Client FPS aggregation | ClientPerformanceReporter | +| Zone visibility optimization | ZoneVisibilityMonitor, VisibilityDataCollector | +| Distance-based LOD | NameplateOptimizationConfig | + +--- + +## 16. Known Issues with Performance Tooling + +1. **PerformanceMonitor sampling disabled** - Was causing OOM due to unbounded memory growth in timings Map +2. **PerformanceManager event loop monitoring disabled** - 100ms interval checks were themselves causing lag +3. **PerformanceManager periodic reporting disabled** - File writes every 60s added overhead +4. **FrameBudgetMonitor console logging disabled** - Console.info calls in hot path caused perf overhead +5. **Three separate monitoring singletons** (PerformanceManager, PerformanceMonitor, PerformanceProfiler) with overlapping functionality +6. **CPU approximation in PerformanceMonitor** - Bun lacks `process.cpuUsage()`, uses rough estimate +7. **Headless player tests depend on Puppeteer** - Requires Chrome/Chromium installed +8. **Profile scripts reference hardcoded file paths** (e.g., `analyze-bottleneck.js` reads specific stats file) diff --git a/ai-memory/docs/perf-framework-research-2026-03-05/hyfire2-perf-analysis-branches.md b/ai-memory/docs/perf-framework-research-2026-03-05/hyfire2-perf-analysis-branches.md new file mode 100644 index 00000000..454b29c1 --- /dev/null +++ b/ai-memory/docs/perf-framework-research-2026-03-05/hyfire2-perf-analysis-branches.md @@ -0,0 +1,372 @@ +# HyFire2 Performance Analysis/Docs Branches - Research Notes + +**Date**: 2026-03-05 +**Source repo**: ~/GitHub/games/hyfire2 (NeuralPixelGames/HyFire2) +**Branches analyzed**: 11 analysis/docs performance branches +**Period covered**: July-October 2025 + +--- + +## Overview + +HyFire2 is a Counter-Strike-style multiplayer FPS game built on the HYTOPIA SDK (Bun/TypeScript server, Three.js client). Over July-October 2025, extensive performance work produced 63+ curated performance PRs and 11 analysis/docs branches documenting monitoring strategies, profiling infrastructure, hotspot analysis, and optimization results. + +The game runs at 60 Hz server tick rate with a 16.67ms frame budget. Performance work focused on server-side tick spikes caused by bot AI, combat systems, death handling, entity lookups, and navigation. + +--- + +## Branch-by-Branch Analysis + +--- + +### 1. `docs-performance` + +**Purpose**: Foundational performance tooling documentation and scripts. +**Head commit**: `cfce3ac4c` ("performance") + +**Key files**: +- `PERFORMANCE_GUIDE.md` -- Comprehensive guide covering all performance tools, metrics thresholds, optimization targets, and advanced profiling techniques. +- `scripts/benchmark-game.ts` -- Automated benchmarking with scenario support (idle, 5v5 combat, 10v10 full, stress test). Collects avg/max tick time, memory, event loop lag, and per-operation percentiles (P95, P99). +- `scripts/profile-server-auto.ts` -- Auto-saving performance monitor for non-interactive terminals (WSL). Saves reports every 30s to `performance-reports/` dir. Displays frame budget, worst frame, spike patterns in color-coded terminal output. +- `scripts/generate-flamegraph.ts` -- Parses collapsed stack format into tree structure and generates interactive HTML flame graphs with zoom/pan/search. +- `test-bot-spawn.ts` -- Simple test script for bot spawn diagnostics. +- `tools/zone-visibility-viewer.html` -- HTML viewer for zone visibility data. +- `tools/log-viewer/`, `tools/replay-viewer/` -- Log and replay visualization tools. + +**Techniques**: +- Real-time server monitoring with PerformanceMonitor, PerformanceProfiler, FrameBudgetMonitor +- Automated benchmarking with configurable scenarios and JSON report output +- HTML flame graph generation from collapsed stack format +- Client FPS overlay (F3 key) +- Linux perf integration (`perf record` + FlameGraph scripts) +- V8 heap snapshots for memory profiling +- Async profiling with `perf_hooks` PerformanceObserver + +**Notable findings**: +- Baseline performance: 1.11ms avg game tick, 0.09ms bot decision, 0.10ms pathfinding +- Theoretical targets: game tick <0.5ms, bot AI <0.01ms, pathfinding <0.05ms +- Proposed advanced techniques: object pooling, spatial hash grids, SIMD operations, WebAssembly for hot paths, flow fields for pathfinding + +--- + +### 2. `analysis/performance-monitoring-strategy` + +**Purpose**: Strategy document for diagnosing production lag (CPU 10% -> 60%+ spikes causing rubber-banding). Implemented monitoring infrastructure. +**Head commit**: `c31ab1ce7` (telemetry threshold tuning) + +**Key files**: +- `PERFORMANCE_MONITORING_STRATEGY.md` -- 4-phase implementation plan: (1) enable Hytopia's Telemetry.initializeSentry with 25ms threshold, (2) add strategic performance spans to bot processing/pathfinding/combat, (3) CPU spike detection with game state snapshots at >40% CPU, (4) lightweight custom profiler (LagSpikeProfiler class). +- `PERFORMANCE_MONITORING_STATUS.md` -- Status report on what was implemented vs what works. Created `PerformanceLagDetector` service (CPU monitoring every 50ms, spike detection at 40%/60% thresholds, snapshots every 30s). Integrated Sentry v10 dual-SDK setup. Added spans to `BotBrain.think` and `ZoneNavigationService.findPath`. Problem: performance traces never appeared in Sentry despite correct span implementation -- likely Hytopia platform limitation. +- `add-granular-perf-tracking.sh` -- Bash script analyzing instrumentation gaps. Identifies methods >5ms without internal instrumentation (CTRotationStrategy.execute at 13.36ms peak, BotCombatSystem.processCombat at 14.94ms peak, TerroristStrategy.execute at 7.37ms peak). Recommends adding `frameBudgetMonitor.measure()` inside combat/navigation/strategy methods. +- `analyze-bottleneck.js` -- Node.js script analyzing worst-frame performance data from JSON stats. Parses per-operation timings, groups by category, identifies outliers. Found: `brain.bombDetection.Whiskey` took 22.65ms (62% of frame), combat operations only 6.96ms total -- bottleneck was NOT in combat/pathfinding but in bomb detection. +- `visualize-perf.js` -- ASCII flame chart and cumulative time visualization from stats JSON. Shows timing breakdown by P99 latency, cumulative time per operation, frame budget analysis. +- `generate-flame-chart.cjs` -- Generates interactive HTML flame chart from performance reports. Color-coded bars (hot/warm/cool), aggregated timing stats by category. +- `scripts/map-profile-to-code.cjs` -- Maps CPU profile (.cpuprofile) function names to game source code using CODEBASE_REF.md. Categorizes functions as game code vs Hytopia SDK vs unknown. +- `scripts/analyze-performance.cjs` -- Analyzes .cpuprofile files: builds call trees, calculates self/total times from samples, finds top 20 hot functions by self time. + +**Techniques**: +- Sentry dual-SDK setup (v10 for errors, Hytopia internal v9 for spans) +- PerformanceLagDetector service with CPU monitoring, spike detection, game state snapshots +- Hytopia Telemetry.startSpan() for bot thinking and pathfinding +- Performance stats JSON export + offline analysis scripts +- CPU profile to source code mapping + +**Notable findings**: +- Sentry performance traces do not appear in dashboard despite correct integration -- likely requires production Hytopia environment variables +- The PerformanceLagDetector works locally but Sentry trace export does not +- Bomb detection was the actual bottleneck (22.65ms), not combat/pathfinding as expected + +--- + +### 3. `analysis/code-hotspots-metrics` + +**Purpose**: Code hotspot analysis and bug pattern analysis. +**Head commit**: `0f9a175ad` (bug pattern analysis of last 1000 commits) + +**Key files**: +- `analysis-reports/bug-analysis/` -- Bug pattern analysis directory +- `PERFORMANCE_FIX_ANALYSIS.md` -- Detailed breakdown of fixable bottlenecks with before/after code and risk assessment: + 1. `_sendRecoilDataToUI` (65ms spike) -- Fix: throttle to 30fps, savings ~50ms + 2. `getCurrentRecoilOffset` (37ms spike) -- Fix: cache until state changes, savings ~30ms + 3. `findZoneForPosition` (61ms spike, 81ms total) -- Fix: spatial grid index (10-unit cells), savings ~40ms + 4. `updateTrackedFlashes` (41ms spike) -- Fix: singleton shared tracker instead of per-bot full entity scan, savings ~30ms + 5. `think()` bot AI -- Lower impact, higher risk + +**Techniques**: +- Static code analysis identifying hot functions by spike severity +- Proposed spatial grid indexing for zone lookups (O(n) -> O(1) amortized) +- Object allocation reduction (eliminate `.clone()` calls in hot paths) +- Throttling UI updates (60fps -> 30fps for recoil data) +- Singleton pattern for shared state (flash tracking) +- Git history churn analysis for bug patterns + +--- + +### 4. `docs/performance-analysis-oct-5` + +**Purpose**: October 5, 2025 performance sprint documentation. +**Head commit**: `d5e7a7412` (5-minute performance test analysis) + +**Key files**: +- `PERFORMANCE_FINDINGS.md` -- handleDeath optimization analysis. Root cause: weaponDrop (0.8-1.1ms) + deathEvent dispatch (0.7-1.5ms) accounting for entire 2-4ms death handling time. Fixes: removed JSON.parse/stringify from deathEvent, optimized grenade disposal logging. Result: 2.969ms -> 1.509ms average (49.2% faster), best case 74.3% faster. +- `scenarios/grenade-death.yaml` -- Test scenario definition for grenade death spike testing. Defines 60s duration, 10 iterations, threshold targets (handleDeath avg <20ms, max <50ms, P95 <35ms, weaponDrop avg <12ms). + +**Techniques**: +- Try-finally instrumentation blocks for granular timing measurement +- setTimeout(fn, 0) deferral for non-critical operations +- JSON serialization elimination from hot paths +- YAML-based test scenario definitions with pass/fail thresholds +- Automated testing with Puppeteer headless player support + +**Notable findings**: +- handleDeath: 49.2% improvement (2.969ms -> 1.509ms) +- Death visibility check: 99.6% improvement (1.532ms -> 0.006ms) +- Grenade disposal: 97% improvement (40-60ms -> 1.03ms) + +--- + +### 5. `docs/performance-analysis-outputs` + +**Purpose**: Comprehensive performance analysis outputs including CPU profile correlation, dependency mapping, Trello integration, and visualization. +**Head commit**: `feaea11a1` (add performance analysis outputs) + +**Key files**: +- `PERFORMANCE_ANALYSIS_SUMMARY.md` -- CPU profile analysis correlating 42,725 functions and 7.5M samples with 17 Trello cards. Top 3 critical fixes: BotTickService.tick (2.12ms spikes, affects 10 downstream issues), StoppingPowerManager.update (1.20ms spikes, affects 7 downstream), Global.checkTerroristApproaches (3.46ms spikes). Scoring system: impact (downstream fixes) + severity (spike size) + frequency. +- `PERFORMANCE_OPTIMIZATIONS_TODO.md` -- Phased optimization plan from profiling data: 192 high-variance functions, 5,080+ spikes, 99 ticks >3ms. Phase 1 low-hanging fruit: FrameBudgetMonitor sampling (90% overhead reduction), BotNavigator squared distance (60% reduction from eliminating Math.sqrt), ZoneVisibilityService zone caching (70% reduction). Phase 2: MomentumPlayerController config caching (worst offender at 33.977ms max spike, 270 spikes). +- `PERFORMANCE_DEPENDENCY_MAP.json` -- JSON dependency graph of 219 performance records with 25 root causes mapped to leaf symptoms. Largest cluster: BombEntity.completeDefusing with 42 nodes and 16 leaves. +- `PERFORMANCE_DEDUPED_TRELLO_LIST.md` -- Deduped Trello card list with spike data. BotTickService.tick: 351 spikes, 85 unique leaves, max non-idle 20.288ms. Detailed per-leaf analysis with caller context, spike counts, and percentage of cluster load. +- `perf_dependency_graph.html`, `perf_dependency_map.html` -- Interactive SVG dependency graph visualizations showing root cause -> symptom relationships. +- `SINGLE_RENDERER_ANALYSIS.md` -- Analysis of single vs dual Three.js WebGLRenderer for weapon view models. Dual renderer is safer (separate WebGL context, guaranteed to work); single renderer saves ~5% GPU but requires monkey-patching Hytopia's minified SDK. Runtime A/B switching with F9 debug panel. +- `scripts/capture-baseline.sh` -- Bash script: starts server with AUTO_START_WITH_BOTS, runs for configurable duration, extracts perf.* events from game logs, calculates statistics (avg/min/max/P50/P95/P99) per operation, outputs JSON baseline file. +- `scripts/compare-baselines.ts` -- TypeScript script comparing two baseline JSON files. Calculates avgChange/maxChange/p95Change percentages, flags regressions vs improvements, supports threshold parameter for CI pass/fail. +- `scripts/comprehensive-analysis.cjs` -- Analyzes replay files (.json.gz) from last 24 hours for tactical anomalies (bomb abandonment, CT not defusing, T not planting, stuck bots). + +**Techniques**: +- CPU profile parsing with call hierarchy construction (parent-child relationships) +- Spike detection with correlation to Trello cards +- Dependency graph construction (root causes vs symptoms) +- De-duplication analysis to identify cascading fix opportunities +- Scoring algorithm: weighted by downstream impact + spike severity + frequency +- Baseline capture and regression comparison (CI-ready) +- Replay file analysis for tactical/performance anomalies +- Interactive SVG dependency graph visualization + +**Notable findings**: +- Fixing BotTickService.tick alone should resolve 10 downstream issues +- Performance monitoring overhead (FrameBudgetMonitor itself) is 15.4% of profiled time -- ironic +- MomentumPlayerController.updateMovementConfig is the single worst offender at 33.977ms max spike + +--- + +### 6. `docs/performance-automation-playbook` + +**Purpose**: Profiling-to-fix workflow documentation. +**Head commit**: `3a2275f61` (add performance automation playbook) + +**Key files**: +- Links CLAUDE.md to performance automation playbook for AI agent consistency +- `ai-memory/` directory contains memory files from many performance fix branches: + - `fix/punch-offset-performance-*` + - `fix/recoil-ui-performance-*` + - `fix/tracked-flashes-performance-*` + - `fix/remove-unused-performance-components-*` + - `fix/knife-visibility-swap-*` + - `perf/` directory + +**Techniques**: +- Standardized profiling-to-fix workflow documentation +- AI memory files tracking progress on individual performance fixes +- Branch-isolated memory for context persistence across sessions + +--- + +### 7. `docs/performance-monitoring-ultrathink-analysis` + +**Purpose**: Deep analysis of the performance monitoring system itself, plus enhancement proposals for industry-standard format exports (Speedscope, Chrome Trace, Perfetto). +**Head commit**: `a6d5312e0` (comprehensive performance monitoring documentation) + +**Key files**: +- `ULTRATHINK_PERFORMANCE_ANALYSIS.md` (93KB) -- Complete technical deep-dive. Smart spike aggregation (groups `player.takeDamage.Yankee` + `.Echo` into `player.takeDamage` category with median/outlier detection). Tree filtering (filters <0.5ms operations, recursive significance check). Granular tracking (broke 6.8ms takeDamage black box into scoreboard 4.2ms + lookup 1.4ms + audio 0.6ms + UI 0.3ms). Fixed negative self-time display bug. +- `PERFORMANCE_SYSTEM_REALITY_CHECK.md` -- Debunks "9-layer architecture" claim. Reality: 4 separate loosely-connected systems (FrameBudgetMonitor, SessionSpikeTracker, PerformanceMonitor, PerformanceMetricsService) with only 5 integration points. All systems start automatically, data stays in RAM, F9 overlay is the only UI. +- `CPU_AND_MEMORY_MONITORING.md` -- Audit: memory tracking is real (process.memoryUsage()), CPU tracking is FAKE (hardcoded 0.7/0.3 multipliers because Bun lacks process.cpuUsage()). Memory shown only in exported reports, not in F9 overlay. Recommends adding memory to F9 UI. +- `PERFORMANCE_TRACKING_HIERARCHY.md` -- Complete map of all 82 unique `frameBudgetMonitor.measure()` tracking points across the codebase. 3-4 level hierarchy: game manager ops -> bot AI (deepest: bot.decision -> brain.combatCheck -> combat.findTarget -> combat.visibilityCheck). Documents every tracked operation with category and nesting. +- `GRANULAR_TRACKING_STATUS.md` -- Implementation status: added granular tracking to weapon shooting (5 sub-operations in Gun._shootProjectile), projectile physics (4 sub-operations in ProjectileEntity.fire), weapon pickup (4 sub-operations), bomb pickup (4 sub-operations). Skipped gm.handleTeamSelect (870 lines, too complex). +- `ADD_GRANULAR_TRACKING.md` -- Implementation guide for adding nested frameBudgetMonitor.measure() calls. +- `EXECUTIVE_SUMMARY.md` -- Research overview for Speedscope/Chrome Trace/Perfetto export. Current system is 95% complete. Missing only standard format exports. Measured overhead: 0.43ms/frame (2.6% of 16.66ms budget). Speedscope: 2-4 hours to implement. Chrome Trace: 6-10 hours. Proposes UnifiedProfiler, ProfileDataBuffer (ring buffer, 60s window, <100MB), PerformanceExporter, and enhanced bottleneck detectors. + +**Techniques**: +- Smart spike aggregation with dynamic suffix stripping (player names, weapon names, bot names) +- Outlier detection (>2x median threshold) +- Recursive tree filtering with significance checks +- Hierarchical call tree with self-time calculation +- Proposed: Chrome Trace Event Format export, Speedscope JSON export, adaptive sampling (3 modes: always/adaptive/on-spike) +- Ring buffer data management (<100MB, 60s window) + +**Notable findings**: +- Performance monitoring system is 4 real components, not 9 as previously documented +- CPU monitoring is completely fake (Bun doesn't expose process.cpuUsage()) +- Monitoring overhead is 15.4% of profiled time (FrameBudgetMonitor 6.59%, SpikeDetector start/end 8.81%) +- 82 unique tracking points in codebase + +--- + +### 8. `docs/performance-testing-infrastructure` + +**Purpose**: Testing infrastructure analysis and roadmap. +**Head commit**: `7d184799a` (comprehensive testing infrastructure analysis) + +**Key files**: +- `PERFORMANCE_FINDINGS.md` -- Same handleDeath analysis as branch #4 (Oct 5). Documents the 49.2% death handling improvement. + +**Techniques**: +- Documents the 3-layer existing monitoring system +- Identifies critical gaps: no headless browser automation, no regression framework, no scenario definitions, Bun vs Node inconsistency +- 4-phase roadmap: (1) Puppeteer + test hooks, (2) grenade fix validation, (3) automated test suite, (4) CI/CD integration + +--- + +### 9. `docs/perf-hotspots-20251004` + +**Purpose**: October 4, 2025 hotspot baseline capture. +**Head commit**: `5963a9e86` (summarize top performance hotspots) + +**Key files**: +- `PERFORMANCE_FINDINGS.md` -- Same handleDeath analysis as other branches. + +**Notable findings**: +- Top 10 HyFire hotspots by self time (from 10-minute AUTO_START_WITH_BOTS run): + 1. BotCoverServiceV2.log -- 369.81ms (10.75%) + 2. WalkingRunPlayerController.tickWithPlayerInput -- 314.78ms (9.15%) + 3. BotTickService.tick -- 264.67ms (7.70%) + 4. FrameBudgetMonitor.measure -- 226.77ms (6.59%) -- monitoring overhead + 5. SpikeDetector.endOperation -- 159.32ms (4.63%) -- monitoring overhead + 6. ZoneVisibilityService.findZoneForPosition -- 151.82ms (4.41%) + 7. SpikeDetector.startOperation -- 143.68ms (4.18%) -- monitoring overhead + 8. TerroristBombCarrierStrategy.execute -- 96.79ms (2.81%) + 9. StoppingPowerManager.update -- 92.72ms (2.70%) + 10. BB.think -- 81.37ms (2.37%) +- Monitoring overhead (#4, #5, #7) = 15.4% of total profiled time + +--- + +### 10. `docs/top-10-performance-analysis` + +**Purpose**: Real player session analysis with top 10 spike detection and fixes. +**Head commit**: `12864b831` (cache PlayerEffectsController to eliminate 7.15ms audio spike) + +**Key files**: +- `PERFORMANCE_FINDINGS.md` -- Same handleDeath analysis. +- `scenarios/grenade-death.yaml` -- Same scenario file as branch #4. + +**Techniques**: +- 10-minute real player session analysis +- 6,900 instrumented events analyzed +- Top issues identified: takeDamage (62.4ms total), damageEffectUI (26.7ms), handleDeath (23.6ms) +- 6 bad spikes >3ms (10.5% of damage events) +- Per-tick raycast caching with bidirectional optimization (A->B also caches B->A) +- PlayerEffectsController caching to eliminate 7.15ms audio spike + +**Notable findings**: +- 9 PRs merged on Oct 5 alone during performance sprint +- Audio spike: 99.9% reduction (7.15ms -> <0.01ms) +- Death visibility: 99.6% improvement (1.532ms -> 0.006ms) +- Raycast cache: 39% reduction (4,062 raycasts saved, bidirectional caching) + +--- + +### 11. `analysis/performance-work-3mo` + +**Purpose**: Comprehensive 3-month retrospective of all performance work (July-October 2025). +**Head commit**: `233c73ad0` (properly curated 63 performance PRs) + +**Key files**: +- `PERFORMANCE_WORK_EXECUTIVE_SUMMARY.md` -- 26 unmerged performance PRs representing 300+ hours of work. Top 5 critical PRs: (1) PR #1473 memory leak fixes, (2) PR #1406 remove sync file operations (5-second player join freeze), (3) PR #1184 bot navigation caching (8.8ms -> <1ms, 53% frame budget freed), (4) PR #1474 disable smoke animation spam, (5) PR #1477 automated performance testing with Puppeteer. +- `CURATED_PERFORMANCE_PRS_2025.md` -- 63 properly curated performance PRs across 4 months. Categories: spike elimination (26 PRs), caching optimizations (15 PRs), monitoring/infrastructure (8 PRs), asset/memory optimization (6 PRs), load distribution/async (5 PRs), log spam reduction (3 PRs). Peak days: Oct 5 (9 PRs), Sep 1 (8 PRs), Aug 16 (8 PRs), Aug 14 (6 PRs). +- `PERFORMANCE_PRS_LAST_3_MONTHS.md` -- Detailed breakdown of 21 PRs (11 merged, 10 open) from the October sprint. Documents per-PR metrics, test infrastructure, methodology. +- `UNMERGED_PERFORMANCE_WORK_ANALYSIS.md` -- Deep analysis of 26 unmerged PRs across 3 categories: monitoring infrastructure (13 PRs), backend optimizations (8 PRs), frontend/system optimizations (5 PRs). PR #1477 automated testing: headless Puppeteer, spike detection, baseline comparison, CI/CD ready (`npm run test:performance`, `npm run test:performance:baseline`, `npm run test:performance:compare`). +- `PERFORMANCE_PR_ACTION_CHECKLIST.md` -- Week-by-week action plan for merging critical PRs with testing requirements per PR. +- `PERFORMANCE_PRS_DATA.json` -- JSON data of all performance PRs. +- `PERFORMANCE_ANALYSIS_INDEX.md` -- Index/navigation document. + +**Notable findings (quantified improvements)**: +- Death visibility: 99.6% faster (1.5ms -> 0.006ms) +- Grenade disposal: 97% faster (40-60ms -> 1.03ms) +- Stopping power cache: 93% faster (3.45ms -> 0.24ms) +- MVP tracking: 93% faster in 10v10 (0.7ms -> <0.05ms) +- Entity lookups: ~90% iteration reduction +- Debug logging removal: 90% overhead eliminated +- Team elimination check: 80% faster (0.98ms -> 0.2ms) +- Raycast operations: ~80% reduction with zone visibility +- Map draw calls: 70% reduction (1,597 -> 487) +- handleDeath: 49.2% faster (3ms -> 1.5ms) +- Weapon drops: 42.8% faster +- Bot combat raycasts: 39% saved (4,062 fewer) +- Weapon attack: 35% faster (4.84ms -> 3.13ms) +- Log spam: 99.7% reduction (3,600/min -> 12/min) +- Recoil UI payload: 80% smaller (150 -> 30 bytes) +- Overall: average game tick went from 10ms+ to 1.11ms + +--- + +## Cross-Branch Patterns and Themes + +### Performance Monitoring Architecture (4 real layers) +1. **FrameBudgetMonitor** -- Instruments code with `.measure(name, fn)` calls, hierarchical call trees, spike detection >1ms. 82 unique tracking points across codebase. +2. **SessionSpikeTracker** -- All-time worst 20 spikes, recent spikes (15s window), pattern aggregation with log correlation. +3. **PerformanceMonitor** -- System metrics (memory real, CPU fake via Bun limitation), 1s sampling, 600-sample history (10 min). +4. **PerformanceMetricsService** -- Aggregates all other systems, sends to F9 UI overlay, supports JSON/CSV/Markdown/HTML export. + +### Key Scripts and Tools +| Script | Purpose | +|--------|---------| +| `scripts/benchmark-game.ts` | Automated benchmarking with scenarios | +| `scripts/profile-server-auto.ts` | Auto-saving profiler for WSL | +| `scripts/generate-flamegraph.ts` | HTML flame graph generation | +| `scripts/capture-baseline.sh` | Capture performance baseline JSON | +| `scripts/compare-baselines.ts` | Compare baselines for CI regression | +| `scripts/analyze-performance.cjs` | CPU profile (.cpuprofile) analysis | +| `scripts/map-profile-to-code.cjs` | Map profile functions to source | +| `scripts/comprehensive-analysis.cjs` | Replay file tactical analysis | +| `add-granular-perf-tracking.sh` | Instrumentation gap analysis | +| `analyze-bottleneck.js` | Worst-frame bottleneck analysis | +| `visualize-perf.js` | ASCII flame chart + cumulative time | +| `generate-flame-chart.cjs` | Interactive HTML flame chart | + +### Optimization Techniques Used +1. **Caching** -- Most common technique (15 PRs). Zone lookups, stopping power, SceneUI references, debug UI config, recoil offsets, input state, bomb entity lookups, alive players list. +2. **Deferral** -- setTimeout(fn, 0) for non-blocking operations: damage UI, weapon drops, MVP tracking, grenade disposal. +3. **Throttling** -- Rate-limit frequent operations: recoil UI to 20-30fps, bot navigator warnings to 1/5s. +4. **Spatial indexing** -- Grid-based zone lookups replacing linear scan (O(n) -> O(1)). +5. **Squared distance** -- Eliminate Math.sqrt() in distance comparisons. +6. **Object allocation reduction** -- Avoid .clone(), cache computed values, pre-allocate. +7. **Log spam elimination** -- Remove debug logging from hot paths, rate-limit warnings. +8. **Bidirectional caching** -- Raycast A->B also caches B->A result. +9. **Singleton shared state** -- Replace per-bot entity scans with shared tracker. +10. **Async conversion** -- Replace synchronous file I/O with startup-time loading + memory cache. + +### Testing Infrastructure +- **Headless browser automation**: Puppeteer for automated performance testing +- **Test hooks**: `ENABLE_TEST_HOOKS=true`, `AUTO_START_WITH_BOTS=true` +- **Scenario definitions**: YAML files with duration, iterations, metric thresholds, pass/fail criteria +- **Baseline comparison**: JSON baseline capture + regression detection for CI/CD +- **npm commands**: `npm run test:performance`, `npm run test:performance:baseline`, `npm run test:performance:compare` +- **F9 overlay**: Real-time performance monitoring UI with export (JSON, CSV, Markdown, Speedscope, flame chart HTML) + +### Known Limitations +1. CPU monitoring is fake (Bun lacks process.cpuUsage()) +2. Sentry performance traces do not appear in dashboard (Hytopia platform limitation) +3. Performance monitoring overhead is 15.4% of profiled time (FrameBudgetMonitor + SpikeDetector) +4. 26 performance PRs remained unmerged as of Oct 2025 despite being production-ready +5. Proposed Speedscope/Chrome Trace exports were never fully implemented (research complete, code examples ready) + +--- + +## Summary Statistics + +- **Total performance PRs (curated)**: 63 +- **Unmerged PRs (as of Oct 2025)**: 26 +- **Estimated development time**: 300+ hours +- **Unique tracking points**: 82 frameBudgetMonitor.measure() calls +- **Peak sprint**: 9 PRs merged on October 5, 2025 +- **Overall improvement**: avg game tick from 10ms+ to 1.11ms +- **Frame budget target**: 16.67ms @ 60 FPS (game now uses ~6.7% of budget) diff --git a/ai-memory/docs/perf-framework-research-2026-03-05/hyfire2-perf-fix-branches.md b/ai-memory/docs/perf-framework-research-2026-03-05/hyfire2-perf-fix-branches.md new file mode 100644 index 00000000..77411166 --- /dev/null +++ b/ai-memory/docs/perf-framework-research-2026-03-05/hyfire2-perf-fix-branches.md @@ -0,0 +1,345 @@ +# HyFire2 Performance Branch Research + +Research date: 2026-03-05 +Source repo: ~/GitHub/games/hyfire2 +All perf/* branches merged to master. fix/* performance branches: some merged, some open. + +--- + +## PERF/* BRANCHES (All Merged) + +### 1. perf/cache-sceneui-takedamage (PR #1446) +**Problem:** `updateTeammateNameTagHealth()` called `sceneUIManager.getAllSceneUIs()` on every damage event, iterating ALL SceneUIs in the world just to find the 1-4 nametags attached to the damaged player. Combined with takeDamage hot path, this was causing multi-ms spikes per hit. + +**Fix:** +- Cache array of SceneUIs attached to each player (`_teammateNameTagSceneUIs: SceneUI[]`) +- First call: search all SceneUIs, build cache. Subsequent calls: use cached array directly. +- Added full performance instrumentation to `takeDamage()` with checkpoint timing (logging, healthCalc, nametag, mvpTracking, sourceTracking, audioAndEffects, healthUI, damageEffectUI) +- Same instrumentation added to `handleDeath()` (init, botCacheInvalidate, audioCleanup, deathEffects, bombCancellation, broadcast, uiNotify, itemDrop) + +**Profiling pattern:** `performance.now()` checkpoints at each section boundary, logged as `perfCheckpoints` object with `.toFixed(3)` values. + +**Results from PERFORMANCE_FINDINGS.md:** +- Death handling: 3ms avg -> 1.5ms avg (50% improvement) +- weaponDrop bottleneck: ~1ms (JSON.stringify for deathEvent + grenade disposal logging) +- Removed `JSON.parse(JSON.stringify())` from deathEvent dispatch, saved ~0.5ms +- Optimized grenade disposal logging (batch instead of per-grenade), saved ~0.1ms + +--- + +### 2. perf/stopping-power-optimization (PR #1451) +**Problem:** `StoppingPowerManager` was loaded via `require()` on every tick and every damage event. Module resolution is not free. + +**Fix:** +- Cached `require()` result in module-level variable with lazy initialization pattern: + ```typescript + let stoppingPowerManagerCache: StoppingPowerManagerType | null = null; + const getStoppingPowerManager = (): StoppingPowerManagerType => { + if (!stoppingPowerManagerCache) { + const { StoppingPowerManager } = require('./managers/StoppingPowerManager'); + stoppingPowerManagerCache = StoppingPowerManager.getInstance(); + } + return stoppingPowerManagerCache; + }; + ``` +- Applied in GameManager tick handler AND GamePlayerEntity damage handler +- Added early exit in `StoppingPowerManager.update()` when no active effects: `if (this.activeEffects.size === 0 && this.cumulativeHits.size === 0) return;` + +**Key pattern:** Cache dynamic `require()` calls at module scope. Check empty collections before doing work. + +--- + +### 3. perf/reduce-weapon-fire-spike (PR #1457) +**Problem:** Multiple bots firing simultaneously caused massive frame spikes from synchronous projectile creation (physics body creation is expensive in Rapier3D). + +**Fixes:** +1. **Deferred projectile creation:** Wrapped `ProjectileEntity` creation + spawn + fire in `setTimeout(() => { ... }, 0)` to spread physics body creation across ticks +2. **Removed redundant planting/defusing checks** from `_canShoot()` (already done in `shoot()`) +3. **Throttled recoil UI updates** to every 50ms (20/sec max) instead of every tick +4. **Skip zero-recoil updates:** Early return when `Math.abs(recoilOffset.x) < 0.0001 && Math.abs(recoilOffset.y) < 0.0001` + +**Key pattern:** `setTimeout(fn, 0)` to defer expensive synchronous work to next tick. Reduces spike severity at cost of 1-frame latency (imperceptible). + +--- + +### 4. perf/reduce-bot-combat-spike (PR #1458) +**Problem:** Multiple bots independently raycasting to check visibility against the SAME targets on the same tick. 5v5 = up to 25 raycasts/tick when all bots in combat. Profiling showed 120ms+ spikes from `BotCombatSystem.validateTargetVisibility`. + +**Fix:** Created `RaycastCacheService` singleton: +- Per-tick cache (Map keyed by `${botId}_${targetId}`) +- Cleared every tick via `onTick()` called from GameManager tick handler +- **Bidirectional caching:** If A can see B, cache both A->B and B->A (visibility is symmetric along a raycast line) +- Also added zone-based pre-check in `BombVisibilityService`: `ZoneVisibilityService.shouldCheckVisibility()` before expensive raycast + +**Results:** 39% overall raycast reduction. Peak 59.2% during intense combat. Scales better with more bots. + +**Key pattern:** Per-tick cache with bidirectional key storage. Clear cache at tick boundary. + +--- + +### 5. perf/fix-damage-ui-spikes (PR #1462) +**Problem:** Damage audio playback and hit effects (blood particles, damage direction UI) were synchronous in the takeDamage hot path. When multiple players take damage simultaneously, these stack. + +**Fix:** Deferred non-critical work to next tick using `setTimeout(() => { ... }, 0)`: +- Damage audio playback +- Blood hit effect creation (PlayerEffectsController) +- Damage direction UI update to client + +**Key pattern:** Same `setTimeout(fn, 0)` deferral pattern for non-gameplay-critical visual/audio effects. + +--- + +### 6. perf/optimize-death-visibility-check (PR #1463) +**Problem:** Death handling did synchronous physics updates (disable collisions, move player below map) which blocked the main thread for ~1.5ms. + +**Fixes:** +1. **Deferred physics updates** on death: collision group clearing + position teleport wrapped in `setTimeout(() => { ... }, 0)` +2. **Cached `require()` for DeathCameraSystem** using same module-level lazy pattern as stopping power + +**Key pattern:** `setTimeout(fn, 0)` for physics state changes that don't need to be frame-perfect. Cache dynamic imports. + +--- + +### 7. perf/investigate-weapon-drop (PR #1465) +**Problem:** Dropping weapons/grenades on death was synchronous and caused spikes, especially with multiple grenades. + +**Fix:** Rewrote item dropping to be sequential-deferred: +- Collect all items to drop (best weapon + grenades) into an array +- Drop them one-per-tick using recursive `setTimeout`: + ```typescript + const dropItemsSequentially = (items, tickIndex = 0) => { + if (tickIndex >= items.length) return; + setTimeout(() => { + // drop items[tickIndex] + dropItemsSequentially(items, tickIndex + 1); + }, 0); + }; + ``` + +**Key pattern:** Sequential deferred processing for batch operations that would otherwise spike a single tick. + +--- + +### 8. perf/optimize-mvp-tracking (PR #1469) +**Problem:** MVP damage tracking (scoreboard updates, XP tracking) was synchronous in takeDamage hot path. Also did expensive `gameManager.getPlayer(killerId)` lookup to check if attacker was a bot. + +**Fixes:** +1. **Deferred MVP tracking** to next tick via `setTimeout(fn, 0)` +2. **Replaced expensive bot check:** Instead of `gameManager.getPlayer(killerId) -> attackerEntity.isBot()`, used simple string prefix check: `killerId.startsWith('bot_')` + +**Key pattern:** Replace expensive lookups with convention-based shortcuts when possible. Defer non-critical tracking. + +--- + +### 9. perf/remove-damage-debug-logging (PR #1470) +**Problem:** Extensive debug logging in the damage source tracking hot path was causing 4ms+ spikes. Logs included `Array.from(gameManager.players.keys())` and multiple `eventLogger.debug/info/warn` calls per hit. + +**Fix:** Removed all debug/info/warn logging from the damage source tracking section and stopping power application section. Kept only error-level logging. + +**Key insight:** In a 60Hz game loop, even "debug" logging has real cost. String interpolation, object creation for log context, and the logger's own overhead add up when called 10+ times per damage event. + +--- + +### 10. perf/rate-limit-bot-navigator-warnings (PR #1471) +**Problem:** Bot navigator warnings for "cannot navigate" (dead bot) and "position access error" fired every tick per affected bot, creating log spam. + +**Fix:** Added rate limiting with timestamps: +```typescript +private lastCannotNavigateTime: number = 0; +private readonly WARNING_RATE_LIMIT_MS = 5000; + +if (now - this.lastCannotNavigateTime > this.WARNING_RATE_LIMIT_MS) { + eventLogger.warn(...); + this.lastCannotNavigateTime = now; +} +``` + +**Key pattern:** Time-based rate limiting for warnings that can fire every tick. + +--- + +### 11. perf/remove-grenade-logging-overhead (PR #1515) +**Problem:** Grenade attack attempt logging was extremely verbose, firing on every left-click when holding a grenade. Included pouchSize, activeGrenadeIndex, cooldown calculations. Cooldown UI feedback was also sent on every blocked click. + +**Fix:** Removed all verbose logging from grenade attack path. Removed cooldown UI feedback messages. Kept only the error-level log for failed attacks. + +**Savings:** ~44 lines of logging code removed from the per-click hot path. + +--- + +### 12. perf/optimize-recoil-offset-calls (PR #1255) +**Problem:** `RecoilSystem.getCurrentRecoilOffset()` called every tick per player (and in `_canShoot()`), always cloning the vector and applying recovery calculation. + +**Fixes:** +1. **Added frame-level cache** to RecoilSystem: + ```typescript + private cachedOffset: Vector3 | null = null; + private lastCacheTime: number = 0; + private readonly CACHE_DURATION_MS = 16; // 1 frame at 60fps + ``` +2. Cache invalidated on `addRecoil()` and `reset()` +3. Skip recoil UI updates when offset is near-zero + +**Key pattern:** Frame-duration caching for values computed multiple times per tick. + +--- + +### 13. perf/web-audio-kill-sounds (PR #1678) +**Problem:** Kill sounds used `new Audio()` + `cloneNode()` (HTML5 Audio API), which has high latency and creates DOM elements. + +**Fix:** Rewrote to use Web Audio API: +- Create `AudioContext` on first user interaction (autoplay policy compliance) +- Pre-decode all audio files into `AudioBuffer` objects on init +- On kill: create lightweight `BufferSource` node, connect to gain node, `source.start(0)` +- Latency: ~5-20ms (vs ~100-200ms for HTML5 Audio) +- No DOM element creation, no cloneNode overhead + +**Key pattern:** Pre-decode audio into AudioBuffers for instant playback. BufferSource nodes are designed for single use and are lightweight. + +--- + +### 14. perf/add-bot-combat-instrumentation (Open, not merged) +**What it does:** Adds `performance.now()` timing to two bot combat methods: +- `BotCombatSystem` angle-based reaction modifier calculation +- `shouldAllowMovementDuringCombat()` check + +Pure instrumentation branch, no optimization. Logged as `perfMs` in existing event logger calls. + +--- + +### 15. perf/bomb-retrieval-optimization (Open, not merged) +**Problem:** Bomb retrieval bot selection was slow due to: +1. Awaited logging calls (`await eventLogger.debug(...)`) blocking the selection loop +2. `Math.sqrt()` for distance calculations when only relative comparison needed +3. No position/distance caching + +**Fixes:** +1. **Fire-and-forget logging:** Changed `await eventLogger.debug(...)` to `eventLogger.debug(...).catch(() => {})` throughout BotNavigator and BotGameService +2. **Squared distance comparison:** Replaced `Math.sqrt(dx*dx + dz*dz)` with raw `dx*dx + dz*dz` for scoring (sqrt is monotonic, so comparison order preserved) +3. **Performance instrumentation** throughout entire call chain with timing for each phase + +**Key pattern:** Never `await` logging in hot paths. Use squared distance for comparisons. + +--- + +### 16. perf/optimize-bot-updates (Open, not merged) +**Problem:** `TerroristSupportStrategy.execute()` taking 3.2ms per bot, with `brain.supportExecute` at 2.2ms (95% self-time). Root causes: +- `Array.from(assignedHoldPositions.values()).includes()` = O(n^2) +- `findBombCarrier()` cache too short (100ms) +- `getBots()` iterated frequently + +**Fixes:** +1. **O(n^2) -> O(1):** Replaced `Array.from().includes()` with `new Set().has()` for hold position filtering +2. **Bomb carrier cache extended:** 100ms -> 500ms (carrier rarely changes) +3. **Entry fragger cache invalidation:** Added `clearEntryFraggerCache()` called when roles change (support promoted to entry fragger) +4. **Comprehensive performance instrumentation** with labeled checkpoints per objective phase +5. **Build position-to-bot map once** instead of `Array.from().some()` per position in patrol logic + +**Key pattern:** Use Set for O(1) lookups instead of Array.includes(). Extend cache durations with proper invalidation. + +--- + +### 17. perf/optimize-logging-overhead (Open, not merged) +**Problem:** `BotCoverServiceV2` logged extensively even in production. `EventLogger` always ran `Promise.all([logToConsole, logToFile])` even when both were disabled. + +**Fixes:** +1. **Environment-gated logging:** Added `BOT_COVER_LOGS_ENABLED` env var check; skip all info/debug logging unless enabled +2. **Early exit in EventLogger.log():** Check if any output targets are enabled BEFORE constructing log entry: + ```typescript + const shouldLogToConsole = this.config.console.enabled && this.shouldLog(level, 'console'); + const shouldLogToFile = this.config.file.enabled && this.shouldLog(level, 'file'); + if (!shouldLogToConsole && !shouldLogToFile && !shouldNotifySubscribers) return; + ``` +3. **Conditional Promise.all:** Only create promise array for enabled outputs + +**Key pattern:** Early exit before constructing log objects. Gate verbose logging behind env vars. + +--- + +## FIX/* PERFORMANCE BRANCHES + +### 18. fix/10v10-performance-analysis (Open, not merged) +**Problem:** 10v10 mode (20 players) caused severe performance degradation. Root cause: O(n^2) broadcast -- every player's state sent to every other player every tick. + +**Fixes:** +1. **DistanceCullingService:** Skip updates for players beyond max distance (60 units default, 40 in perf mode). Different update rates by distance tier: + - Close (<20): every tick + - Medium (<40): every 3 ticks + - Far (<60): every 6 ticks +2. **OptimizedBroadcastService:** Batched updates with priority scoring (distance + team + armed). Limits max players per update (12 in perf mode, 20 default). +3. **Accuracy data dedup:** Only send UI updates when JSON.stringify(data) changes +4. **Teammate nametag SceneUIs disabled** (load calls commented out) for testing + +**Key pattern:** Distance-based LOD for network updates. Tiered update rates. Dedup before send. + +--- + +### 19. fix/teammate-ui-performance-lag (Open, not merged) +**Problem:** SceneUI-based teammate nametags were a massive GPU/CPU bottleneck: +- Each SceneUI creates a DOM layer with CSS compositing +- `text-shadow` on nametags caused per-frame GPU repaints +- Duplicate SceneUIs created (one per viewer per teammate = n^2 SceneUIs) +- `getAllSceneUIs()` iterated on every health update + +**Fix progression (11 commits, iterative):** +1. Removed `text-shadow` (GPU repaint trigger) +2. Removed forced GPU layer (`will-change`, `transform: translateZ(0)`) +3. Eliminated duplicate SceneUI creation (50% reduction) +4. Added memoization + visibility optimization +5. **Final solution:** Replaced all SceneUI nametags with screen-space UI. Server sends `teammate_positions` data every 3 ticks via `player.ui.sendData()`, client renders nametags in HTML overlay using camera projection. +6. Reverted intermediate approaches that didn't work + +**Key insight:** SceneUI (world-space HTML overlay per entity) is expensive at scale. Screen-space UI (single HTML layer with projected positions) is far cheaper. + +--- + +### 20. fix/memory-optimizations (Open, not merged) +**Problem:** Chrome heap timeline showed 12-30MB garbage collected every 5 seconds. 1,331 Array references, 372 Set references in heap dump. Mouse skipping and micro-stutters from GC pauses. + +**Root causes identified:** +1. `BotManager.getBots()` returned `[...this._bots]` (new array copy) on every call, called 10+ times/sec +2. A* pathfinding created `new Map()` + `new Set()` per path request. 10 bots x 10 ticks/sec = 300 Maps/Sets per second +3. Player filtering via `Array.from(map.values()).filter()` creating 2 arrays per call +4. Blood particle test spawning particles every 5 seconds during warmup + +**Fixes:** +1. **BotManager.getBots():** Return `readonly` reference to internal array (zero allocation) +2. **PathfindingCache:** Pre-allocated pool of 20 Maps and 20 Sets. A* acquires from pool, uses, releases back. Path results cached for 30 seconds with LRU eviction. +3. **PlayerCache:** Singleton that pre-categorizes players by team/alive/human/bot in single pass. Reuses same array instances (`array.length = 0` + push). Returns `readonly` references. +4. **Disabled blood particle test** during warmup + +**MEMORY_OPTIMIZATION_FINDINGS.md documents:** Bun/JSC Rust aliasing rules that can crash the server if you do `entity.position.x = 5; entity.position.y = 10;` (multiple mutable borrows). Must copy position first. + +**Key patterns:** Object pooling for hot-path allocations. Return readonly references instead of copies. Pre-categorize collections in single pass. `array.length = 0` to reuse array identity. + +--- + +## CROSS-CUTTING PATTERNS SUMMARY + +### Profiling Techniques Used +1. **Checkpoint timing:** `const perfStart = performance.now(); ... perfCheckpoints.name = performance.now() - perfStart;` +2. **Conditional logging:** Only log when time exceeds threshold (0.1ms, 0.3ms, 0.5ms) +3. **Per-tick cache stats:** Hit/miss counters with periodic reporting (every 60 ticks) +4. **Chrome heap timeline analysis:** For memory/GC issues (identified 1331 Array refs, 372 Set refs) + +### Top Optimization Patterns (Ranked by Impact) +1. **setTimeout(fn, 0) deferral** -- Used in 5+ branches. Spreads synchronous work across ticks. Best for: audio, effects, physics state, item drops, MVP tracking. Cost: 1 frame latency (imperceptible). +2. **Per-tick raycast cache** -- 39% raycast reduction with bidirectional keying. Scales with player count. +3. **Object/collection pooling** -- PathfindingCache pools Maps/Sets. PlayerCache reuses arrays. Eliminated 300+ allocations/sec. +4. **Replace SceneUI with screen-space UI** -- n^2 SceneUI DOM layers replaced with single HTML overlay + projected positions. +5. **Cache dynamic require()** -- Module-level lazy singleton pattern. Eliminates module resolution on every call. +6. **Set-based lookups** -- Replace `Array.from(map.values()).includes()` (O(n)) with `new Set(map.values()).has()` (O(1)). +7. **Squared distance comparison** -- Skip Math.sqrt() when only comparing relative distances. +8. **Fire-and-forget logging** -- `.catch(() => {})` instead of `await` for log calls in hot paths. +9. **Rate-limited warnings** -- Timestamp-based throttle for per-tick warnings. +10. **Remove debug logging from hot paths** -- Even "debug" level has real cost (string creation, object allocation, logger overhead). + +### Recurring Anti-Patterns Found +- `await eventLogger.debug(...)` in loops (blocks iteration on log I/O) +- `Array.from(map.values()).includes()` (O(n) scan, creates intermediate array) +- `[...this._bots]` on every getter call (constant array allocation) +- `require('module')` inside tick/damage handlers (module resolution overhead) +- `JSON.parse(JSON.stringify(obj))` for deep copy (expensive serialization roundtrip) +- `sceneUIManager.getAllSceneUIs()` to find specific UI elements (full scan) +- Verbose logging in per-damage, per-tick, per-grenade paths +- Creating new Map/Set in A* pathfinding (GC pressure at 300/sec) diff --git a/ai-memory/docs/perf-framework-research-2026-03-05/hytopia-sdk-perf-systems.md b/ai-memory/docs/perf-framework-research-2026-03-05/hytopia-sdk-perf-systems.md new file mode 100644 index 00000000..ebf4f4e7 --- /dev/null +++ b/ai-memory/docs/perf-framework-research-2026-03-05/hytopia-sdk-perf-systems.md @@ -0,0 +1,410 @@ +# HYTOPIA SDK/Engine Performance Infrastructure Deep-Dive + +Date: 2026-03-05 +Codebase: /home/ab/GitHub/hytopia/work1 + +--- + +## 1. SERVER-SIDE PERFORMANCE SYSTEMS + +### 1.1 Telemetry (Sentry-based span profiling) + +**File:** `server/src/metrics/Telemetry.ts` + +The primary server profiling system wraps Sentry's tracing SDK. Key design: + +- **Zero-overhead in dev**: `Telemetry.startSpan()` checks `Sentry.isInitialized()` and if false, simply calls the callback directly -- no timing overhead at all. +- **Production gating**: Only sends spans to Sentry when `tickTimeMs > tickTimeMsThreshold` (default 50ms). This means normal ticks are never reported -- only slow ticks. +- **Span hierarchy**: The TICKER_TICK span is the root transaction. Nested inside it are WORLD_TICK spans, and nested inside those are sub-spans for each phase. + +**Defined span operations** (enum `TelemetrySpanOperation`): +``` +TICKER_TICK -- root: the fixed-timestep ticker loop + WORLD_TICK -- one full world tick + ENTITIES_TICK -- entity.tick() for all active entities + SIMULATION_STEP -- Rapier physics step + cleanup + PHYSICS_STEP -- rapier.step() only + PHYSICS_CLEANUP -- drain events + collider cleanup + ENTITIES_EMIT_UPDATES -- entity.checkAndEmitUpdates() + NETWORK_SYNCHRONIZE -- full network sync flush + BUILD_PACKETS -- (not currently used in WorldLoop) + SERIALIZE_PACKETS -- msgpackr.pack() + gzip if >64KB + SERIALIZE_PACKETS_ENCODE -- (sub-span, not directly used) + SEND_PACKETS -- per-connection send + SEND_ALL_PACKETS -- send loop over all players + NETWORK_SYNCHRONIZE_CLEANUP -- clear queues + caches + SERIALIZE_FREE_BUFFERS -- (not currently used) +``` + +**Process stats** (`Telemetry.getProcessStats()`): +- `jsHeapSizeMb`, `jsHeapCapacityMb`, `jsHeapUsagePercent`, `processHeapSizeMb`, `rssSizeMb` +- Attached to every Sentry error event and every slow-tick transaction + +**Usage sites** (7 files import Telemetry): +- `WorldLoop.ts` -- WORLD_TICK + sub-spans +- `Ticker.ts` -- TICKER_TICK root span +- `Simulation.ts` -- PHYSICS_STEP, PHYSICS_CLEANUP +- `Connection.ts` -- SERIALIZE_PACKETS, SEND_PACKETS +- `NetworkSynchronizer.ts` -- SEND_ALL_PACKETS, NETWORK_SYNCHRONIZE_CLEANUP +- `index.ts` -- re-export for SDK consumers + +### 1.2 World Loop Timing + +**File:** `server/src/worlds/WorldLoop.ts` + +The world loop wraps `Ticker` and runs at 60Hz (DEFAULT_TICK_RATE). Performance instrumentation: + +- `performance.now()` captured at tick start and end +- `TICK_START` event payload includes `tickDeltaMs` (the fixed timestep) +- `TICK_END` event payload includes `tickDurationMs` (actual wall-clock time spent) +- SDK developers can listen to `WorldLoopEvent.TICK_END` to monitor per-tick cost + +**Tick order:** +1. `entityManager.tickEntities(tickDeltaMs)` -- all active (non-environmental) entities +2. `simulation.step(tickDeltaMs)` -- Rapier physics +3. `entityManager.checkAndEmitUpdates()` -- dirty-flag position/rotation change detection +4. `networkSynchronizer.synchronize()` -- only every 2nd tick (30Hz) + +### 1.3 Ticker (Fixed Timestep Engine) + +**File:** `server/src/shared/classes/Ticker.ts` + +Key performance constants: +- `TICK_SLOW_UPDATE_CAP = 2` -- max catch-up ticks per loop iteration (prevents spiral of death) +- `MAX_ACCUMULATOR_TICK_MULTIPLE = 3` -- clamp accumulator to prevent massive catch-up + +Uses `setTimeout` (not `setImmediate`) to give the main thread breathing room for GC. The delay is calculated as `Math.max(0, fixedTimestepMs - accumulatorMs)`. + +### 1.4 Physics Simulation Timing + +**File:** `server/src/worlds/physics/Simulation.ts` + +- `performance.now()` at step start/end +- Emits `SimulationEvent.STEP_START` and `STEP_END` with `stepDurationMs` +- Debug rendering (wireframe colliders) is gated by `_debugRenderingEnabled` -- explicitly warns "avoid in production; can cause noticeable lag" +- Debug raycasting emits per-raycast events when enabled + +### 1.5 Network Synchronizer Performance Design + +**File:** `server/src/networking/NetworkSynchronizer.ts` (very large, ~1200 lines) + +Performance-critical design decisions: +- **30Hz sync rate**: `TICKS_PER_NETWORK_SYNC = Math.round(60 / 30) = 2` -- network flushes every other physics tick +- **Reliable vs unreliable splitting**: Entity position/rotation updates go over unreliable WebTransport datagrams (90%+ of traffic), other entity updates go reliable +- **Lazy queue clearing**: "We only clear queues if they aren't empty, otherwise it causes significant memory growth and triggers unnecessary major GCs" -- explicit GC-aware optimization +- **IterationMap** (`server/src/shared/classes/IterationMap.ts`): Custom Map+Array hybrid for ~2x faster iteration than `Map.values()`. Used throughout NetworkSynchronizer for all sync queues. +- **Packet serialization cache**: `Connection._cachedPacketsSerializedBuffer` caches encoded packets by array identity -- encode once, send to N players. Cleared each sync cycle. + +### 1.6 Connection & Packet Performance + +**File:** `server/src/networking/Connection.ts` + +- **Gzip compression**: Packets >64KB are gzip-compressed at level 1 (fast) +- **WebTransport unreliable cap**: Datagrams >1200 bytes promoted to reliable (MTU-aware) +- **Serialization telemetry**: SERIALIZE_PACKETS span records packet count, IDs, and serialized byte count +- **Per-send span**: SEND_PACKETS wraps each connection.send() + +### 1.7 Sync Request/Response (RTT Measurement) + +**Server side** (`Player.ts` + `NetworkSynchronizer.ts`): +- Client sends SyncRequest packet (null payload) every 2 seconds +- Server records `Date.now()` and `performance.now()` on receipt +- Server responds with SyncResponse containing: + - `r`: server absolute time at request receipt + - `s`: server absolute time at response + - `p`: high-res processing time (ms) from receipt to response + - `n`: ms until next server tick + +### 1.8 Model Preloading Timing + +**File:** `server/src/models/ModelRegistry.ts` +- `performance.now()` before/after model preloading loop +- Console logs total preload time + +### 1.9 GameServer Start Timing + +**File:** `server/src/GameServer.ts` +- Emits `GameServerEvent.START` with `startedAtMs: performance.now()` + +--- + +## 2. CLIENT-SIDE PERFORMANCE SYSTEMS + +### 2.1 PerformanceMetricsManager (FPS + Memory) + +**File:** `client/src/core/PerformanceMetricsManager.ts` + +Core metrics manager that runs every frame: +- **FPS**: Calculated every 1 second (`FPS_UPDATE_INTERVAL_IN_SEC = 1.0`), averaged over that window +- **Delta time**: Uses Three.js `Clock.getDelta()` +- **Memory**: Reads `performance.memory` (Chrome-only API) for `usedJSHeapSize` and `totalJSHeapSize` +- **Refresh rate estimation**: Samples 30 `requestAnimationFrame` deltas, trims 10% outliers, rounds to nearest common rate (30/60/72/90/120/144/165/240/300/360) +- Exposed properties: `fps`, `deltaTime`, `frameCount`, `usedMemory`, `totalMemory`, `refreshRate` + +### 2.2 Debug Panel (lil-gui + Stats.js) + +**File:** `client/src/core/DebugPanel.ts` + +Full debug overlay toggled with backtick (`) or F3 (or 5-finger touch on mobile): + +**Stats.js panels:** +- Default FPS/MS panels (from three/examples Stats) +- Custom MB panel (heap memory in MB) +- Custom RTT(ms) panel (round-trip time to server) + +**lil-gui folders:** +- **Lobby**: lobby ID +- **User Agent**: browser info +- **Player**: position +- **Camera**: position +- **Server**: send/receive protocol (ws/wt), SDK version +- **Performance**: quality preset level +- **WebGL**: draw calls, geometries, textures, triangles, programs (from `renderer.info`) +- **Entity**: count, static environment, in-view-distance, frustum-culled, update-skip, animation play, local/world matrix updates, light-level updates, custom textures +- **Chunks**: count, visible, blocks, opaque/transparent/liquid faces, block textures +- **glTF**: file count, source/cloned/instanced meshes, draw calls saved, attribute elements updated +- **Scene UI**: count, visible +- **Arrows**: count, visible +- **Audio**: count, matrix updates, skip matrix updates + +### 2.3 Stats Classes (Per-Subsystem Counters) + +All use static fields, reset per-frame by their respective managers: + +**EntityStats** (`client/src/entities/EntityStats.ts`): +- count, staticEnvironmentCount, inViewDistanceCount, frustumCulledCount, updateSkipCount +- animationPlayCount, localMatrixUpdateCount, worldMatrixUpdateCount, lightLevelUpdateCount, customTextureCount + +**ChunkStats** (`client/src/chunks/ChunkStats.ts`): +- count, visibleCount, blockCount, opaqueFaceCount, transparentFaceCount, liquidFaceCount, blockTextureCount + +**GLTFStats** (`client/src/gltf/GLTFStats.ts`): +- fileCount, sourceMeshCount, clonedMeshCount, instancedMeshCount, drawCallsSaved, attributeElementsUpdated + +**AudioStats** (`client/src/audio/AudioStats.ts`): +- count, matrixUpdateCount, matrixUpdateSkipCount + +**ArrowStats** (`client/src/arrows/ArrowStats.ts`): +- count, visibleCount + +**SceneUIStats** (`client/src/ui/SceneUIStats.ts`): +- count, visibleCount + +### 2.4 Renderer Performance + +**File:** `client/src/core/Renderer.ts` + +- `renderer.info.autoReset = false` -- manual reset per frame via `renderer.info.reset()` before render calls +- WebGL stats read from `renderer.info`: draw calls, geometries, textures, triangles, programs +- FPS cap via `SettingsManager.qualityPerfTradeoff.fpsCap` -- skips render frames if elapsed time < 1/fpsCap +- Scene-level `matrixAutoUpdate = false` and `matrixWorldAutoUpdate = false` on all 4 scenes (main, viewModel, overlay, UI) -- all matrix updates are manual +- Custom transparent sort function using cached sort keys per render frame +- WebGL context loss detection with user alert + +### 2.5 Automatic Quality Adjustment + +**File:** `client/src/settings/SettingsManager.ts` + +Dynamic quality presets: ULTRA, HIGH, MEDIUM, LOW, POWER_SAVING + +Each preset controls: +- Resolution multiplier (0.5x to 2.0x) +- View distance (50 to 600 units) +- Fog near/far +- Environmental animations (enabled/disabled) +- Post-processing (outline, bloom, SMAA) +- FPS cap (30 for POWER_SAVING) +- Antialias + +**Auto-adjustment algorithm:** +- Warmup period: 10 seconds after first world packet before any adjustment +- Quality up: FPS >= refreshRate - 1 for 5 consecutive seconds +- Quality down: FPS < min(30, refreshRate * 0.5) for 3 consecutive seconds +- Bounce protection: max 5 up/down oscillations before locking +- Mobile cap: MEDIUM max on mobile +- Auto levels: HIGH, MEDIUM, LOW only (ULTRA and POWER_SAVING are manual-only) +- Tab visibility check: skips adjustment when tab inactive + +### 2.6 Performance Timeline Marks + +**File:** `client/src/network/NetworkManager.ts` + `client/src/chunks/ChunkManager.ts` + +Uses Performance API marks/measures for startup profiling (visible in browser DevTools Performance tab): +- `NetworkManager:connecting` -- mark at connection start +- `NetworkManager:connected` -- mark when connected +- `NetworkManager:connected-time` -- measure: connecting to connected +- `NetworkManager:world-packet-received` -- mark at first world packet +- `NetworkManager:connected-to-first-packet-time` -- measure: connected to first packet +- `NetworkManager:game-ready-time` -- measure: connecting to game ready +- `ChunkManager:first-chunk-batch-built` -- mark when first chunk batch is built +- `ChunkManager:first-chunk-batch-built-time` -- measure: connected to first chunk batch + +### 2.7 RTT Measurement (Client Side) + +**File:** `client/src/network/NetworkManager.ts` + +- Sends SyncRequest every 2 seconds +- Receives SyncResponse with server processing time +- Calculates RTT: `clientReceiveTime - syncStartTime - serverProcessingTime` +- Exponential moving average with smoothing factor 0.5 +- Tracks max RTT +- Displayed in debug panel RTT(ms) stats panel + +### 2.8 Entity View Distance + Frustum Culling + +**File:** `client/src/entities/Entity.ts` + `client/src/entities/EntityManager.ts` + +- Per-entity view distance check using squared distance (avoids sqrt) +- Frustum culling tracked in EntityStats +- Update skipping for entities outside view distance +- Matrix update optimization: only updates local/world matrices when dirty + +### 2.9 Chunk Worker (Web Worker Meshing) + +**File:** `client/src/workers/ChunkWorker.ts` + +- Chunk mesh building runs in a Web Worker (off main thread) +- Greedy meshing with ambient occlusion +- No explicit timing/profiling inside the worker itself + +--- + +## 3. PROTOCOL PERFORMANCE SUPPORT + +### 3.1 Debug Packets + +**DebugConfig (inbound):** `protocol/packets/inbound/DebugConfig.ts` + `protocol/schemas/DebugConfig.ts` +- Schema: `{ pdr?: boolean }` -- toggles physics debug rendering +- Client sends this to enable/disable server-side debug render + +**PhysicsDebugRender (outbound):** `protocol/packets/outbound/PhysicsDebugRender.ts` +- Sends collider wireframe vertices/colors per tick when enabled +- Very expensive -- for development only + +**PhysicsDebugRaycasts (outbound):** `protocol/packets/outbound/PhysicsDebugRaycasts.ts` +- Sends raycast visualization data when debug raycasting enabled + +### 3.2 Sync Request/Response (RTT) + +- `SyncRequest` (inbound): null payload, triggers server timestamp capture +- `SyncResponse` (outbound): `{ r: number, s: number, p: number, n: number }` + - `r` = server time at request receipt (Date.now()) + - `s` = server time at response (Date.now()) + - `p` = high-res processing time (ms) + - `n` = ms until next server tick + +### 3.3 Server Tick in Packets + +All outbound packets include a `WorldTick` field (the current world loop tick count). This enables the client to reason about packet ordering and staleness. + +--- + +## 4. SDK EXAMPLES WITH PERFORMANCE RELEVANCE + +### 4.1 big-world + +**File:** `sdk-examples/big-world/index.ts` + +Explicit stress test: 750x750 block area (~2M+ blocks, thousands of chunks). Comments state it is "meant to showcase the performance of the server" and "benchmark and test client performance." No custom profiling code -- just loads a huge map and spawns players. + +### 4.2 ark-game (WorldGenerator) + +**File:** `sdk-examples/ark-game/src/generator/WorldGenerator.ts` + +Has per-pass timing using `performance.now()`: +``` +[Generator] Starting NxN world (seed: X) +[Generator] TerrainPass: Xms +[Generator] CavePass: Xms +... +[Generator] Complete: N blocks in Xms +``` + +--- + +## 5. PERFORMANCE DATA STRUCTURES + +### 5.1 IterationMap + +**File:** `server/src/shared/classes/IterationMap.ts` + +Custom Map+Array hybrid for hot-path iteration. Maintains a backing `Map` for O(1) lookups and a separate `V[]` array for fast iteration without `Map.values()` overhead. Used in all NetworkSynchronizer sync queues. Lazy dirty-flag array rebuild. + +### 5.2 Connection Packet Cache + +**File:** `server/src/networking/Connection.ts` + +Static `Map` caches serialized packets by array identity. Encode-once, send-to-N optimization. Cleared each network sync cycle via `Connection.clearCachedPacketsSerializedBuffers()`. + +--- + +## 6. GAPS IDENTIFIED + +### 6.1 Server Gaps + +1. **No local profiling without Sentry**: Telemetry.startSpan() is a no-op when Sentry is not initialized. There is NO built-in way to profile tick timing locally during development without setting up a Sentry DSN. The TICK_END event emits `tickDurationMs` but nothing aggregates or logs it. + +2. **No tick budget tracking**: No system tracks what percentage of the ~16.67ms tick budget is consumed, or warns when ticks consistently exceed budget. The Ticker's `TICK_SLOW_UPDATE_CAP = 2` silently caps catch-up without logging. + +3. **No per-entity cost attribution**: `ENTITIES_TICK` is one span for ALL entities. There is no way to identify which entity's `tick()` callback is expensive. No per-entity timing. + +4. **No network bandwidth metrics**: Serialized byte count is recorded in Sentry span attributes but never aggregated or exposed. No per-player bandwidth tracking. No packet-rate counters. + +5. **No console.time / console.timeEnd usage**: Zero instances of `console.time` in the server codebase. Developers must rely on Sentry or roll their own timing. + +6. **No GC monitoring**: While `process.memoryUsage()` is captured in Telemetry.getProcessStats(), there is no GC event tracking (e.g., `--expose-gc` / `performance.measureUserAgentSpecificMemory()`). The GC-aware clearing in NetworkSynchronizer is based on experience, not measured. + +7. **No entity count budget or scaling warnings**: Nothing warns the developer when entity counts or chunk counts approach limits that would degrade tick performance. + +### 6.2 Client Gaps + +1. **No frame time breakdown**: The client tracks FPS but does NOT break down frame time into components (render time, JS time, animation update time, network processing time, etc.). The DebugPanel shows subsystem counters but not timings. + +2. **No GPU profiling**: No use of WebGL timer queries (`EXT_disjoint_timer_query`). The renderer tracks draw calls/triangles/geometries but not actual GPU milliseconds. + +3. **No chunk meshing timing**: The ChunkWorker does greedy meshing in a Web Worker but has no timing instrumentation. Slow chunk builds are invisible. + +4. **No memory trend tracking**: Memory is sampled once per frame but there is no leak detection, no trend analysis, no warning system for memory growth. + +5. **No network jitter metrics**: RTT is tracked with exponential smoothing but there is no jitter calculation (variance of RTT), no packet loss counting, no out-of-order detection. + +6. **No client-side Telemetry equivalent**: The client has no span/trace system. All client perf monitoring is ad-hoc (Stats.js panels + static counters). + +7. **Stats classes are not time-series**: All Stats classes (EntityStats, ChunkStats, etc.) are instantaneous counters reset each frame. No historical data, no min/max/avg tracking, no percentiles. + +8. **Quality auto-adjustment uses simple FPS threshold**: The algorithm only looks at whether FPS is above or below a threshold for N seconds. It does not consider frame time variance, GPU load, or thermal state. + +9. **performance.memory is Chrome-only**: The memory tracking in PerformanceMetricsManager relies on `performance.memory` which is non-standard and Chrome-only. Firefox/Safari users get no memory data. + +### 6.3 Protocol Gaps + +1. **No performance telemetry packet**: There is no packet type for the server to send tick timing data to the client for display. The debug panel cannot show "server tick time: Xms" or "server entity count: N" because no packet carries that data. + +2. **No client-to-server performance report**: The client cannot report its FPS, frame time, or quality level back to the server for server-side analytics. + +### 6.4 SDK Examples Gaps + +1. **big-world has no automated benchmark**: It loads a large world but has no timing, no entity stress test, no automated performance measurement. It's a manual "look at it and see if it's slow" test. + +2. **No dedicated benchmark example**: No SDK example exercises entity spawning at scale, particle systems under load, or rapid block modifications to establish performance baselines. + +--- + +## 7. SUMMARY TABLE + +| System | Location | What it Measures | Trigger/Availability | +|--------|----------|-----------------|---------------------| +| Telemetry spans | server/src/metrics/Telemetry.ts | Tick subsystem durations | Only with Sentry DSN + slow tick | +| WorldLoop events | server/src/worlds/WorldLoop.ts | tickDeltaMs, tickDurationMs | Always (SDK event) | +| Simulation events | server/src/worlds/physics/Simulation.ts | stepDurationMs | Always (SDK event) | +| Process stats | server/src/metrics/Telemetry.ts | Heap, RSS memory | On Sentry error/slow tick | +| FPS | client/src/core/PerformanceMetricsManager.ts | Frames per second | Always | +| Memory | client/src/core/PerformanceMetricsManager.ts | JS heap (Chrome only) | Always | +| RTT | client/src/network/NetworkManager.ts | Round-trip latency | Every 2 seconds | +| WebGL stats | client/src/core/DebugPanel.ts | Draw calls, triangles, etc | When debug panel open | +| Entity stats | client/src/entities/EntityStats.ts | Count, culling, updates | Always (static counters) | +| Chunk stats | client/src/chunks/ChunkStats.ts | Count, faces, visibility | Always (static counters) | +| Quality auto-adjust | client/src/settings/SettingsManager.ts | FPS vs threshold | Always | +| Startup timeline | client/src/network/NetworkManager.ts | Connection-to-ready timing | On startup | +| Debug rendering | server/src/worlds/physics/Simulation.ts | Collider wireframes | Manual toggle (dev only) | +| Packet cache | server/src/networking/Connection.ts | Serialized buffer reuse | Always (implicit) | diff --git a/ai-memory/docs/perf-framework-research-2026-03-05/raw/COLLIDER_ARCHITECTURE_RESEARCH.md b/ai-memory/docs/perf-framework-research-2026-03-05/raw/COLLIDER_ARCHITECTURE_RESEARCH.md new file mode 100644 index 00000000..3fe81f1a --- /dev/null +++ b/ai-memory/docs/perf-framework-research-2026-03-05/raw/COLLIDER_ARCHITECTURE_RESEARCH.md @@ -0,0 +1,159 @@ +# Collider Architecture Research + +**Purpose:** Guide the refactor of Hytopia’s block collider system from O(world) to O(nearby chunks). +**Audience:** Engineers implementing Phase 1 (Collider Locality) and Phase 2 (Incremental Voxel Updates). + +--- + +## 1. Current Architecture + +### 1.1 Block Type → Collider + +- One collider per **block type** (dirt, stone, etc.), not per block. +- Voxel collider: Rapier voxel grid; each cell = block present/absent. +- Trimesh collider: Used for non-cube blocks; rebuilt when any block of that type changes. + +### 1.2 Critical Path + +``` +setBlock / addChunkBlocks + → _addBlockTypePlacement + → _getBlockTypePlacements() // iterates ALL chunks of this block type + → _combineVoxelStates(collider) // merges placements into voxel grid + → collider.addToSimulation / setVoxel +``` + +**Problem:** `_getBlockTypePlacements` and `_combineVoxelStates` touch every chunk that contains the block type. As world size grows, this becomes O(world). + +--- + +## 2. Target Architecture: Spatial Locality + +### 2.1 Principle + +- Colliders should only include blocks from chunks **within N chunks of any player** (e.g. N=4). +- When a chunk unloads (player moves away), remove its blocks from colliders. +- When a chunk loads, add its blocks to colliders only if it’s within the active radius. + +### 2.2 Data Structure Change + +**Current:** `_blockTypePlacements` is global (or implicitly spans all chunks). + +**Target:** Maintain a **spatial index**: + +```ts +// Chunk key (bigint) → for each block type in that chunk: Set of global coordinates +private _chunkBlockPlacements: Map>> = new Map(); + +// Active chunk keys: chunks within COLLIDER_RADIUS of any player +private _activeColliderChunkKeys: Set = new Set(); +``` + +- On chunk load: add chunk key to index; add block placements. +- On chunk unload: remove chunk key; remove blocks from colliders. +- `_getBlockTypePlacements` for collider: only return placements from `_activeColliderChunkKeys`. +- `_combineVoxelStates`: only iterate over placements from active chunks. + +### 2.3 Update Flow + +``` +Player moves + → Update _activeColliderChunkKeys (chunks within radius) + → For chunks that left radius: remove from colliders + → For chunks that entered radius: add to colliders + → _combineVoxelStates only over active placements +``` + +--- + +## 3. Incremental Voxel Updates + +### 3.1 Current + +- Adding a chunk: all 4096 blocks added at once to the voxel collider. +- Heavy: `setVoxel` 4096 times + propagation. + +### 3.2 Target + +- Add blocks in **batches** (e.g. 256–512 per tick). +- Time-budget: stop when budget exceeded; resume next tick. +- Rapier voxel API: check if it supports incremental `setVoxel` without full rebuild. + +### 3.3 Implementation Sketch + +```ts +private _pendingVoxelAdds: Array<{ chunk: Chunk; blockTypeId: number; nextIndex: number }> = []; + +function processPendingVoxelAdds(timeBudgetMs: number) { + const start = performance.now(); + while (this._pendingVoxelAdds.length > 0 && (performance.now() - start) < timeBudgetMs) { + const next = this._pendingVoxelAdds[0]; + const chunk = next.chunk; + const count = Math.min(256, chunk.blockCountForType(next.blockTypeId) - next.nextIndex); + for (let i = 0; i < count; i++) { + const idx = next.nextIndex + i; + const globalCoord = chunk.getGlobalCoordinateFromIndex(idx); + collider.setVoxel(globalCoord, true); + } + next.nextIndex += count; + if (next.nextIndex >= chunk.blockCountForType(next.blockTypeId)) { + this._pendingVoxelAdds.shift(); + } + } +} +``` + +--- + +## 4. Trimesh Optimization + +### 4.1 Current + +- Trimesh collider rebuilt whenever any block of that type is added/removed. +- Rebuild = collect all placements, generate mesh, replace collider. + +### 4.2 Options + +1. **Spatial locality:** Only include trimesh blocks from active chunks. Reduces vertex count for large worlds. +2. **Deferred rebuild:** Queue rebuild; execute in next tick within time budget. +3. **Per-chunk trimesh:** If block type is sparse, consider per-chunk trimesh instances instead of one giant trimesh. (Larger change.) + +**Recommendation:** Start with (1) and (2). (3) is Phase 6. + +--- + +## 5. Collider Unload + +When a chunk unloads: + +1. Remove its block placements from the spatial index. +2. For each block type in that chunk: + - Voxel: `setVoxel(coord, false)` for each placement. + - Trimesh: trigger rebuild (only over active chunks). +3. Remove chunk from `_activeColliderChunkKeys`. + +--- + +## 6. Rapier Voxel API Notes + +- Check `rapier3d` docs for `ColliderDesc.heightfield` vs `ColliderDesc.voxel`. +- Voxel colliders: typically a 3D grid; `setVoxel` may or may not support incremental updates. +- If full rebuild required per update: minimize rebuild frequency (batch changes) and scope (active chunks only). + +--- + +## 7. Success Criteria + +| Metric | Before | After | +|--------|--------|-------| +| Chunks scanned per collider update | O(world) | O(active) ~100–300 | +| Time per `_combineVoxelStates` | 5–50 ms | <2 ms | +| Collider add spikes | Full chunk at once | Batched, time-budgeted | + +--- + +## References + +- `ChunkLattice.ts` – `_addChunkBlocksToColliders`, `_combineVoxelStates`, `_getBlockTypePlacements` +- Rapier3D voxel API +- Minecraft: per-section collision, spatial culling diff --git a/ai-memory/docs/perf-framework-research-2026-03-05/raw/ENTITY_SYNC_DELTA_COMPRESSION_DESIGN (1).md b/ai-memory/docs/perf-framework-research-2026-03-05/raw/ENTITY_SYNC_DELTA_COMPRESSION_DESIGN (1).md new file mode 100644 index 00000000..c689a4de --- /dev/null +++ b/ai-memory/docs/perf-framework-research-2026-03-05/raw/ENTITY_SYNC_DELTA_COMPRESSION_DESIGN (1).md @@ -0,0 +1,226 @@ +# Entity Sync: Delta / Compression Design + +**Goal:** Reduce entity position/rotation packet size and bandwidth (currently ~90% of all packets) by replacing full pos/rot with delta or compressed formats. + +--- + +## 1. Current State + +### Flow +- **Server:** Every tick, `entityManager.checkAndEmitUpdates()` runs; each entity calls `checkAndEmitUpdates()`. +- **Entity:** Emits `UPDATE_POSITION` or `UPDATE_ROTATION` when change exceeds threshold: + - **Position:** `ENTITY_POSITION_UPDATE_THRESHOLD_SQ = 0.04²` (0.04 block) + - **Rotation:** `ENTITY_ROTATION_UPDATE_THRESHOLD = cos(3°/2)` (~3°) + - **Player:** Looser position threshold `0.1²` blocks +- **NetworkSynchronizer:** Queues `{ i: id, p: [x,y,z] }` and/or `{ i: id, r: [x,y,z,w] }`. +- **Every 2 ticks (30 Hz):** Splits into reliable vs unreliable; pos/rot-only goes to **unreliable** channel. +- **Serializer:** `serializeVector` → `[x, y, z]`, `serializeQuaternion` → `[x, y, z, w]` (full floats). +- **Transport:** msgpackr with `useFloat32: FLOAT32_OPTIONS.ALWAYS` → 4 bytes per float. + +### Per-Entity Packet Size (approx) +| Format | Bytes (msgpack) | +|--------|-----------------| +| `{ i, p }` pos-only | ~25–35 | +| `{ i, r }` rot-only | ~30–40 | +| `{ i, p, r }` both | ~50–65 | +| 10 entities, pos+rot | ~500–650 | + +With 20 entities at 30 Hz: **~15–20 KB/s** for entity sync alone. + +--- + +## 2. Options for Delta / Compression + +### Option A: Quantized Position (Fixed-Point) + +**Idea:** Encode position as integers. 1 unit = 1/256 block → 0.004 block precision. + +- Range ±32768 blocks → 16-bit signed per axis. +- 3 × 2 bytes = **6 bytes** vs 3 × 4 = 12 bytes (float32). +- **~50% smaller** for position. + +**Implementation:** +```ts +// Server +const QUANT = 256; +p: [Math.round(x * QUANT), Math.round(y * QUANT), Math.round(z * QUANT)] + +// Client +position.x = p[0] / QUANT; // etc. +``` + +**Trade-off:** Precision ~0.004 block. For player/NPC movement this is fine. For very small objects, may need higher quant (e.g. 1024). + +--- + +### Option B: Quantized Quaternion (Smallest-Three) + +**Idea:** Unit quaternion has `q.x² + q.y² + q.z² + q.w² = 1`. Store the 3 components with largest magnitude; reconstruct 4th. + +- 3 × 2 bytes (quantized) = **6 bytes** vs 4 × 4 = 16 bytes. +- **~62% smaller** for rotation. + +**Implementation:** Standard "smallest three" quaternion compression (e.g. [RigidBodyDynamics](https://github.com/gameworks-builder/rigid-body-dynamics) style). Needs protocol change to support packed format. + +--- + +### Option C: Yaw-Only for Player Rotation + +**Idea:** Many entities (players, NPCs) only rotate around Y. Send 1 float (yaw) instead of 4. + +- **4 bytes** vs 16 bytes. +- **75% smaller** for rotation when applicable. + +**Caveat:** Doesn't work for entities with pitch/roll (e.g. flying, vehicles). Use as opt-in per entity type. + +--- + +### Option D: Delta Encoding (Δ from Last Sent) + +**Idea:** Send `Δp = p - p_last` instead of absolute `p`. Small movements → small deltas → msgpack encodes as smaller integers. + +- No schema change; still `[dx, dy, dz]` but values typically small. +- msgpack variable-length integers: small values use 1 byte. +- **Benefit:** 20–50% smaller when movement is small. No extra state on client if server tracks last-sent. + +**Implementation:** Server stores `_lastSentPosition` per entity per player (or broadcast). Send delta; client adds to last known position. Requires client to track "last applied" position. + +--- + +### Option E: Bulk / AoS Format + +**Idea:** Instead of `[{i:1,p:[x,y,z]},{i:2,p:[x,y,z]},...]` use structure of arrays: + +```ts +{ ids: [1,2,3], p: [[x,y,z],[x,y,z],[x,y,z]] } +``` + +- Avoids repeating keys `i`, `p` for every entity (msgpack dedup helps but structure still has overhead). +- **Benefit:** ~15–25% smaller from less map/array framing. + +**Caveat:** Requires new packet schema and client deserializer changes. All-or-nothing; can't mix with current EntitySchema in same packet. + +--- + +### Option F: Distance-Based Sync Rate + +**Idea:** Sync nearby entities at 30 Hz, distant at 10 Hz or 5 Hz. + +- **Benefit:** Fewer packets for far entities; natural LOD. +- **Implementation:** In `checkAndEmitUpdates` or NetworkSynchronizer, track distance from each player; only queue updates for entity if `tick % rateDivisor === 0` based on distance band. + +--- + +## 3. Recommended Approach + +### Phase 1: Low-Risk Wins (1–2 days each) + +| # | Change | Impact | Effort | +|---|--------|--------|--------| +| 1 | **Quantized position** (1/256 block) | ~50% smaller pos | 1 day | +| 2 | **Distance-based sync rate** (30/15/5 Hz bands) | Fewer far-entity updates | 1 day | +| 3 | **Yaw-only rotation** for player entities | ~75% smaller rot for players | 0.5 day | + +### Phase 2: Schema Changes (3–5 days) + +| # | Change | Impact | Effort | +|---|--------|--------|--------| +| 4 | **Quantized quaternion** (smallest-three) | ~62% smaller rot | 2–3 days | +| 5 | **Bulk entity update packet** | ~15–25% smaller framing | 2 days | + +### Phase 3: Advanced (Optional) + +| # | Change | Impact | Effort | +|---|--------|--------|--------| +| 6 | **Delta encoding** | Additional 20–50% when movement small | 2–3 days | +| 7 | **Client-side prediction** | Reduce perceived latency, fewer corrections | 1+ week | + +--- + +## 4. Protocol Changes Required + +### Option 1: Extend EntitySchema (Backwards Compatible) + +Add optional compressed fields; client detects and uses when present: + +```ts +// New optional fields +EntitySchema = { + i: number; + p?: VectorSchema; // existing: [x,y,z] float + r?: QuaternionSchema; // existing: [x,y,z,w] float + pq?: [number,number,number]; // quantized position (1/256 block) + rq?: [number,number,number]; // quantized quaternion (smallest-three) + ry?: number; // yaw only (radians) + // ... +} +``` + +- Server sends `pq` instead of `p` when quantized format enabled. +- Client checks `pq` first, falls back to `p`. +- Old clients ignore `pq`; new clients prefer `pq` when present. + +### Option 2: New Packet Type + +Add `EntityPosRotBulkPacket`: + +```ts +{ + ids: number[], + positions?: Int16Array | number[][], // quantized + rotations?: number[][] | Int16Array[] // quantized or yaw-only +} +``` + +- Used only for unreliable pos/rot updates. +- Existing `EntitiesPacket` still used for spawn/reliable updates. + +--- + +## 5. Key Files + +| Component | Path | +|-----------|------| +| Entity update emission | `server/src/worlds/entities/Entity.ts` (checkAndEmitUpdates) | +| Player threshold | `server/src/worlds/entities/PlayerEntity.ts` | +| Network sync queue | `server/src/networking/NetworkSynchronizer.ts` | +| Serializer | `server/src/networking/Serializer.ts` | +| Protocol schema | `protocol/schemas/Entity.ts` | +| Client deserializer | `client/src/network/Deserializer.ts` | +| Client entity update | `client/src/entities/EntityManager.ts` (_updateEntity) | +| Transport | `server/src/networking/Connection.ts`, `client/.../NetworkManager.ts` | + +--- + +## 6. Quantization Constants (Suggested) + +```ts +// Position: 1/256 block = 0.0039 block precision +const POSITION_QUANT = 256; + +// Position range: ±32768 blocks (16-bit signed) +// Covers ~1km in each direction +const POSITION_MAX = 32767; +const POSITION_MIN = -32768; + +// Quaternion: 16-bit per component, range [-1, 1] → 1/32767 precision +const QUATERNION_QUANT = 32767; +``` + +--- + +## 7. Success Metrics + +| Metric | Current | Target (Phase 1) | Target (Phase 2) | +|--------|---------|------------------|------------------| +| Entity bytes/update (10 entities) | ~500–650 | ~300–400 | ~200–280 | +| Entity sync % of total packets | ~90% | ~70% | ~50% | +| Bandwidth (20 entities, 30 Hz) | ~15–20 KB/s | ~8–12 KB/s | ~5–8 KB/s | + +--- + +## 8. References + +- [Quaternion Compression (smallest three)](http://gafferongames.com/networked-physics/snapshot-compression/) +- [Minecraft entity sync (delta/quantization)](https://wiki.vg/Protocol#Entity_Metadata) +- Current codebase: `Entity.ts` (checkAndEmitUpdates), `NetworkSynchronizer.ts` (entity sync split), `Serializer.ts` (serializeVector/Quaternion) diff --git a/ai-memory/docs/perf-framework-research-2026-03-05/raw/ENTITY_SYNC_DELTA_COMPRESSION_DESIGN.md b/ai-memory/docs/perf-framework-research-2026-03-05/raw/ENTITY_SYNC_DELTA_COMPRESSION_DESIGN.md new file mode 100644 index 00000000..c689a4de --- /dev/null +++ b/ai-memory/docs/perf-framework-research-2026-03-05/raw/ENTITY_SYNC_DELTA_COMPRESSION_DESIGN.md @@ -0,0 +1,226 @@ +# Entity Sync: Delta / Compression Design + +**Goal:** Reduce entity position/rotation packet size and bandwidth (currently ~90% of all packets) by replacing full pos/rot with delta or compressed formats. + +--- + +## 1. Current State + +### Flow +- **Server:** Every tick, `entityManager.checkAndEmitUpdates()` runs; each entity calls `checkAndEmitUpdates()`. +- **Entity:** Emits `UPDATE_POSITION` or `UPDATE_ROTATION` when change exceeds threshold: + - **Position:** `ENTITY_POSITION_UPDATE_THRESHOLD_SQ = 0.04²` (0.04 block) + - **Rotation:** `ENTITY_ROTATION_UPDATE_THRESHOLD = cos(3°/2)` (~3°) + - **Player:** Looser position threshold `0.1²` blocks +- **NetworkSynchronizer:** Queues `{ i: id, p: [x,y,z] }` and/or `{ i: id, r: [x,y,z,w] }`. +- **Every 2 ticks (30 Hz):** Splits into reliable vs unreliable; pos/rot-only goes to **unreliable** channel. +- **Serializer:** `serializeVector` → `[x, y, z]`, `serializeQuaternion` → `[x, y, z, w]` (full floats). +- **Transport:** msgpackr with `useFloat32: FLOAT32_OPTIONS.ALWAYS` → 4 bytes per float. + +### Per-Entity Packet Size (approx) +| Format | Bytes (msgpack) | +|--------|-----------------| +| `{ i, p }` pos-only | ~25–35 | +| `{ i, r }` rot-only | ~30–40 | +| `{ i, p, r }` both | ~50–65 | +| 10 entities, pos+rot | ~500–650 | + +With 20 entities at 30 Hz: **~15–20 KB/s** for entity sync alone. + +--- + +## 2. Options for Delta / Compression + +### Option A: Quantized Position (Fixed-Point) + +**Idea:** Encode position as integers. 1 unit = 1/256 block → 0.004 block precision. + +- Range ±32768 blocks → 16-bit signed per axis. +- 3 × 2 bytes = **6 bytes** vs 3 × 4 = 12 bytes (float32). +- **~50% smaller** for position. + +**Implementation:** +```ts +// Server +const QUANT = 256; +p: [Math.round(x * QUANT), Math.round(y * QUANT), Math.round(z * QUANT)] + +// Client +position.x = p[0] / QUANT; // etc. +``` + +**Trade-off:** Precision ~0.004 block. For player/NPC movement this is fine. For very small objects, may need higher quant (e.g. 1024). + +--- + +### Option B: Quantized Quaternion (Smallest-Three) + +**Idea:** Unit quaternion has `q.x² + q.y² + q.z² + q.w² = 1`. Store the 3 components with largest magnitude; reconstruct 4th. + +- 3 × 2 bytes (quantized) = **6 bytes** vs 4 × 4 = 16 bytes. +- **~62% smaller** for rotation. + +**Implementation:** Standard "smallest three" quaternion compression (e.g. [RigidBodyDynamics](https://github.com/gameworks-builder/rigid-body-dynamics) style). Needs protocol change to support packed format. + +--- + +### Option C: Yaw-Only for Player Rotation + +**Idea:** Many entities (players, NPCs) only rotate around Y. Send 1 float (yaw) instead of 4. + +- **4 bytes** vs 16 bytes. +- **75% smaller** for rotation when applicable. + +**Caveat:** Doesn't work for entities with pitch/roll (e.g. flying, vehicles). Use as opt-in per entity type. + +--- + +### Option D: Delta Encoding (Δ from Last Sent) + +**Idea:** Send `Δp = p - p_last` instead of absolute `p`. Small movements → small deltas → msgpack encodes as smaller integers. + +- No schema change; still `[dx, dy, dz]` but values typically small. +- msgpack variable-length integers: small values use 1 byte. +- **Benefit:** 20–50% smaller when movement is small. No extra state on client if server tracks last-sent. + +**Implementation:** Server stores `_lastSentPosition` per entity per player (or broadcast). Send delta; client adds to last known position. Requires client to track "last applied" position. + +--- + +### Option E: Bulk / AoS Format + +**Idea:** Instead of `[{i:1,p:[x,y,z]},{i:2,p:[x,y,z]},...]` use structure of arrays: + +```ts +{ ids: [1,2,3], p: [[x,y,z],[x,y,z],[x,y,z]] } +``` + +- Avoids repeating keys `i`, `p` for every entity (msgpack dedup helps but structure still has overhead). +- **Benefit:** ~15–25% smaller from less map/array framing. + +**Caveat:** Requires new packet schema and client deserializer changes. All-or-nothing; can't mix with current EntitySchema in same packet. + +--- + +### Option F: Distance-Based Sync Rate + +**Idea:** Sync nearby entities at 30 Hz, distant at 10 Hz or 5 Hz. + +- **Benefit:** Fewer packets for far entities; natural LOD. +- **Implementation:** In `checkAndEmitUpdates` or NetworkSynchronizer, track distance from each player; only queue updates for entity if `tick % rateDivisor === 0` based on distance band. + +--- + +## 3. Recommended Approach + +### Phase 1: Low-Risk Wins (1–2 days each) + +| # | Change | Impact | Effort | +|---|--------|--------|--------| +| 1 | **Quantized position** (1/256 block) | ~50% smaller pos | 1 day | +| 2 | **Distance-based sync rate** (30/15/5 Hz bands) | Fewer far-entity updates | 1 day | +| 3 | **Yaw-only rotation** for player entities | ~75% smaller rot for players | 0.5 day | + +### Phase 2: Schema Changes (3–5 days) + +| # | Change | Impact | Effort | +|---|--------|--------|--------| +| 4 | **Quantized quaternion** (smallest-three) | ~62% smaller rot | 2–3 days | +| 5 | **Bulk entity update packet** | ~15–25% smaller framing | 2 days | + +### Phase 3: Advanced (Optional) + +| # | Change | Impact | Effort | +|---|--------|--------|--------| +| 6 | **Delta encoding** | Additional 20–50% when movement small | 2–3 days | +| 7 | **Client-side prediction** | Reduce perceived latency, fewer corrections | 1+ week | + +--- + +## 4. Protocol Changes Required + +### Option 1: Extend EntitySchema (Backwards Compatible) + +Add optional compressed fields; client detects and uses when present: + +```ts +// New optional fields +EntitySchema = { + i: number; + p?: VectorSchema; // existing: [x,y,z] float + r?: QuaternionSchema; // existing: [x,y,z,w] float + pq?: [number,number,number]; // quantized position (1/256 block) + rq?: [number,number,number]; // quantized quaternion (smallest-three) + ry?: number; // yaw only (radians) + // ... +} +``` + +- Server sends `pq` instead of `p` when quantized format enabled. +- Client checks `pq` first, falls back to `p`. +- Old clients ignore `pq`; new clients prefer `pq` when present. + +### Option 2: New Packet Type + +Add `EntityPosRotBulkPacket`: + +```ts +{ + ids: number[], + positions?: Int16Array | number[][], // quantized + rotations?: number[][] | Int16Array[] // quantized or yaw-only +} +``` + +- Used only for unreliable pos/rot updates. +- Existing `EntitiesPacket` still used for spawn/reliable updates. + +--- + +## 5. Key Files + +| Component | Path | +|-----------|------| +| Entity update emission | `server/src/worlds/entities/Entity.ts` (checkAndEmitUpdates) | +| Player threshold | `server/src/worlds/entities/PlayerEntity.ts` | +| Network sync queue | `server/src/networking/NetworkSynchronizer.ts` | +| Serializer | `server/src/networking/Serializer.ts` | +| Protocol schema | `protocol/schemas/Entity.ts` | +| Client deserializer | `client/src/network/Deserializer.ts` | +| Client entity update | `client/src/entities/EntityManager.ts` (_updateEntity) | +| Transport | `server/src/networking/Connection.ts`, `client/.../NetworkManager.ts` | + +--- + +## 6. Quantization Constants (Suggested) + +```ts +// Position: 1/256 block = 0.0039 block precision +const POSITION_QUANT = 256; + +// Position range: ±32768 blocks (16-bit signed) +// Covers ~1km in each direction +const POSITION_MAX = 32767; +const POSITION_MIN = -32768; + +// Quaternion: 16-bit per component, range [-1, 1] → 1/32767 precision +const QUATERNION_QUANT = 32767; +``` + +--- + +## 7. Success Metrics + +| Metric | Current | Target (Phase 1) | Target (Phase 2) | +|--------|---------|------------------|------------------| +| Entity bytes/update (10 entities) | ~500–650 | ~300–400 | ~200–280 | +| Entity sync % of total packets | ~90% | ~70% | ~50% | +| Bandwidth (20 entities, 30 Hz) | ~15–20 KB/s | ~8–12 KB/s | ~5–8 KB/s | + +--- + +## 8. References + +- [Quaternion Compression (smallest three)](http://gafferongames.com/networked-physics/snapshot-compression/) +- [Minecraft entity sync (delta/quantization)](https://wiki.vg/Protocol#Entity_Metadata) +- Current codebase: `Entity.ts` (checkAndEmitUpdates), `NetworkSynchronizer.ts` (entity sync split), `Serializer.ts` (serializeVector/Quaternion) diff --git a/ai-memory/docs/perf-framework-research-2026-03-05/raw/GREEDY_MESHING_IMPLEMENTATION_GUIDE.md b/ai-memory/docs/perf-framework-research-2026-03-05/raw/GREEDY_MESHING_IMPLEMENTATION_GUIDE.md new file mode 100644 index 00000000..66642683 --- /dev/null +++ b/ai-memory/docs/perf-framework-research-2026-03-05/raw/GREEDY_MESHING_IMPLEMENTATION_GUIDE.md @@ -0,0 +1,182 @@ +# Greedy Meshing Implementation Guide + +**Purpose:** Step-by-step guide for implementing greedy quad merging (cubic/canonical meshing) in Hytopia’s ChunkWorker. +**Audience:** Engineers implementing Phase 4 (Greedy Meshing). +**Prerequisites:** Read [0fps Part 1](https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/) and [Part 2](https://0fps.net/2012/07/07/meshing-minecraft-part-2/). + +--- + +## 1. Algorithm Overview + +### 1.1 Input and Output + +- **Input:** Chunk of 16³ blocks. Each block has type ID, optional rotation. +- **Output:** Merged quads (position, size, normal, block type, AO, light). + +### 1.2 High-Level Steps + +1. **Group by (block type, normal, material flags).** Faces with same texture and normal are mergeable. +2. **For each direction** (±X, ±Y, ±Z): + - Build a 2D slice of visible faces (e.g. for +Y, iterate Y layers; for each layer, collect top faces). + - Run 2D greedy merge: combine adjacent same-type faces into rectangles. +3. **Emit merged quads** with correct UVs, AO, and lighting. + +--- + +## 2. Detailed Algorithm (0fps Style) + +### 2.1 Slice Extraction + +For direction `+Y` (top faces): + +- For each Y level `y = 0..15`: + - For each (x, z) in 16×16: + - If block at (x, y, z) is solid and block at (x, y+1, z) is air/transparent: + - Add face with normal (0, 1, 0), block type = block at (x, y, z). + - This gives a 16×16 grid of “face presence” per block type. + - Run 2D greedy merge on this grid. + +Repeat for −Y, ±X, ±Z. + +### 2.2 2D Greedy Merge (Per Slice, Per Block Type) + +``` +for each row j in slice: + for each column i in slice: + if visited[i,j]: continue + if no face at (i,j): continue + blockType = face at (i,j) + width = 1 + while i+width < 16 and same block at (i+width, j) and same AO/light: + width++ + height = 1 + while j+height < 16: + row OK = true + for k = 0 to width-1: + if different block or visited[i+k, j+height]: row OK = false; break + if !row OK: break + height++ + mark (i,j)..(i+width-1, j+height-1) as visited + emit quad: origin (i,j), size (width, height), blockType +``` + +### 2.3 Lexicographic Order (0fps) + +To get deterministic, visually stable meshes, merge in a fixed order (e.g. top-to-bottom, left-to-right) and prefer the lexicographically smallest representation when multiple merges are possible. + +--- + +## 3. Integration with ChunkWorker + +### 3.1 Current Flow (Simplified) + +``` +for each block in chunk: + for each face (6 directions): + if face visible (neighbor empty/transparent): + emit quad +``` + +### 3.2 New Flow + +``` +// Group 1: Opaque solid blocks (greedy) +for dir in [+X,-X,+Y,-Y,+Z,-Z]: + slice = extractVisibleFaces(chunk, dir) + for blockType in unique block types in slice: + subslice = slice filtered by blockType + quads = greedyMerge2D(subslice, dir) + emit quads with AO, light + +// Group 2: Transparent / special (per-face, existing logic) +for each block in chunk: + if block is transparent or special: + for each face: + if visible: emit quad +``` + +### 3.3 AO and Lighting + +- Ambient occlusion: compute per-vertex AO from neighbor blocks (as today). +- Light: sample from light volume (as today). +- For merged quads: corners may have different AO/light. Options: + - **Option A:** Use min AO/light of the merged region (slightly darker; simpler). + - **Option B:** Subdivide quad where AO/light changes (more quads, better quality). + - **Recommendation:** Start with Option A; optimize later. + +--- + +## 4. Data Structures + +### 4.1 Slice Representation + +```ts +// 16x16 grid, value = block type ID (0 = no face) +type Slice = Uint8Array; // 256 elements + +// Or: (blockTypeId, ao, light) per cell if we merge only when all match +interface SliceCell { + blockTypeId: number; + ao: number; + light: number; +} +``` + +### 4.2 Visited Mask + +```ts +// 16x16 boolean +const visited = new Uint8Array(256); // 1 bit per cell, or just 256 bytes +``` + +### 4.3 Merged Quad Output + +```ts +interface MergedQuad { + x: number; // local origin + y: number; + z: number; + width: number; // in blocks, along one horizontal axis + height: number; // in blocks, along other axis + normal: [number, number, number]; + blockTypeId: number; + ao: number; // or per-corner if subdividing + light: number; +} +``` + +--- + +## 5. Implementation Order + +| Step | Task | Est. Time | +|------|------|-----------| +| 1 | Slice extraction for +Y (top faces) | 1 day | +| 2 | 2D greedy merge for +Y slice | 1 day | +| 3 | Apply to all 6 directions | 0.5 day | +| 4 | AO/light handling for merged quads | 1 day | +| 5 | Integration: replace per-face loop for opaque solids | 1 day | +| 6 | Benchmark: vertex count and build time | 0.5 day | +| 7 | Edge cases: chunk boundaries, multi-type batches | 1 day | + +--- + +## 6. Expected Results + +| Terrain Type | Before (vertices) | After (est.) | Reduction | +|--------------|-------------------|--------------|-----------| +| Flat 16×16 | ~6000 | ~200 | ~30× | +| Hilly | ~8000 | ~800 | ~10× | +| Caves | ~4000 | ~600 | ~7× | +| Mixed | ~6000 | ~500 | ~12× | + +Build time may increase by 10–30% due to extra passes; vertex reduction should yield net FPS gain. + +--- + +## 7. References + +- [0fps Part 1 – Meshing in a Minecraft Game](https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/) +- [0fps Part 2 – Multiple block types](https://0fps.net/2012/07/07/meshing-minecraft-part-2/) +- [mikolalysenko/greedy-mesher](https://github.com/mikolalysenko/greedy-mesher) (JavaScript reference) +- [Vercidium greedy voxel meshing gist](https://gist.github.com/Vercidium/a3002bd083cce2bc854c9ff8f0118d33) diff --git a/ai-memory/docs/perf-framework-research-2026-03-05/raw/MAP_ENGINE_ARCHITECTURE (1).md b/ai-memory/docs/perf-framework-research-2026-03-05/raw/MAP_ENGINE_ARCHITECTURE (1).md new file mode 100644 index 00000000..f58f543a --- /dev/null +++ b/ai-memory/docs/perf-framework-research-2026-03-05/raw/MAP_ENGINE_ARCHITECTURE (1).md @@ -0,0 +1,272 @@ +# Hytopia Map Engine Architecture + +This document describes how the Hytopia map engine is set up, its data flow, and a roadmap for adapting it to support **binary maps** for extremely large worlds (e.g., 100k×100k×64 blocks). + +--- + +## 1. Architecture Overview + +The map engine spans **server** (authoritative block state), **client** (rendering, meshing), and **protocol** (network serialization). Maps are loaded once at world initialization and populate a chunk-based block lattice. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ MAP LOAD PIPELINE │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ JSON Map File World.loadMap() ChunkLattice │ +│ (blockTypes, blocks, ───────────────► initializeBlockEntries() │ +│ entities) │ │ │ +│ │ │ ▼ │ +│ │ │ ChunkLattice clears, │ +│ │ │ creates Chunks, │ +│ │ │ builds colliders │ +│ │ │ │ │ +│ │ ▼ ▼ │ +│ │ BlockTypeRegistry Map │ +│ │ (block types) (sparse chunks) │ +│ │ │ │ +│ │ ▼ │ +│ │ NetworkSynchronizer │ +│ │ (chunk sync to │ +│ │ clients) │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 2. WorldMap Interface (JSON Format) + +Maps conform to the `WorldMap` interface used by `World.loadMap()`: + +| Section | Purpose | Location | +|---------------|--------------------------------------------------------|-------------------------------| +| `blockTypes` | Block type definitions (id, name, textureUri, etc.) | `server/src/worlds/World.ts` | +| `blocks` | Block placements keyed by `"x,y,z"` string | `WorldMap.blocks` | +| `entities` | Entity spawns keyed by `"x,y,z"` position | `WorldMap.entities` | + +### Block Format in JSON + +Each block entry is either: + +- **Short form:** `"x,y,z": ` (e.g. `"-25,0,-16": 7`) +- **Extended form:** `"x,y,z": { "i": , "r": }` + +Coordinates are **world block coordinates** (integers). Block type IDs are 0–255 (0 = air, 1–255 = registered block types). + +### Size Implications of JSON Maps + +| Factor | Impact | +|---------------------------|-----------------------------------------------------------------------| +| Sparse object keys | Each block = `"x,y,z"` string key (10–20+ chars) + JSON overhead | +| No chunk-level batching | All blocks listed individually; no spatial grouping | +| Parsing cost | Full JSON parse loads entire map into memory before processing | +| File size | `boilerplate-small.json` ≈ 4,600+ lines; `big-world` ≈ 309,000+ lines | + +For a **100k×100k×64** fully dense map: + +- Blocks: 640 billion +- JSON would be impractically huge (hundreds of GB+ as text) +- Even sparse terrain would produce multi-GB JSON for large worlds + +--- + +## 3. Chunk Model + +### Chunk Dimensions + +| Constant | Value | Location | +|----------------|-------|--------------------------------------| +| `CHUNK_SIZE` | 16 | `server/src/worlds/blocks/Chunk.ts` | +| `CHUNK_VOLUME` | 4096 | 16³ blocks per chunk | +| `MAX_BLOCK_TYPE_ID` | 255 | `Chunk.ts` | + +Chunk origins are multiples of 16 on each axis (e.g. `(0,0,0)`, `(16,0,0)`, `(0,16,0)`). + +### Chunk Storage + +- **`Chunk._blocks`:** `Uint8Array(4096)` – block type ID per voxel +- **`Chunk._blockRotations`:** `Map` – sparse map of block index → rotation +- **Block index:** `x + (y << 4) + (z << 8)` (local coords 0–15) + +Chunks are stored in `ChunkLattice._chunks` as `Map` keyed by packed chunk origin: + +```typescript +// ChunkLattice._packCoordinate() – 54 bits per axis +chunkKey = (x << 108) | (y << 54) | z +``` + +--- + +## 4. Load Flow: `World.loadMap()` + +```typescript +// server/src/worlds/World.ts +public loadMap(map: WorldMap) { + this.chunkLattice.clear(); + + // 1. Register block types + if (map.blockTypes) { + for (const blockTypeData of map.blockTypes) { + this.blockTypeRegistry.registerGenericBlockType({ ... }); + } + } + + // 2. Iterate blocks as generator, feed to ChunkLattice + if (map.blocks) { + const blockEntries = function* () { + for (const key in mapBlocks) { + const blockValue = mapBlocks[key]; + const blockTypeId = typeof blockValue === 'number' ? blockValue : blockValue.i; + const blockRotationIndex = typeof blockValue === 'number' ? undefined : blockValue.r; + const [x, y, z] = key.split(',').map(Number); + yield { globalCoordinate: { x, y, z }, blockTypeId, blockRotation }; + } + }; + this.chunkLattice.initializeBlockEntries(blockEntries()); + } + + // 3. Spawn entities + if (map.entities) { ... } +} +``` + +### `ChunkLattice.initializeBlockEntries()` + +- Clears the lattice +- For each block: resolves chunk, creates chunk if needed, calls `chunk.setBlock()` +- Tracks block placements per type for colliders +- After all blocks: builds one collider per block type (voxel or trimesh) + +--- + +## 5. Client-Server Chunk Sync + +Chunks are serialized and sent to clients via `NetworkSynchronizer`: + +| Protocol Field | Description | +|----------------|--------------------------------------| +| `c` | Chunk origin `[x, y, z]` | +| `b` | Block IDs `Uint8Array \| number[]` (4096) | +| `r` | Rotations: flat `[blockIndex, rotIndex, ...]` | +| `rm` | Chunk removed flag | + +- **Serializer:** `Serializer.serializeChunk()` → `protocol.ChunkSchema` +- **Client:** `Deserializer.deserializeChunk()` → `DeserializedChunk` +- **ChunkWorker:** Receives `chunk_update`, registers chunk, builds meshes + +The client does **not** load the JSON map. It receives chunks from the server over the network after a player joins a world. + +--- + +## 6. Key Files Reference + +| Component | Path | +|----------------------|--------------------------------------------------| +| WorldMap interface | `server/src/worlds/World.ts` | +| loadMap | `server/src/worlds/World.ts` | +| ChunkLattice | `server/src/worlds/blocks/ChunkLattice.ts` | +| Chunk | `server/src/worlds/blocks/Chunk.ts` | +| ChunkSchema (proto) | `protocol/schemas/Chunk.ts` | +| Serializer | `server/src/networking/Serializer.ts` | +| ChunkWorker (client) | `client/src/workers/ChunkWorker.ts` | +| Deserializer | `client/src/network/Deserializer.ts` | + +--- + +## 7. Binary Map Adaptation Roadmap for 100k×100k×64 + +To support huge maps efficiently, the engine should move from JSON to **binary map sources** with **chunk-level loading** and **streaming**. + +### 7.1 Binary Chunk Format (Proposed) + +Store one file or region per chunk (or region of chunks): + +``` +chunk.{cx}.{cy}.{cz}.bin OR region.{rx}.{ry}.{rz}.bin +``` + +**Suggested layout per chunk (raw):** + +| Offset | Size | Content | +|--------|--------|------------------------------------------| +| 0 | 12 | Origin (3× int32: x, y, z) | +| 12 | 4096 | Block IDs (Uint8Array) | +| 4108 | var | Sparse rotations: count + [idx, rot]... | + +Or use a compact format (e.g. run-length encoding for air, or palette indices) for sparse chunks. + +### 7.2 Streaming / Lazy Loading + +- **Do not** load the entire map into memory. +- Use a **chunk provider** that: + - Accepts `(chunkOriginX, chunkOriginY, chunkOriginZ)` and returns chunk data + - Reads from binary files, memory-mapped files, or a database +- Replace the current `loadMap()` bulk load with: + - Initial load of a small seed area (e.g. spawn region) + - On-demand loading when `ChunkLattice.getOrCreateChunk()` needs a chunk not yet in memory + +### 7.3 Implementation Strategy + +1. **`MapProvider` interface** + ```typescript + interface MapProvider { + getChunk(origin: Vector3Like): ChunkData | null | Promise; + getBlockTypes(): BlockTypeOptions[]; + } + ``` + +2. **`BinaryMapProvider`** + - Reads `.bin` chunk files from disk or object storage + - Maps chunk origin → file path or byte range + - Returns `{ blocks: Uint8Array, rotations: Map }` + +3. **ChunkLattice changes** + - Replace `initializeBlockEntries()` full load with lazy `getOrCreateChunk()` that: + - Checks `_chunks` cache + - If miss: calls `MapProvider.getChunk()`, creates `Chunk`, inserts into `_chunks` + - Optionally preload chunks in a radius around player(s) + +4. **Block types** + - Keep block types in a small JSON or separate binary; they are tiny compared to block data. + - Load once at startup; no need to stream. + +### 7.4 Scale Estimates for 100k×100k×64 + +| Metric | Value | +|---------------------------|--------------------------| +| World dimensions | 100,000 × 100,000 × 64 | +| Chunks (16³) | 6,250 × 6,250 × 4 ≈ 156M chunks | +| Bytes per chunk (raw) | ~4.1 KB (blocks only) | +| Raw block data (if dense) | ~640 GB | +| Sparse (e.g. surface) | Much less; only store non-air chunks | + +Binary format advantages: + +- No JSON parsing; direct `Uint8Array` use +- Chunk-level I/O; load only what’s needed +- Possible memory-mapping for large files +- Optional compression (e.g. LZ4, Zstd) per chunk or region + +### 7.5 Migration Path + +1. **Phase 1:** Add `BinaryMapProvider` that reads chunk `.bin` files; `loadMap()` can accept `WorldMap | MapProvider`. +2. **Phase 2:** Make `ChunkLattice.getOrCreateChunk()` use the provider when a chunk is missing. +3. **Phase 3:** Add tooling to convert existing JSON maps → binary chunk files. +4. **Phase 4:** Optional region/compression format for production. + +--- + +## 8. Summary + +| Current (JSON) | Target (Binary + Streaming) | +|----------------------------|----------------------------------| +| Full map in memory | Chunk-level loading | +| Single large JSON parse | Small reads per chunk | +| Sparse object keys | Dense `Uint8Array` per chunk | +| Not viable for 100k³ scale | Designed for huge worlds | + +The existing `Chunk` and `ChunkLattice` design already matches a chunk-oriented model. The main changes are: + +1. Replace JSON as the map source with a binary chunk provider. +2. Add lazy loading so chunks are fetched on demand. +3. Provide conversion tools and a clear binary chunk layout. diff --git a/ai-memory/docs/perf-framework-research-2026-03-05/raw/MAP_ENGINE_ARCHITECTURE.md b/ai-memory/docs/perf-framework-research-2026-03-05/raw/MAP_ENGINE_ARCHITECTURE.md new file mode 100644 index 00000000..f58f543a --- /dev/null +++ b/ai-memory/docs/perf-framework-research-2026-03-05/raw/MAP_ENGINE_ARCHITECTURE.md @@ -0,0 +1,272 @@ +# Hytopia Map Engine Architecture + +This document describes how the Hytopia map engine is set up, its data flow, and a roadmap for adapting it to support **binary maps** for extremely large worlds (e.g., 100k×100k×64 blocks). + +--- + +## 1. Architecture Overview + +The map engine spans **server** (authoritative block state), **client** (rendering, meshing), and **protocol** (network serialization). Maps are loaded once at world initialization and populate a chunk-based block lattice. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ MAP LOAD PIPELINE │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ JSON Map File World.loadMap() ChunkLattice │ +│ (blockTypes, blocks, ───────────────► initializeBlockEntries() │ +│ entities) │ │ │ +│ │ │ ▼ │ +│ │ │ ChunkLattice clears, │ +│ │ │ creates Chunks, │ +│ │ │ builds colliders │ +│ │ │ │ │ +│ │ ▼ ▼ │ +│ │ BlockTypeRegistry Map │ +│ │ (block types) (sparse chunks) │ +│ │ │ │ +│ │ ▼ │ +│ │ NetworkSynchronizer │ +│ │ (chunk sync to │ +│ │ clients) │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 2. WorldMap Interface (JSON Format) + +Maps conform to the `WorldMap` interface used by `World.loadMap()`: + +| Section | Purpose | Location | +|---------------|--------------------------------------------------------|-------------------------------| +| `blockTypes` | Block type definitions (id, name, textureUri, etc.) | `server/src/worlds/World.ts` | +| `blocks` | Block placements keyed by `"x,y,z"` string | `WorldMap.blocks` | +| `entities` | Entity spawns keyed by `"x,y,z"` position | `WorldMap.entities` | + +### Block Format in JSON + +Each block entry is either: + +- **Short form:** `"x,y,z": ` (e.g. `"-25,0,-16": 7`) +- **Extended form:** `"x,y,z": { "i": , "r": }` + +Coordinates are **world block coordinates** (integers). Block type IDs are 0–255 (0 = air, 1–255 = registered block types). + +### Size Implications of JSON Maps + +| Factor | Impact | +|---------------------------|-----------------------------------------------------------------------| +| Sparse object keys | Each block = `"x,y,z"` string key (10–20+ chars) + JSON overhead | +| No chunk-level batching | All blocks listed individually; no spatial grouping | +| Parsing cost | Full JSON parse loads entire map into memory before processing | +| File size | `boilerplate-small.json` ≈ 4,600+ lines; `big-world` ≈ 309,000+ lines | + +For a **100k×100k×64** fully dense map: + +- Blocks: 640 billion +- JSON would be impractically huge (hundreds of GB+ as text) +- Even sparse terrain would produce multi-GB JSON for large worlds + +--- + +## 3. Chunk Model + +### Chunk Dimensions + +| Constant | Value | Location | +|----------------|-------|--------------------------------------| +| `CHUNK_SIZE` | 16 | `server/src/worlds/blocks/Chunk.ts` | +| `CHUNK_VOLUME` | 4096 | 16³ blocks per chunk | +| `MAX_BLOCK_TYPE_ID` | 255 | `Chunk.ts` | + +Chunk origins are multiples of 16 on each axis (e.g. `(0,0,0)`, `(16,0,0)`, `(0,16,0)`). + +### Chunk Storage + +- **`Chunk._blocks`:** `Uint8Array(4096)` – block type ID per voxel +- **`Chunk._blockRotations`:** `Map` – sparse map of block index → rotation +- **Block index:** `x + (y << 4) + (z << 8)` (local coords 0–15) + +Chunks are stored in `ChunkLattice._chunks` as `Map` keyed by packed chunk origin: + +```typescript +// ChunkLattice._packCoordinate() – 54 bits per axis +chunkKey = (x << 108) | (y << 54) | z +``` + +--- + +## 4. Load Flow: `World.loadMap()` + +```typescript +// server/src/worlds/World.ts +public loadMap(map: WorldMap) { + this.chunkLattice.clear(); + + // 1. Register block types + if (map.blockTypes) { + for (const blockTypeData of map.blockTypes) { + this.blockTypeRegistry.registerGenericBlockType({ ... }); + } + } + + // 2. Iterate blocks as generator, feed to ChunkLattice + if (map.blocks) { + const blockEntries = function* () { + for (const key in mapBlocks) { + const blockValue = mapBlocks[key]; + const blockTypeId = typeof blockValue === 'number' ? blockValue : blockValue.i; + const blockRotationIndex = typeof blockValue === 'number' ? undefined : blockValue.r; + const [x, y, z] = key.split(',').map(Number); + yield { globalCoordinate: { x, y, z }, blockTypeId, blockRotation }; + } + }; + this.chunkLattice.initializeBlockEntries(blockEntries()); + } + + // 3. Spawn entities + if (map.entities) { ... } +} +``` + +### `ChunkLattice.initializeBlockEntries()` + +- Clears the lattice +- For each block: resolves chunk, creates chunk if needed, calls `chunk.setBlock()` +- Tracks block placements per type for colliders +- After all blocks: builds one collider per block type (voxel or trimesh) + +--- + +## 5. Client-Server Chunk Sync + +Chunks are serialized and sent to clients via `NetworkSynchronizer`: + +| Protocol Field | Description | +|----------------|--------------------------------------| +| `c` | Chunk origin `[x, y, z]` | +| `b` | Block IDs `Uint8Array \| number[]` (4096) | +| `r` | Rotations: flat `[blockIndex, rotIndex, ...]` | +| `rm` | Chunk removed flag | + +- **Serializer:** `Serializer.serializeChunk()` → `protocol.ChunkSchema` +- **Client:** `Deserializer.deserializeChunk()` → `DeserializedChunk` +- **ChunkWorker:** Receives `chunk_update`, registers chunk, builds meshes + +The client does **not** load the JSON map. It receives chunks from the server over the network after a player joins a world. + +--- + +## 6. Key Files Reference + +| Component | Path | +|----------------------|--------------------------------------------------| +| WorldMap interface | `server/src/worlds/World.ts` | +| loadMap | `server/src/worlds/World.ts` | +| ChunkLattice | `server/src/worlds/blocks/ChunkLattice.ts` | +| Chunk | `server/src/worlds/blocks/Chunk.ts` | +| ChunkSchema (proto) | `protocol/schemas/Chunk.ts` | +| Serializer | `server/src/networking/Serializer.ts` | +| ChunkWorker (client) | `client/src/workers/ChunkWorker.ts` | +| Deserializer | `client/src/network/Deserializer.ts` | + +--- + +## 7. Binary Map Adaptation Roadmap for 100k×100k×64 + +To support huge maps efficiently, the engine should move from JSON to **binary map sources** with **chunk-level loading** and **streaming**. + +### 7.1 Binary Chunk Format (Proposed) + +Store one file or region per chunk (or region of chunks): + +``` +chunk.{cx}.{cy}.{cz}.bin OR region.{rx}.{ry}.{rz}.bin +``` + +**Suggested layout per chunk (raw):** + +| Offset | Size | Content | +|--------|--------|------------------------------------------| +| 0 | 12 | Origin (3× int32: x, y, z) | +| 12 | 4096 | Block IDs (Uint8Array) | +| 4108 | var | Sparse rotations: count + [idx, rot]... | + +Or use a compact format (e.g. run-length encoding for air, or palette indices) for sparse chunks. + +### 7.2 Streaming / Lazy Loading + +- **Do not** load the entire map into memory. +- Use a **chunk provider** that: + - Accepts `(chunkOriginX, chunkOriginY, chunkOriginZ)` and returns chunk data + - Reads from binary files, memory-mapped files, or a database +- Replace the current `loadMap()` bulk load with: + - Initial load of a small seed area (e.g. spawn region) + - On-demand loading when `ChunkLattice.getOrCreateChunk()` needs a chunk not yet in memory + +### 7.3 Implementation Strategy + +1. **`MapProvider` interface** + ```typescript + interface MapProvider { + getChunk(origin: Vector3Like): ChunkData | null | Promise; + getBlockTypes(): BlockTypeOptions[]; + } + ``` + +2. **`BinaryMapProvider`** + - Reads `.bin` chunk files from disk or object storage + - Maps chunk origin → file path or byte range + - Returns `{ blocks: Uint8Array, rotations: Map }` + +3. **ChunkLattice changes** + - Replace `initializeBlockEntries()` full load with lazy `getOrCreateChunk()` that: + - Checks `_chunks` cache + - If miss: calls `MapProvider.getChunk()`, creates `Chunk`, inserts into `_chunks` + - Optionally preload chunks in a radius around player(s) + +4. **Block types** + - Keep block types in a small JSON or separate binary; they are tiny compared to block data. + - Load once at startup; no need to stream. + +### 7.4 Scale Estimates for 100k×100k×64 + +| Metric | Value | +|---------------------------|--------------------------| +| World dimensions | 100,000 × 100,000 × 64 | +| Chunks (16³) | 6,250 × 6,250 × 4 ≈ 156M chunks | +| Bytes per chunk (raw) | ~4.1 KB (blocks only) | +| Raw block data (if dense) | ~640 GB | +| Sparse (e.g. surface) | Much less; only store non-air chunks | + +Binary format advantages: + +- No JSON parsing; direct `Uint8Array` use +- Chunk-level I/O; load only what’s needed +- Possible memory-mapping for large files +- Optional compression (e.g. LZ4, Zstd) per chunk or region + +### 7.5 Migration Path + +1. **Phase 1:** Add `BinaryMapProvider` that reads chunk `.bin` files; `loadMap()` can accept `WorldMap | MapProvider`. +2. **Phase 2:** Make `ChunkLattice.getOrCreateChunk()` use the provider when a chunk is missing. +3. **Phase 3:** Add tooling to convert existing JSON maps → binary chunk files. +4. **Phase 4:** Optional region/compression format for production. + +--- + +## 8. Summary + +| Current (JSON) | Target (Binary + Streaming) | +|----------------------------|----------------------------------| +| Full map in memory | Chunk-level loading | +| Single large JSON parse | Small reads per chunk | +| Sparse object keys | Dense `Uint8Array` per chunk | +| Not viable for 100k³ scale | Designed for huge worlds | + +The existing `Chunk` and `ChunkLattice` design already matches a chunk-oriented model. The main changes are: + +1. Replace JSON as the map source with a binary chunk provider. +2. Add lazy loading so chunks are fetched on demand. +3. Provide conversion tools and a clear binary chunk layout. diff --git a/ai-memory/docs/perf-framework-research-2026-03-05/raw/MINECRAFT_ARCHITECTURE_RESEARCH.md b/ai-memory/docs/perf-framework-research-2026-03-05/raw/MINECRAFT_ARCHITECTURE_RESEARCH.md new file mode 100644 index 00000000..c7280c72 --- /dev/null +++ b/ai-memory/docs/perf-framework-research-2026-03-05/raw/MINECRAFT_ARCHITECTURE_RESEARCH.md @@ -0,0 +1,161 @@ +# Minecraft Architecture Research + +**Purpose:** Inform Hytopia’s voxel engine design with lessons from Minecraft Java and Bedrock. +**Audience:** Engineers implementing chunk loading, colliders, and meshing. +**Sources:** Technical wikis, decompilations, community analysis, engine talks. + +--- + +## 1. Chunk System Overview + +### 1.1 Chunk Structure + +| Version | Chunk Size | Subchunk | Notes | +|---------|------------|----------|-------| +| Java | 16×256×16 (XZ columns) | 16×16×16 sections | Vertical column; sections loaded independently | +| Bedrock | 16×256×16 | 16×16×16 | Similar; different storage layout | + +**Hytopia:** 16×16×16 chunks, 2×2×2 batches (32³). Aligns with common practice. + +### 1.2 Loading States (Java 1.14+) + +Minecraft separates chunk lifecycle into distinct states: + +| State | Purpose | +|-------|---------| +| **Empty** | Not loaded | +| **Structure** | Structures placed | +| **Noise** | Terrain generated | +| **Surface** | Surface blocks, biomes | +| **Carvers** | Caves, ravines | +| **Features** | Trees, ores, etc. | +| **Entity ticking** | Physics, entities, block updates | + +**Key insight:** Entity ticking requires a 5×5 grid of loaded chunks around the center chunk. Border chunks can be “lazy” (block updates only, no entities). This **spatial locality** keeps entity/physics work bounded. + +**Hytopia takeaway:** Only tick entities and step physics for chunks near players. Don’t pay for distant chunks. + +### 1.3 Spawn Chunks + +- 19×19 chunks (Java) or 23×23 (Bedrock) always loaded around spawn. +- Only center ~12×12 process entities. +- Reduces load/unload churn at spawn. + +**Hytopia:** Preload radius already exists; consider an “always loaded” spawn core for hubs. + +--- + +## 2. File I/O and Region Format + +### 2.1 Region Files + +- One file per 32×32 chunk region (XZ). +- Anvil format: 4 KB header (1024 entries × 4 bytes) + chunk payloads. +- Chunks stored with length prefix + compression (typically zlib; Bedrock uses different schemes). +- **Async I/O:** Modern implementations use background threads; main thread never blocks on disk. + +### 2.2 Chunk Serialization + +- Block IDs, block states, light, heightmap, biomes stored per chunk. +- Compression reduces size by ~90% for typical terrain. + +**Hytopia:** Region format exists; `readChunkAsync` and `writeChunk` (sync) are in place. Priority: make persist async. + +--- + +## 3. Terrain Generation + +### 3.1 Worker Pool + +- Terrain generation runs in worker threads. +- Main thread requests chunk; worker generates; result returned asynchronously. +- Multiple workers allow parallelism. + +### 3.2 Generation Stages + +- Noise → carvers → features (trees, ores). +- Each stage can be parallelized or deferred. + +**Hytopia:** `TerrainWorkerPool` + `generateChunkAsync` exist. Ensure `requestChunk` uses this path and doesn’t fall back to sync. + +--- + +## 4. Physics and Collision + +### 4.1 Chunk-Section Colliders + +- Collision is built per 16×16×16 section. +- Sections far from players may not have colliders at all, or use simplified shapes. +- Colliders are created/updated in batches, not all at once. + +### 4.2 Spatial Partitioning + +- Physics world uses spatial partitioning (e.g. broadphase). +- Entity vs. block collision: only check nearby chunks. +- No global scan over entire world. + +**Hytopia gap:** `_combineVoxelStates` iterates all chunks of a block type. Must restrict to nearby chunks. + +--- + +## 5. Meshing and Rendering + +### 5.1 Greedy Meshing (Ambient Occlusion) + +- Minecraft uses an approximation of greedy meshing (block model merging). +- Adjacent faces of same block type are merged into larger quads where possible. +- Results in 2–64× fewer quads than per-face rendering. + +### 5.2 Occlusion Culling + +- Section-level visibility: if a section is fully behind solid terrain, skip rendering. +- BFS from camera through air/transparent blocks; mark visible sections. +- ~10–15% frame time savings in cave-heavy areas. + +### 5.3 LOD + +- Distant chunks use lower-detail meshes or impostors. +- Reduces overdraw and vertex count. + +**Hytopia:** Face culling ✅; greedy meshing ❌; occlusion partial; LOD step 2/4. Biggest win: greedy meshing. + +--- + +## 6. Network + +### 6.1 Chunk Packets + +- Chunks sent incrementally; rate-limited to avoid client flood. +- Delta updates for modified chunks (block changes) vs. full chunk for new loads. + +### 6.2 Entity Sync + +- Position/rotation use compact encodings (fixed-point or quantized). +- Entities use delta or relative positioning where possible. +- Distant entities may sync at lower rate. + +**Source:** [Minecraft Protocol (wiki.vg)](https://wiki.vg/Protocol#Entity_Metadata) + +--- + +## 7. Lessons for Hytopia + +| Minecraft Pattern | Hytopia Status | Action | +|-------------------|----------------|--------| +| Async chunk load | ✅ `requestChunk` + `getChunkAsync` | Verify usage | +| Async I/O | ✅ `readChunkAsync` | Make persist async | +| Worker terrain gen | ✅ TerrainWorkerPool | Verify | +| Collider locality | ❌ O(world) scans | Phase 1: spatial index, scoped merge | +| Greedy meshing | ❌ | Phase 4 | +| Occlusion | ⚠️ Partial | Phase 5 | +| Entity quantization | ❌ | Phase 3 | +| Distance-based sync | ❌ | Phase 3 | + +--- + +## References + +- [Chunk Loading – Technical Minecraft Wiki](https://techmcdocs.github.io/pages/GameMechanics/ChunkLoading/) +- [Minecraft Protocol – wiki.vg](https://wiki.vg/Protocol) +- [0fps Meshing in a Minecraft Game](https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/) +- [Fabric Modding Documentation (chunk loading states)](https://fabricmc.net/wiki/) diff --git a/ai-memory/docs/perf-framework-research-2026-03-05/raw/NETWORK_PROTOCOL_2026_RESEARCH.md b/ai-memory/docs/perf-framework-research-2026-03-05/raw/NETWORK_PROTOCOL_2026_RESEARCH.md new file mode 100644 index 00000000..722d42a8 --- /dev/null +++ b/ai-memory/docs/perf-framework-research-2026-03-05/raw/NETWORK_PROTOCOL_2026_RESEARCH.md @@ -0,0 +1,130 @@ +# Network Protocol 2026 Research + +**Purpose:** Modern entity sync and chunk sync patterns for low-bandwidth, low-latency voxel multiplayer. +**Audience:** Engineers implementing Phase 3 (Entity Sync Compression). + +--- + +## 1. Entity Sync: Industry Patterns + +### 1.1 Minecraft (Java) + +- Entity position/rotation sent as fixed-point or scaled integers. +- Metadata uses compact type tags. +- Delta updates for moving entities; full state on spawn or major change. + +### 1.2 Source Engine / Garry’s Mod + +- **Delta compression:** Send only changed fields; baseline is last full update. +- **Quantization:** Position in 1/16 or 1/32 unit; angles in 16-bit. + +### 1.3 Overwatch / Modern FPS + +- Client-side prediction + server reconciliation. +- Entity updates at 20–60 Hz for nearby; lower for distant. +- Snapshot compression: delta from previous snapshot. + +### 1.4 Gaffer On Games (Networked Physics) + +- [Snapshot Compression](http://gafferongames.com/networked-physics/snapshot-compression/) +- Quaternion: store 3 largest components (smallest-three); 4th derived. +- Position: fixed-point or quantized. +- Delta encoding: send difference from last acked state. + +--- + +## 2. Quantization Formulas + +### 2.1 Position (Fixed-Point) + +```ts +const QUANT = 256; // 1/256 block = 0.0039 block precision +const clamp = (v: number) => Math.max(-32768, Math.min(32767, Math.round(v * QUANT))); + +// Encode +pq: [clamp(x), clamp(y), clamp(z)] // Int16Array or [number, number, number] + +// Decode +x = pq[0] / QUANT; +``` + +**Range:** ±32768 blocks ≈ ±524 km. More than enough. + +### 2.2 Quaternion (Smallest-Three) + +- Unit quaternion: `q.x² + q.y² + q.z² + q.w² = 1`. +- One component can be derived from the other three. +- Store the 3 components with largest magnitude; 1 byte for index of omitted component. +- Quantize each stored component to 16-bit: `value * 32767` for range [-1, 1]. + +**Size:** 1 + 3×2 = 7 bytes vs 4×4 = 16 bytes (float32). ~56% smaller. + +**Reference:** [Gaffer On Games](http://gafferongames.com/networked-physics/snapshot-compression/) + +### 2.3 Yaw-Only (Euler) + +- For entities that only rotate around Y: send 1 float (radians) or 16-bit quantized. +- `yaw = 2*PI * (int16 / 65536)`. +- 2 bytes vs 16 bytes for full quaternion. + +--- + +## 3. Distance-Based Sync Rate + +| Distance Band | Sync Rate | Use Case | +|---------------|-----------|----------| +| 0–4 chunks | 30 Hz | Player, nearby NPCs | +| 4–8 chunks | 15 Hz | Mid-range entities | +| 8+ chunks | 5 Hz | Far entities, environmental | + +**Implementation:** In `checkAndEmitUpdates` or NetworkSynchronizer, compute distance from nearest player; only emit if `tick % rateDivisor === 0`. + +--- + +## 4. Bulk Format (Structure of Arrays) + +Instead of: + +```json +[ + { "i": 1, "p": [10.5, 20.1, 30.2] }, + { "i": 2, "p": [11.2, 20.0, 31.1] } +] +``` + +Use: + +```json +{ + "ids": [1, 2], + "p": [[2693, 5146, 7733], [2867, 5120, 7962]] +} +``` + +- Quantized positions in `p` (Int16). +- Avoids repeating keys; msgpack benefits from smaller maps. +- **Caveat:** New packet type; client must support. Can run parallel to existing EntitiesPacket during migration. + +--- + +## 5. Protocol Versioning + +- Add optional fields to EntitySchema: `pq`, `rq`, `ry`. +- Old clients ignore unknown fields; new clients prefer them. +- Server flag: `useQuantizedEntitySync=true` (default for new connections after version bump). + +--- + +## 6. Chunk Delta Updates (Phase 6) + +- When a single block changes, send delta: `{ chunkId, blockIndex, blockTypeId }` instead of full chunk. +- Client applies delta to local chunk; requests full chunk if out of sync. +- Reduces bandwidth for frequent block edits (mining, building). + +--- + +## 7. References + +- [Gaffer On Games – Snapshot Compression](http://gafferongames.com/networked-physics/snapshot-compression/) +- [Minecraft Protocol – wiki.vg](https://wiki.vg/Protocol) +- [ENTITY_SYNC_DELTA_COMPRESSION_DESIGN.md](../ENTITY_SYNC_DELTA_COMPRESSION_DESIGN.md) – Hytopia-specific design diff --git a/ai-memory/docs/perf-framework-research-2026-03-05/raw/SMOOTH_WORLD_STREAMING_REFACTOR_PLAN (1).md b/ai-memory/docs/perf-framework-research-2026-03-05/raw/SMOOTH_WORLD_STREAMING_REFACTOR_PLAN (1).md new file mode 100644 index 00000000..cead4812 --- /dev/null +++ b/ai-memory/docs/perf-framework-research-2026-03-05/raw/SMOOTH_WORLD_STREAMING_REFACTOR_PLAN (1).md @@ -0,0 +1,180 @@ +# Smooth World Streaming Refactor Plan + +> **Canonical roadmap:** See [VOXEL_ENGINE_2026_MASTER_PLAN.md](./VOXEL_ENGINE_2026_MASTER_PLAN.md) for the full executive plan and phased roadmap. This document provides additional context and cross-references. + +**Goal:** Peak performance for the procedurally generated world—smooth streaming, no lag spikes, Minecraft/Hytale/bloxd-level polish. + +**Sources:** Codebase analysis, [VOXEL_PERFORMANCE_MASTER_PLAN.md](./VOXEL_PERFORMANCE_MASTER_PLAN.md), [CHUNK_LOADING_ARCHITECTURE.md](./CHUNK_LOADING_ARCHITECTURE.md), [VOXEL_RENDERING_RESEARCH.md](./VOXEL_RENDERING_RESEARCH.md), [PR #21](https://github.com/hytopiagg/hytopia-source/pull/21), and industry patterns from Minecraft, Hytale, and Bloxd. + +--- + +## 1. Competitive Analysis: Minecraft vs Hytale vs Bloxd vs Hytopia + +| Aspect | Minecraft | Hytale | Bloxd | Hytopia (Current) | +|--------|-----------|--------|-------|-------------------| +| **Chunk load** | Worker threads, async | Worker pool | JS async | ✅ `requestChunk` + `getChunkAsync` (TerrainWorkerPool) | +| **File I/O** | Async | Async | N/A (streaming) | ✅ `readChunkAsync` (PersistenceChunkProvider) | +| **Terrain gen** | Worker threads | Worker pool | — | ✅ `generateChunkAsync` (TerrainWorkerPool) | +| **Physics colliders** | Deferred, O(chunk) | Batched, spatial | Custom voxel | ❌ Sync, O(world) via `_combineVoxelStates` | +| **Collider locality** | Per-chunk, near player | Spatial culling | — | ⚠️ Partial (COLLIDER_MAX_CHUNK_DISTANCE=3) | +| **Greedy meshing** | ✅ | ✅ (mesh culling) | ✅ | ❌ 1 quad/face, ~64× extra geometry | +| **Chunk send rate** | Incremental, rate-limited | Batched | Streaming | ⚠️ MAX_CHUNKS_PER_SYNC=8, can burst | +| **Entity sync** | Delta / compressed | — | — | Full pos/rot 30 Hz, 90%+ of packets | +| **LOD** | ✅ | Variable chunk sizes | — | ✅ (step 2/4) | +| **Occlusion** | Cave culling | Partial | — | ⚠️ Only when over face limit | +| **Vertex pooling** | — | — | ✅ | ⚠️ Partial (size-match reuse) | +| **Map compression** | Region format | — | — | ❌ JSON maps large; PR #21 adds compression | + +**Gap summary:** Hytopia’s biggest gaps are (1) collider work O(world) and sync, (2) no greedy meshing, (3) entity sync volume, (4) JSON map size for non-procedural games. Procedural world already uses async load + worker terrain gen; collider and client-side mesh work are the main bottlenecks. + +--- + +## 2. PR #21 Relevance to Procedural World + +[PR #21: Compressed world maps](https://github.com/hytopiagg/hytopia-source/pull/21) targets **JSON maps** (`loadMap(map.json)`), not procedural/region worlds. It adds: + +| Feature | Applies to Procedural? | Notes | +|---------|------------------------|-------| +| `map.compressed.json` | ❌ | JSON map format only | +| `map.chunks.bin` (chunk cache) | ❌ | Prebaked JSON map chunks | +| Chunk cache collider build | ⚠️ Partially | “perf: speed up chunk cache collider build” can inform collider design | +| Brotli compression | ❌ | For map JSON, not region .bin | +| Auto-detect / `hytopia map-compress` | ❌ | JSON map workflow | + +**Recommendation:** Merge PR #21 for JSON-map games (huntcraft, boilerplate, etc.). For procedural world, reuse the collider build approach where relevant. Procedural persistence uses region `.bin`; consider Brotli for region payloads later. + +--- + +## 3. Root Cause Summary + +When a player joins and blocks have physics: + +1. **Physics step (60 Hz):** Rapier steps the entire world, including all block colliders + player rigid body. +2. **Collider creation:** `_addChunkBlocksToColliders` → `_combineVoxelStates` scans all chunks of each block type (O(world)). +3. **Entity sync (30 Hz):** Full position/rotation for entities/players every 2 ticks; dominates packet volume. +4. **Chunk sync:** Up to 8 chunks per sync; client mesh build can spike main thread. +5. **Client mesh:** No greedy meshing → 2–64× more vertices than needed. +6. **ADD_CHUNK events:** Environmental entity spawn per chunk runs synchronously. + +--- + +## 4. Refactoring Plan (Prioritized) + +### Phase 1: Stop the Bleeding (1–2 weeks) + +| # | Task | Impact | Effort | Files | +|---|------|--------|--------|-------| +| 1.1 | **Collider locality – spatial index** | High | 3–5 days | `ChunkLattice.ts` | +| 1.2 | **Scoped `_combineVoxelStates`** | High | 2–3 days | `ChunkLattice.ts` | +| 1.3 | **Time-budget collider processing** | Medium | ✅ Done | `playground.ts` | +| 1.4 | **CHUNKS_PER_TICK = 3** | ✅ Done | — | `playground.ts` | +| 1.5 | **Defer environmental entity spawn** | Medium | 1 day | `playground.ts` | + +**1.1–1.2:** Replace global scans with spatial indexing. `_getBlockTypePlacements` and `_combineVoxelStates` should only consider chunks within a radius (e.g. 4–5 chunks) of any player. Add a spatial index (e.g. chunk key → block placements) and only merge voxel state for nearby chunks. + +### Phase 2: Main Thread Freedom (2–3 weeks) + +| # | Task | Impact | Effort | Files | +|---|------|--------|--------|-------| +| 2.1 | **Async persistChunk** | Medium | 1–2 days | `PersistenceChunkProvider.ts`, `RegionFileFormat.ts` | +| 2.2 | **Worker terrain gen verification** | — | 0.5 day | `TerrainWorkerPool.ts`, `ProceduralChunkProvider.ts` | +| 2.3 | **Incremental voxel collider updates** | High | 3–5 days | `ChunkLattice.ts` | +| 2.4 | **Chunk send pacing** | Medium | 1–2 days | `NetworkSynchronizer.ts` | + +**2.1:** `persistChunk` currently calls `writeChunk` (sync). Move to async; queue writes and process in background. + +**2.3:** Add blocks to voxel colliders in batches (e.g. 256–512/tick) instead of full chunk. Use Rapier voxel API if it supports incremental updates. + +### Phase 3: Network & Sync (2–3 weeks) + +| # | Task | Impact | Effort | Files | +|---|------|--------|--------|-------| +| 3.1 | **Entity delta/compression** | High | 5–7 days | `NetworkSynchronizer.ts`, `Serializer.ts`, protocol | +| 3.2 | **Chunk delta updates** | Medium | 3–4 days | `NetworkSynchronizer.ts`, `ChunkLattice` | +| 3.3 | **Predictive chunk preload** | Medium | 2–3 days | `playground.ts` | + +**3.1:** Send position/rotation deltas or use quantized floats. Reference: Minecraft’s entity compression, Hytale’s QUIC usage. + +### Phase 4: Client Render Pipeline (3–4 weeks) + +| # | Task | Impact | Effort | Files | +|---|------|--------|--------|-------| +| 4.1 | **Greedy meshing (quad merging)** | Very high | 5–7 days | `ChunkWorker.ts` | +| 4.2 | **Vertex pooling** | Medium | 2–3 days | `ChunkMeshManager.ts`, `ChunkWorker.ts` | +| 4.3 | **Occlusion culling always-on** | Medium | 2–3 days | `ChunkManager.ts`, `Renderer.ts` | +| 4.4 | **Mesh apply budget** | Low | 1 day | `ChunkManager.ts` | + +**4.1:** Implement 0fps-style greedy meshing for opaque solids. Merge adjacent same-type faces; expect 2–64× fewer vertices. References: [0fps](https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/), [mikolalysenko/greedy-mesher](https://github.com/mikolalysenko/greedy-mesher). + +### Phase 5: Long-Term & Polish (ongoing) + +| # | Task | Impact | Effort | +|---|------|--------|--------| +| 5.1 | LOD impostors for distant chunks | Medium | 2–3 weeks | +| 5.2 | Brotli for region .bin payloads | Low | 1 week | +| 5.3 | Block/face limits (safety cap) | Low | <1 day | +| 5.4 | Profiling hooks (tick, chunk, mesh) | Low | 2–3 days | + +--- + +## 5. Implementation Order + +``` +Week 1–2: Phase 1 (collider locality, scoped _combineVoxelStates, defer env spawn) +Week 3–4: Phase 2 (async persistChunk, incremental voxel, chunk send pacing) +Week 5–6: Phase 3 (entity delta, chunk delta, predictive preload) +Week 7–10: Phase 4 (greedy meshing, vertex pooling, occlusion) +Ongoing: Phase 5 +``` + +--- + +## 6. Success Metrics + +| Metric | Current (Est.) | Target | +|--------|----------------|--------| +| Lag spikes when walking | Every ~5 steps | None within preload radius | +| Server tick time (p99) | 50–200 ms | < 16 ms | +| Chunk load (blocking) | 20–100 ms | < 5 ms (async) | +| Vertices per flat chunk | ~6000 | ~200–500 (greedy) | +| Client frame time | Spikes on new chunks | Stable ~16 ms (60 fps) | +| Entity packet share | ~90% | < 50% (delta/compression) | + +--- + +## 7. Key Files Reference + +| Component | Path | +|-----------|------| +| Chunk load loop | `server/src/playground.ts` | +| Collider processing | `server/src/worlds/blocks/ChunkLattice.ts` | +| Physics simulation | `server/src/worlds/physics/Simulation.ts` | +| Mesh generation | `client/src/workers/ChunkWorker.ts` | +| Chunk sync | `server/src/networking/NetworkSynchronizer.ts` | +| Region I/O | `server/src/worlds/maps/RegionFileFormat.ts` | +| Terrain gen | `server/src/worlds/maps/TerrainGenerator.ts`, `TerrainWorkerPool.ts` | +| Procedural provider | `server/src/worlds/maps/ProceduralChunkProvider.ts` | +| Persistence provider | `server/src/worlds/maps/PersistenceChunkProvider.ts` | +| World loop | `server/src/worlds/WorldLoop.ts` | + +--- + +## 8. PR #21 Action Items + +1. **Merge PR #21** for JSON-map games (boilerplate, huntcraft, etc.). +2. **Reuse chunk cache collider patterns** in `ChunkLattice` if applicable. +3. **Later:** Consider Brotli for region payloads or a similar compression layer. + +--- + +## 9. References + +- [VOXEL_PERFORMANCE_MASTER_PLAN.md](./VOXEL_PERFORMANCE_MASTER_PLAN.md) +- [CHUNK_LOADING_ARCHITECTURE.md](./CHUNK_LOADING_ARCHITECTURE.md) +- [VOXEL_RENDERING_RESEARCH.md](./VOXEL_RENDERING_RESEARCH.md) +- [OPTIMIZATION_STRATEGY.md](./OPTIMIZATION_STRATEGY.md) +- [PR #21 – Compressed world maps](https://github.com/hytopiagg/hytopia-source/pull/21) +- [0fps Greedy Meshing](https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/) +- [mikolalysenko/greedy-mesher](https://github.com/mikolalysenko/greedy-mesher) +- [Minecraft Chunk Loading (Technical Wiki)](https://techmcdocs.github.io/pages/GameMechanics/ChunkLoading/) +- [Hytale Engine Technical Deep Dive](https://hytalecharts.com/news/hytale-engine-technical-deep-dive) diff --git a/ai-memory/docs/perf-framework-research-2026-03-05/raw/SMOOTH_WORLD_STREAMING_REFACTOR_PLAN.md b/ai-memory/docs/perf-framework-research-2026-03-05/raw/SMOOTH_WORLD_STREAMING_REFACTOR_PLAN.md new file mode 100644 index 00000000..cead4812 --- /dev/null +++ b/ai-memory/docs/perf-framework-research-2026-03-05/raw/SMOOTH_WORLD_STREAMING_REFACTOR_PLAN.md @@ -0,0 +1,180 @@ +# Smooth World Streaming Refactor Plan + +> **Canonical roadmap:** See [VOXEL_ENGINE_2026_MASTER_PLAN.md](./VOXEL_ENGINE_2026_MASTER_PLAN.md) for the full executive plan and phased roadmap. This document provides additional context and cross-references. + +**Goal:** Peak performance for the procedurally generated world—smooth streaming, no lag spikes, Minecraft/Hytale/bloxd-level polish. + +**Sources:** Codebase analysis, [VOXEL_PERFORMANCE_MASTER_PLAN.md](./VOXEL_PERFORMANCE_MASTER_PLAN.md), [CHUNK_LOADING_ARCHITECTURE.md](./CHUNK_LOADING_ARCHITECTURE.md), [VOXEL_RENDERING_RESEARCH.md](./VOXEL_RENDERING_RESEARCH.md), [PR #21](https://github.com/hytopiagg/hytopia-source/pull/21), and industry patterns from Minecraft, Hytale, and Bloxd. + +--- + +## 1. Competitive Analysis: Minecraft vs Hytale vs Bloxd vs Hytopia + +| Aspect | Minecraft | Hytale | Bloxd | Hytopia (Current) | +|--------|-----------|--------|-------|-------------------| +| **Chunk load** | Worker threads, async | Worker pool | JS async | ✅ `requestChunk` + `getChunkAsync` (TerrainWorkerPool) | +| **File I/O** | Async | Async | N/A (streaming) | ✅ `readChunkAsync` (PersistenceChunkProvider) | +| **Terrain gen** | Worker threads | Worker pool | — | ✅ `generateChunkAsync` (TerrainWorkerPool) | +| **Physics colliders** | Deferred, O(chunk) | Batched, spatial | Custom voxel | ❌ Sync, O(world) via `_combineVoxelStates` | +| **Collider locality** | Per-chunk, near player | Spatial culling | — | ⚠️ Partial (COLLIDER_MAX_CHUNK_DISTANCE=3) | +| **Greedy meshing** | ✅ | ✅ (mesh culling) | ✅ | ❌ 1 quad/face, ~64× extra geometry | +| **Chunk send rate** | Incremental, rate-limited | Batched | Streaming | ⚠️ MAX_CHUNKS_PER_SYNC=8, can burst | +| **Entity sync** | Delta / compressed | — | — | Full pos/rot 30 Hz, 90%+ of packets | +| **LOD** | ✅ | Variable chunk sizes | — | ✅ (step 2/4) | +| **Occlusion** | Cave culling | Partial | — | ⚠️ Only when over face limit | +| **Vertex pooling** | — | — | ✅ | ⚠️ Partial (size-match reuse) | +| **Map compression** | Region format | — | — | ❌ JSON maps large; PR #21 adds compression | + +**Gap summary:** Hytopia’s biggest gaps are (1) collider work O(world) and sync, (2) no greedy meshing, (3) entity sync volume, (4) JSON map size for non-procedural games. Procedural world already uses async load + worker terrain gen; collider and client-side mesh work are the main bottlenecks. + +--- + +## 2. PR #21 Relevance to Procedural World + +[PR #21: Compressed world maps](https://github.com/hytopiagg/hytopia-source/pull/21) targets **JSON maps** (`loadMap(map.json)`), not procedural/region worlds. It adds: + +| Feature | Applies to Procedural? | Notes | +|---------|------------------------|-------| +| `map.compressed.json` | ❌ | JSON map format only | +| `map.chunks.bin` (chunk cache) | ❌ | Prebaked JSON map chunks | +| Chunk cache collider build | ⚠️ Partially | “perf: speed up chunk cache collider build” can inform collider design | +| Brotli compression | ❌ | For map JSON, not region .bin | +| Auto-detect / `hytopia map-compress` | ❌ | JSON map workflow | + +**Recommendation:** Merge PR #21 for JSON-map games (huntcraft, boilerplate, etc.). For procedural world, reuse the collider build approach where relevant. Procedural persistence uses region `.bin`; consider Brotli for region payloads later. + +--- + +## 3. Root Cause Summary + +When a player joins and blocks have physics: + +1. **Physics step (60 Hz):** Rapier steps the entire world, including all block colliders + player rigid body. +2. **Collider creation:** `_addChunkBlocksToColliders` → `_combineVoxelStates` scans all chunks of each block type (O(world)). +3. **Entity sync (30 Hz):** Full position/rotation for entities/players every 2 ticks; dominates packet volume. +4. **Chunk sync:** Up to 8 chunks per sync; client mesh build can spike main thread. +5. **Client mesh:** No greedy meshing → 2–64× more vertices than needed. +6. **ADD_CHUNK events:** Environmental entity spawn per chunk runs synchronously. + +--- + +## 4. Refactoring Plan (Prioritized) + +### Phase 1: Stop the Bleeding (1–2 weeks) + +| # | Task | Impact | Effort | Files | +|---|------|--------|--------|-------| +| 1.1 | **Collider locality – spatial index** | High | 3–5 days | `ChunkLattice.ts` | +| 1.2 | **Scoped `_combineVoxelStates`** | High | 2–3 days | `ChunkLattice.ts` | +| 1.3 | **Time-budget collider processing** | Medium | ✅ Done | `playground.ts` | +| 1.4 | **CHUNKS_PER_TICK = 3** | ✅ Done | — | `playground.ts` | +| 1.5 | **Defer environmental entity spawn** | Medium | 1 day | `playground.ts` | + +**1.1–1.2:** Replace global scans with spatial indexing. `_getBlockTypePlacements` and `_combineVoxelStates` should only consider chunks within a radius (e.g. 4–5 chunks) of any player. Add a spatial index (e.g. chunk key → block placements) and only merge voxel state for nearby chunks. + +### Phase 2: Main Thread Freedom (2–3 weeks) + +| # | Task | Impact | Effort | Files | +|---|------|--------|--------|-------| +| 2.1 | **Async persistChunk** | Medium | 1–2 days | `PersistenceChunkProvider.ts`, `RegionFileFormat.ts` | +| 2.2 | **Worker terrain gen verification** | — | 0.5 day | `TerrainWorkerPool.ts`, `ProceduralChunkProvider.ts` | +| 2.3 | **Incremental voxel collider updates** | High | 3–5 days | `ChunkLattice.ts` | +| 2.4 | **Chunk send pacing** | Medium | 1–2 days | `NetworkSynchronizer.ts` | + +**2.1:** `persistChunk` currently calls `writeChunk` (sync). Move to async; queue writes and process in background. + +**2.3:** Add blocks to voxel colliders in batches (e.g. 256–512/tick) instead of full chunk. Use Rapier voxel API if it supports incremental updates. + +### Phase 3: Network & Sync (2–3 weeks) + +| # | Task | Impact | Effort | Files | +|---|------|--------|--------|-------| +| 3.1 | **Entity delta/compression** | High | 5–7 days | `NetworkSynchronizer.ts`, `Serializer.ts`, protocol | +| 3.2 | **Chunk delta updates** | Medium | 3–4 days | `NetworkSynchronizer.ts`, `ChunkLattice` | +| 3.3 | **Predictive chunk preload** | Medium | 2–3 days | `playground.ts` | + +**3.1:** Send position/rotation deltas or use quantized floats. Reference: Minecraft’s entity compression, Hytale’s QUIC usage. + +### Phase 4: Client Render Pipeline (3–4 weeks) + +| # | Task | Impact | Effort | Files | +|---|------|--------|--------|-------| +| 4.1 | **Greedy meshing (quad merging)** | Very high | 5–7 days | `ChunkWorker.ts` | +| 4.2 | **Vertex pooling** | Medium | 2–3 days | `ChunkMeshManager.ts`, `ChunkWorker.ts` | +| 4.3 | **Occlusion culling always-on** | Medium | 2–3 days | `ChunkManager.ts`, `Renderer.ts` | +| 4.4 | **Mesh apply budget** | Low | 1 day | `ChunkManager.ts` | + +**4.1:** Implement 0fps-style greedy meshing for opaque solids. Merge adjacent same-type faces; expect 2–64× fewer vertices. References: [0fps](https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/), [mikolalysenko/greedy-mesher](https://github.com/mikolalysenko/greedy-mesher). + +### Phase 5: Long-Term & Polish (ongoing) + +| # | Task | Impact | Effort | +|---|------|--------|--------| +| 5.1 | LOD impostors for distant chunks | Medium | 2–3 weeks | +| 5.2 | Brotli for region .bin payloads | Low | 1 week | +| 5.3 | Block/face limits (safety cap) | Low | <1 day | +| 5.4 | Profiling hooks (tick, chunk, mesh) | Low | 2–3 days | + +--- + +## 5. Implementation Order + +``` +Week 1–2: Phase 1 (collider locality, scoped _combineVoxelStates, defer env spawn) +Week 3–4: Phase 2 (async persistChunk, incremental voxel, chunk send pacing) +Week 5–6: Phase 3 (entity delta, chunk delta, predictive preload) +Week 7–10: Phase 4 (greedy meshing, vertex pooling, occlusion) +Ongoing: Phase 5 +``` + +--- + +## 6. Success Metrics + +| Metric | Current (Est.) | Target | +|--------|----------------|--------| +| Lag spikes when walking | Every ~5 steps | None within preload radius | +| Server tick time (p99) | 50–200 ms | < 16 ms | +| Chunk load (blocking) | 20–100 ms | < 5 ms (async) | +| Vertices per flat chunk | ~6000 | ~200–500 (greedy) | +| Client frame time | Spikes on new chunks | Stable ~16 ms (60 fps) | +| Entity packet share | ~90% | < 50% (delta/compression) | + +--- + +## 7. Key Files Reference + +| Component | Path | +|-----------|------| +| Chunk load loop | `server/src/playground.ts` | +| Collider processing | `server/src/worlds/blocks/ChunkLattice.ts` | +| Physics simulation | `server/src/worlds/physics/Simulation.ts` | +| Mesh generation | `client/src/workers/ChunkWorker.ts` | +| Chunk sync | `server/src/networking/NetworkSynchronizer.ts` | +| Region I/O | `server/src/worlds/maps/RegionFileFormat.ts` | +| Terrain gen | `server/src/worlds/maps/TerrainGenerator.ts`, `TerrainWorkerPool.ts` | +| Procedural provider | `server/src/worlds/maps/ProceduralChunkProvider.ts` | +| Persistence provider | `server/src/worlds/maps/PersistenceChunkProvider.ts` | +| World loop | `server/src/worlds/WorldLoop.ts` | + +--- + +## 8. PR #21 Action Items + +1. **Merge PR #21** for JSON-map games (boilerplate, huntcraft, etc.). +2. **Reuse chunk cache collider patterns** in `ChunkLattice` if applicable. +3. **Later:** Consider Brotli for region payloads or a similar compression layer. + +--- + +## 9. References + +- [VOXEL_PERFORMANCE_MASTER_PLAN.md](./VOXEL_PERFORMANCE_MASTER_PLAN.md) +- [CHUNK_LOADING_ARCHITECTURE.md](./CHUNK_LOADING_ARCHITECTURE.md) +- [VOXEL_RENDERING_RESEARCH.md](./VOXEL_RENDERING_RESEARCH.md) +- [OPTIMIZATION_STRATEGY.md](./OPTIMIZATION_STRATEGY.md) +- [PR #21 – Compressed world maps](https://github.com/hytopiagg/hytopia-source/pull/21) +- [0fps Greedy Meshing](https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/) +- [mikolalysenko/greedy-mesher](https://github.com/mikolalysenko/greedy-mesher) +- [Minecraft Chunk Loading (Technical Wiki)](https://techmcdocs.github.io/pages/GameMechanics/ChunkLoading/) +- [Hytale Engine Technical Deep Dive](https://hytalecharts.com/news/hytale-engine-technical-deep-dive) diff --git a/ai-memory/docs/perf-framework-research-2026-03-05/raw/VOXEL_ENGINE_2026_MASTER_PLAN (1).md b/ai-memory/docs/perf-framework-research-2026-03-05/raw/VOXEL_ENGINE_2026_MASTER_PLAN (1).md new file mode 100644 index 00000000..c74ee120 --- /dev/null +++ b/ai-memory/docs/perf-framework-research-2026-03-05/raw/VOXEL_ENGINE_2026_MASTER_PLAN (1).md @@ -0,0 +1,218 @@ +# Voxel Engine 2026: World-Class Performance Master Plan + +**Document Owner:** Head of Development +**Classification:** Engineering Roadmap +**Target:** Minecraft/Hytale-grade smoothness; browser-first, 2026-ready +**Version:** 1.0 +**Date:** March 2026 + +--- + +## Executive Summary + +Hytopia aims to deliver voxel gameplay that feels as smooth and responsive as Minecraft and Hytale, while running in the browser. The current architecture has solid foundations—async chunk loading, worker terrain generation, deferred colliders—but several bottlenecks prevent parity with industry leaders. This plan addresses those gaps with a phased, research-backed approach that delivers measurable improvements without over-engineering. + +**Key thesis:** The lag and stutter are almost entirely **software architecture** issues, not hardware. Minecraft and Hytale run smoothly on similar hardware because they use different patterns. We close the gap by adopting those patterns. + +**Target outcome:** Walk/fly through a procedural world with **no perceptible lag spikes** within the preload radius, **stable 60 FPS** on the client, and **<16 ms server tick times** (p99). + +--- + +## Part 1: Strategic Context + +### 1.1 Industry Benchmark: What “On Par” Means + +| Game | Chunk Load | Physics | Rendering | Network | Notes | +|------|------------|---------|-----------|---------|-------| +| **Minecraft Java** | Worker threads, region format | Per-chunk colliders, deferred | Greedy meshing (approximate), occlusion | Delta/delta-like entity sync | 15+ years of iteration | +| **Minecraft Bedrock** | Async pipeline, priority queue | Spatial partitioning | Meshing + LOD | Variable tick rate by distance | C++ / C#; mobile-first | +| **Hytale** | Worker pool, variable chunk sizes | Batched, spatial | Mesh culling, LOD | QUIC, lower latency | Modern engine, Flecs ECS | +| **Bloxd.io** | Browser streaming | Custom voxel physics | Face culling, vertex pooling | JS-based | Browser-only | + +**Hytopia’s position:** We are browser-bound (Node server + Web client). We can’t use C++ or multiple cores on the client, but we *can* adopt the same *concepts*: async I/O, spatial locality, greedy meshing, quantized network formats, and time-budgeted main-thread work. + +### 1.2 Gap Analysis (Prioritized) + +| Priority | Gap | Impact | Root Cause | +|----------|-----|--------|------------| +| P0 | Collider work O(world) | Tick spikes, unplayable under load | `_combineVoxelStates` scans all chunks of each block type | +| P0 | No greedy meshing | 2–64× more vertices than needed | Per-face quads, no merging | +| P1 | Entity sync volume | ~90% of packets | Full pos/rot floats, no quantization | +| P1 | Sync chunk persist | Main-thread blocking | `writeChunk` sync | +| P2 | No occlusion culling | Overdraw in caves | All loaded batches rendered | +| P2 | No distance-based entity LOD | Far entities same cost as near | Single sync rate | +| P3 | Vertex allocation churn | GC spikes on mesh updates | No pooling | + +--- + +## Part 2: Phased Roadmap + +### Phase 0: Foundation & Instrumentation (Week 1) + +**Goal:** Establish baselines and guardrails before major refactors. + +| Task | Owner | Deliverable | +|------|-------|-------------| +| Profiling hooks | Eng | Tick duration, chunk load time, collider time, mesh build time | +| Metrics dashboard | Eng | Real-time charts for key metrics | +| Block/face limits | Eng | Hard cap (e.g. 500K faces) to avoid meltdown | +| Regression suite | QA | Automated “fly-through” test, capture tick/frame times | + +**Success:** We can measure and reproduce performance issues in CI and on-device. + +--- + +### Phase 1: Collider Locality (Weeks 2–3) + +**Goal:** Remove O(world) collider scans. Physics and chunk work must scale with **visible/nearby** chunks only. + +| Task | Effort | Description | +|------|--------|-------------| +| Spatial index for block placements | 3 days | Chunk key → block placements; no global iteration | +| Scoped `_combineVoxelStates` | 2 days | Merge only chunks within N chunks of any player | +| Collider unload for distant chunks | 1 day | Remove colliders when chunk unloads; don’t keep in physics | +| Time-budget verification | 0.5 day | Ensure 8 ms cap is respected; tune if needed | + +**Files:** `ChunkLattice.ts`, `playground.ts` + +**Success:** Tick time (p99) drops from 50–200 ms to <25 ms under typical load. + +--- + +### Phase 2: Main-Thread Freedom (Weeks 4–5) + +**Goal:** No sync blocking on I/O or heavy computation on the game loop. + +| Task | Effort | Description | +|------|--------|-------------| +| Async `persistChunk` | 1.5 days | Queue writes; flush in background | +| Async provider audit | 0.5 day | Confirm `requestChunk` → `getChunkAsync` path is used | +| Incremental voxel collider updates | 4 days | Add blocks in batches (256–512/tick) instead of full chunk | +| Chunk send pacing | 1.5 days | Smooth chunk sync; avoid burst of 8 chunks in one tick | + +**Files:** `PersistenceChunkProvider.ts`, `RegionFileFormat.ts`, `ChunkLattice.ts`, `NetworkSynchronizer.ts` + +**Success:** Chunk load + persist never block tick; no “catch up” spikes. + +--- + +### Phase 3: Entity Sync Compression (Weeks 6–7) + +**Goal:** Reduce entity pos/rot from ~90% of packets to <50%, with no perceptible quality loss. + +| Task | Effort | Description | +|------|--------|-------------| +| Quantized position (1/256 block, 16-bit) | 1 day | Server sends `pq`; client decodes | +| Yaw-only rotation for players | 0.5 day | 1 float vs 4 for player avatars | +| Distance-based sync rate (30/15/5 Hz) | 1 day | Near = 30 Hz, mid = 15 Hz, far = 5 Hz | +| Quantized quaternion (smallest-three) | 2 days | For NPCs and other full-rotation entities | +| Bulk pos/rot packet (optional) | 2 days | Structure-of-arrays for unreliable updates | + +**Files:** `Serializer.ts`, `NetworkSynchronizer.ts`, `protocol/schemas/Entity.ts`, `Deserializer.ts`, `EntityManager.ts` + +**Success:** Entity sync bytes/update reduced by 50–60%; bandwidth share <50%. + +--- + +### Phase 4: Greedy Meshing (Weeks 8–10) + +**Goal:** Cut vertex count by 2–64× for typical terrain; stable 60 FPS on chunk load. + +| Task | Effort | Description | +|------|--------|-------------| +| Greedy mesh algorithm (opaque solids) | 5 days | 0fps-style sweep and merge; ref `docs/research/GREEDY_MESHING_IMPLEMENTATION_GUIDE.md` | +| Integration with ChunkWorker | 2 days | Per-batch-type merge; transparent blocks unchanged | +| AO + lighting on merged quads | 1 day | Ensure ambient occlusion and lighting still apply | +| Benchmarks and tuning | 1 day | Measure build time vs vertex reduction | + +**Files:** `ChunkWorker.ts`, `ChunkMeshManager.ts` + +**Success:** Flat chunk: ~6000 vertices → ~200–500; frame time stable on new chunk load. + +--- + +### Phase 5: Render Pipeline Polish (Weeks 11–13) + +**Goal:** GPU efficiency and graceful degradation on low-end devices. + +| Task | Effort | Description | +|------|--------|-------------| +| Vertex pooling | 2 days | Reuse BufferGeometry/ArrayBuffers; avoid per-frame allocations | +| Occlusion culling always-on | 2 days | BFS from camera; cull hidden batches | +| Mesh apply budget | 1 day | Limit meshes applied per frame; spread load | +| Block/face limits enforcement | 0.5 day | Reduce view distance when over cap | + +**Files:** `ChunkMeshManager.ts`, `ChunkManager.ts`, `ChunkWorker.ts`, `Renderer.ts` + +**Success:** No GC spikes on chunk load; overdraw reduced in cave-heavy areas. + +--- + +### Phase 6: Long-Term (Month 4+) + +| Task | Impact | Effort | +|------|--------|--------| +| LOD impostors for distant chunks | Medium | 2–3 weeks | +| Brotli (or similar) for region payloads | Low | 1 week | +| Predictive chunk preload | Medium | 1 week | +| Client-side entity prediction | Medium (latency) | 2+ weeks | + +--- + +## Part 3: Research Documentation + +The following research docs support implementation and design decisions: + +| Document | Purpose | +|----------|---------| +| [MINECRAFT_ARCHITECTURE_RESEARCH.md](./research/MINECRAFT_ARCHITECTURE_RESEARCH.md) | How Minecraft structures chunk loading, colliders, and meshing | +| [GREEDY_MESHING_IMPLEMENTATION_GUIDE.md](./research/GREEDY_MESHING_IMPLEMENTATION_GUIDE.md) | Step-by-step greedy meshing for ChunkWorker | +| [COLLIDER_ARCHITECTURE_RESEARCH.md](./research/COLLIDER_ARCHITECTURE_RESEARCH.md) | Spatial locality and incremental colliders | +| [NETWORK_PROTOCOL_2026_RESEARCH.md](./research/NETWORK_PROTOCOL_2026_RESEARCH.md) | Modern entity sync: quantization, delta, LOD | + +**Mandate:** Engineers implementing Phase 2+ work must read the relevant research doc before coding. + +--- + +## Part 4: Success Metrics + +| Metric | Baseline (Current) | Phase 3 Target | Phase 6 Target | +|--------|--------------------|----------------|----------------| +| Server tick time (p99) | 50–200 ms | <25 ms | <16 ms | +| Chunk load (blocking) | 20–100 ms | 0 (async) | 0 | +| Vertices per flat chunk | ~6000 | ~200–500 | ~200–500 | +| Entity sync % of packets | ~90% | ~60% | <50% | +| Client frame time (p99) | Spikes to 50+ ms | <25 ms | <16 ms | +| Perceived lag spikes | Every ~5 steps | None in preload | None | + +--- + +## Part 5: Risks & Mitigations + +| Risk | Mitigation | +|------|------------| +| Greedy meshing regresses build time | Time-budget; fallback to non-greedy if over budget | +| Protocol changes break old clients | Backward-compatible optional fields; version handshake | +| Collider refactor introduces physics bugs | Rigorous test: spawn, walk, mine, place; compare before/after | +| Scope creep | Phases are fixed; Phase 6 is explicitly “long-term” | + +--- + +## Part 6: Dependencies & Prerequisites + +- **PR #21 (Compressed JSON maps):** Merge for JSON-map games; not blocking procedural world. +- **TerrainWorkerPool:** Already in place; verify `getChunkAsync` is used in playground. +- **Protocol package:** Schema changes require protocol version bump; coordinate with SDK consumers. +- **Browser support:** Target evergreen browsers; no polyfills for cutting-edge APIs. + +--- + +## Part 7: Sign-Off + +This plan represents a realistic path to Minecraft/Hytale-grade smoothness for Hytopia’s procedural world. It prioritizes the highest-impact bottlenecks (colliders, greedy meshing, entity sync) and defers nice-to-haves (LOD impostors, prediction) to later phases. + +**Recommendation:** Approve and execute Phase 0–1 immediately. Re-evaluate after Phase 3 based on metrics and user feedback. + +--- + +*— Head of Development* diff --git a/ai-memory/docs/perf-framework-research-2026-03-05/raw/VOXEL_ENGINE_2026_MASTER_PLAN.md b/ai-memory/docs/perf-framework-research-2026-03-05/raw/VOXEL_ENGINE_2026_MASTER_PLAN.md new file mode 100644 index 00000000..c74ee120 --- /dev/null +++ b/ai-memory/docs/perf-framework-research-2026-03-05/raw/VOXEL_ENGINE_2026_MASTER_PLAN.md @@ -0,0 +1,218 @@ +# Voxel Engine 2026: World-Class Performance Master Plan + +**Document Owner:** Head of Development +**Classification:** Engineering Roadmap +**Target:** Minecraft/Hytale-grade smoothness; browser-first, 2026-ready +**Version:** 1.0 +**Date:** March 2026 + +--- + +## Executive Summary + +Hytopia aims to deliver voxel gameplay that feels as smooth and responsive as Minecraft and Hytale, while running in the browser. The current architecture has solid foundations—async chunk loading, worker terrain generation, deferred colliders—but several bottlenecks prevent parity with industry leaders. This plan addresses those gaps with a phased, research-backed approach that delivers measurable improvements without over-engineering. + +**Key thesis:** The lag and stutter are almost entirely **software architecture** issues, not hardware. Minecraft and Hytale run smoothly on similar hardware because they use different patterns. We close the gap by adopting those patterns. + +**Target outcome:** Walk/fly through a procedural world with **no perceptible lag spikes** within the preload radius, **stable 60 FPS** on the client, and **<16 ms server tick times** (p99). + +--- + +## Part 1: Strategic Context + +### 1.1 Industry Benchmark: What “On Par” Means + +| Game | Chunk Load | Physics | Rendering | Network | Notes | +|------|------------|---------|-----------|---------|-------| +| **Minecraft Java** | Worker threads, region format | Per-chunk colliders, deferred | Greedy meshing (approximate), occlusion | Delta/delta-like entity sync | 15+ years of iteration | +| **Minecraft Bedrock** | Async pipeline, priority queue | Spatial partitioning | Meshing + LOD | Variable tick rate by distance | C++ / C#; mobile-first | +| **Hytale** | Worker pool, variable chunk sizes | Batched, spatial | Mesh culling, LOD | QUIC, lower latency | Modern engine, Flecs ECS | +| **Bloxd.io** | Browser streaming | Custom voxel physics | Face culling, vertex pooling | JS-based | Browser-only | + +**Hytopia’s position:** We are browser-bound (Node server + Web client). We can’t use C++ or multiple cores on the client, but we *can* adopt the same *concepts*: async I/O, spatial locality, greedy meshing, quantized network formats, and time-budgeted main-thread work. + +### 1.2 Gap Analysis (Prioritized) + +| Priority | Gap | Impact | Root Cause | +|----------|-----|--------|------------| +| P0 | Collider work O(world) | Tick spikes, unplayable under load | `_combineVoxelStates` scans all chunks of each block type | +| P0 | No greedy meshing | 2–64× more vertices than needed | Per-face quads, no merging | +| P1 | Entity sync volume | ~90% of packets | Full pos/rot floats, no quantization | +| P1 | Sync chunk persist | Main-thread blocking | `writeChunk` sync | +| P2 | No occlusion culling | Overdraw in caves | All loaded batches rendered | +| P2 | No distance-based entity LOD | Far entities same cost as near | Single sync rate | +| P3 | Vertex allocation churn | GC spikes on mesh updates | No pooling | + +--- + +## Part 2: Phased Roadmap + +### Phase 0: Foundation & Instrumentation (Week 1) + +**Goal:** Establish baselines and guardrails before major refactors. + +| Task | Owner | Deliverable | +|------|-------|-------------| +| Profiling hooks | Eng | Tick duration, chunk load time, collider time, mesh build time | +| Metrics dashboard | Eng | Real-time charts for key metrics | +| Block/face limits | Eng | Hard cap (e.g. 500K faces) to avoid meltdown | +| Regression suite | QA | Automated “fly-through” test, capture tick/frame times | + +**Success:** We can measure and reproduce performance issues in CI and on-device. + +--- + +### Phase 1: Collider Locality (Weeks 2–3) + +**Goal:** Remove O(world) collider scans. Physics and chunk work must scale with **visible/nearby** chunks only. + +| Task | Effort | Description | +|------|--------|-------------| +| Spatial index for block placements | 3 days | Chunk key → block placements; no global iteration | +| Scoped `_combineVoxelStates` | 2 days | Merge only chunks within N chunks of any player | +| Collider unload for distant chunks | 1 day | Remove colliders when chunk unloads; don’t keep in physics | +| Time-budget verification | 0.5 day | Ensure 8 ms cap is respected; tune if needed | + +**Files:** `ChunkLattice.ts`, `playground.ts` + +**Success:** Tick time (p99) drops from 50–200 ms to <25 ms under typical load. + +--- + +### Phase 2: Main-Thread Freedom (Weeks 4–5) + +**Goal:** No sync blocking on I/O or heavy computation on the game loop. + +| Task | Effort | Description | +|------|--------|-------------| +| Async `persistChunk` | 1.5 days | Queue writes; flush in background | +| Async provider audit | 0.5 day | Confirm `requestChunk` → `getChunkAsync` path is used | +| Incremental voxel collider updates | 4 days | Add blocks in batches (256–512/tick) instead of full chunk | +| Chunk send pacing | 1.5 days | Smooth chunk sync; avoid burst of 8 chunks in one tick | + +**Files:** `PersistenceChunkProvider.ts`, `RegionFileFormat.ts`, `ChunkLattice.ts`, `NetworkSynchronizer.ts` + +**Success:** Chunk load + persist never block tick; no “catch up” spikes. + +--- + +### Phase 3: Entity Sync Compression (Weeks 6–7) + +**Goal:** Reduce entity pos/rot from ~90% of packets to <50%, with no perceptible quality loss. + +| Task | Effort | Description | +|------|--------|-------------| +| Quantized position (1/256 block, 16-bit) | 1 day | Server sends `pq`; client decodes | +| Yaw-only rotation for players | 0.5 day | 1 float vs 4 for player avatars | +| Distance-based sync rate (30/15/5 Hz) | 1 day | Near = 30 Hz, mid = 15 Hz, far = 5 Hz | +| Quantized quaternion (smallest-three) | 2 days | For NPCs and other full-rotation entities | +| Bulk pos/rot packet (optional) | 2 days | Structure-of-arrays for unreliable updates | + +**Files:** `Serializer.ts`, `NetworkSynchronizer.ts`, `protocol/schemas/Entity.ts`, `Deserializer.ts`, `EntityManager.ts` + +**Success:** Entity sync bytes/update reduced by 50–60%; bandwidth share <50%. + +--- + +### Phase 4: Greedy Meshing (Weeks 8–10) + +**Goal:** Cut vertex count by 2–64× for typical terrain; stable 60 FPS on chunk load. + +| Task | Effort | Description | +|------|--------|-------------| +| Greedy mesh algorithm (opaque solids) | 5 days | 0fps-style sweep and merge; ref `docs/research/GREEDY_MESHING_IMPLEMENTATION_GUIDE.md` | +| Integration with ChunkWorker | 2 days | Per-batch-type merge; transparent blocks unchanged | +| AO + lighting on merged quads | 1 day | Ensure ambient occlusion and lighting still apply | +| Benchmarks and tuning | 1 day | Measure build time vs vertex reduction | + +**Files:** `ChunkWorker.ts`, `ChunkMeshManager.ts` + +**Success:** Flat chunk: ~6000 vertices → ~200–500; frame time stable on new chunk load. + +--- + +### Phase 5: Render Pipeline Polish (Weeks 11–13) + +**Goal:** GPU efficiency and graceful degradation on low-end devices. + +| Task | Effort | Description | +|------|--------|-------------| +| Vertex pooling | 2 days | Reuse BufferGeometry/ArrayBuffers; avoid per-frame allocations | +| Occlusion culling always-on | 2 days | BFS from camera; cull hidden batches | +| Mesh apply budget | 1 day | Limit meshes applied per frame; spread load | +| Block/face limits enforcement | 0.5 day | Reduce view distance when over cap | + +**Files:** `ChunkMeshManager.ts`, `ChunkManager.ts`, `ChunkWorker.ts`, `Renderer.ts` + +**Success:** No GC spikes on chunk load; overdraw reduced in cave-heavy areas. + +--- + +### Phase 6: Long-Term (Month 4+) + +| Task | Impact | Effort | +|------|--------|--------| +| LOD impostors for distant chunks | Medium | 2–3 weeks | +| Brotli (or similar) for region payloads | Low | 1 week | +| Predictive chunk preload | Medium | 1 week | +| Client-side entity prediction | Medium (latency) | 2+ weeks | + +--- + +## Part 3: Research Documentation + +The following research docs support implementation and design decisions: + +| Document | Purpose | +|----------|---------| +| [MINECRAFT_ARCHITECTURE_RESEARCH.md](./research/MINECRAFT_ARCHITECTURE_RESEARCH.md) | How Minecraft structures chunk loading, colliders, and meshing | +| [GREEDY_MESHING_IMPLEMENTATION_GUIDE.md](./research/GREEDY_MESHING_IMPLEMENTATION_GUIDE.md) | Step-by-step greedy meshing for ChunkWorker | +| [COLLIDER_ARCHITECTURE_RESEARCH.md](./research/COLLIDER_ARCHITECTURE_RESEARCH.md) | Spatial locality and incremental colliders | +| [NETWORK_PROTOCOL_2026_RESEARCH.md](./research/NETWORK_PROTOCOL_2026_RESEARCH.md) | Modern entity sync: quantization, delta, LOD | + +**Mandate:** Engineers implementing Phase 2+ work must read the relevant research doc before coding. + +--- + +## Part 4: Success Metrics + +| Metric | Baseline (Current) | Phase 3 Target | Phase 6 Target | +|--------|--------------------|----------------|----------------| +| Server tick time (p99) | 50–200 ms | <25 ms | <16 ms | +| Chunk load (blocking) | 20–100 ms | 0 (async) | 0 | +| Vertices per flat chunk | ~6000 | ~200–500 | ~200–500 | +| Entity sync % of packets | ~90% | ~60% | <50% | +| Client frame time (p99) | Spikes to 50+ ms | <25 ms | <16 ms | +| Perceived lag spikes | Every ~5 steps | None in preload | None | + +--- + +## Part 5: Risks & Mitigations + +| Risk | Mitigation | +|------|------------| +| Greedy meshing regresses build time | Time-budget; fallback to non-greedy if over budget | +| Protocol changes break old clients | Backward-compatible optional fields; version handshake | +| Collider refactor introduces physics bugs | Rigorous test: spawn, walk, mine, place; compare before/after | +| Scope creep | Phases are fixed; Phase 6 is explicitly “long-term” | + +--- + +## Part 6: Dependencies & Prerequisites + +- **PR #21 (Compressed JSON maps):** Merge for JSON-map games; not blocking procedural world. +- **TerrainWorkerPool:** Already in place; verify `getChunkAsync` is used in playground. +- **Protocol package:** Schema changes require protocol version bump; coordinate with SDK consumers. +- **Browser support:** Target evergreen browsers; no polyfills for cutting-edge APIs. + +--- + +## Part 7: Sign-Off + +This plan represents a realistic path to Minecraft/Hytale-grade smoothness for Hytopia’s procedural world. It prioritizes the highest-impact bottlenecks (colliders, greedy meshing, entity sync) and defers nice-to-haves (LOD impostors, prediction) to later phases. + +**Recommendation:** Approve and execute Phase 0–1 immediately. Re-evaluate after Phase 3 based on metrics and user feedback. + +--- + +*— Head of Development* diff --git a/ai-memory/docs/perf-framework-research-2026-03-05/raw/VOXEL_PERFORMANCE_MASTER_PLAN.md b/ai-memory/docs/perf-framework-research-2026-03-05/raw/VOXEL_PERFORMANCE_MASTER_PLAN.md new file mode 100644 index 00000000..9b7fdda0 --- /dev/null +++ b/ai-memory/docs/perf-framework-research-2026-03-05/raw/VOXEL_PERFORMANCE_MASTER_PLAN.md @@ -0,0 +1,153 @@ +# Voxel Engine Performance Master Plan +## Making Hytopia as Smooth as Minecraft & Hytale + +**Problem:** Lag every ~5 steps; constant chunk rendering; engine feels clunky compared to Minecraft/Hytale. + +**Conclusion:** This is primarily a **software/codebase architecture** issue, not hardware. Minecraft and Hytale run smoothly on similar hardware because they use different architectures. The plan below addresses the gaps. + +--- + +## Part 1: Root Cause Analysis + +### Why "Every 5 Steps" Lag Happens + +| Step | What Happens | Bottleneck | +|------|--------------|------------| +| 1 | Player moves → enters new chunk/batch range | Server loads 1 chunk/tick (CHUNKS_PER_TICK=1) | +| 2 | `getOrCreateChunk` runs | **Sync** disk read or procedural gen blocks main thread | +| 3 | Chunk queued for collider | `processPendingColliderChunks(1)` – 1/tick | +| 4 | `_addChunkBlocksToColliders` | **Heavy:** 4096 blocks, voxel propagation, `_combineVoxelStates` scans ALL chunks of that block type | +| 5 | Server sends chunk to client | Network ok, but chunk sync triggers client work | +| 6 | Client receives ChunksPacket | Posts to ChunkWorker | +| 7 | ChunkWorker builds mesh | **No greedy meshing** – 1 quad per face, 64× more than optimal for flat terrain | +| 8 | Mesh sent back, added to scene | BufferGeometry creation, possible GC spike | +| 9 | Main thread applies mesh | Can cause frame hitch | + +### Current vs. Minecraft/Hytale + +| Aspect | Hytopia (Current) | Minecraft / Hytale | +|--------|-------------------|---------------------| +| Chunk load | Sync on main thread | Worker threads, async | +| File I/O | `fs.readSync`, `zlib.gunzipSync` | Async, or worker | +| Terrain gen | Sync in main thread | Worker pool | +| Collider creation | Sync, 1/tick, O(world size) | Deferred, batched, O(chunk) | +| Mesh generation | Worker ✅ | Worker ✅ | +| Greedy meshing | ❌ (1 quad/face) | ✅ (merged quads, 2–64× fewer) | +| LOD | ✅ (step 2/4) | ✅ + impostors | +| Occlusion culling | Only when over face limit | Chunk-section visibility | +| Chunk send rate | Per ADD_CHUNK event | Batched, rate-limited | + +--- + +## Part 2: Prioritized Fixes + +### Tier 1: Quick Wins (1–3 days each) + +| # | Fix | Impact | Effort | Files | +|---|-----|--------|--------|-------| +| 1 | **Increase CHUNKS_PER_TICK** to 2–3 | Fewer "catch up" spikes when moving | 5 min | `playground.ts` | +| 2 | **Time-budget collider processing** | Cap ms per tick (e.g. 8 ms), process multiple chunks if time allows | Medium | `ChunkLattice.ts`, `playground.ts` | +| 3 | **Chunk send batching** | Don’t flood client; batch chunk sync every N ms or per tick | Medium | `NetworkSynchronizer.ts` | +| 4 | **Avoid collider work for distant chunks** | Only add colliders for chunks within 2–3 chunks of player | Medium | `ChunkLattice.ts`, `playground.ts` | + +### Tier 2: High Impact (3–7 days each) + +| # | Fix | Impact | Effort | Notes | +|---|-----|--------|--------|-------| +| 5 | **Greedy meshing (quad merging)** | 2–64× fewer vertices for terrain | 3–5 days | ChunkWorker; ref 0fps, mikolalysenko/greedy-mesher | +| 6 | **Async chunk provider** | `getChunk()` returns `Promise`; no main-thread blocking | 2–3 days | PersistenceChunkProvider, ProceduralChunkProvider, ChunkLattice | +| 7 | **Worker terrain generation** | Move `generateChunk` to `worker_threads` | 2–3 days | TerrainGenerator, ProceduralChunkProvider | +| 8 | **Async file I/O** | `fs.promises`, `zlib.gunzip` async | 1–2 days | RegionFileFormat.ts | + +### Tier 3: Architectural (1–2 weeks each) + +| # | Fix | Impact | Effort | Notes | +|---|-----|--------|--------|-------| +| 9 | **Incremental colliders** | Add blocks to voxel collider in batches (e.g. 256/tick) instead of full chunk | High | Rapier voxel API; ChunkLattice | +| 10 | **Collider locality** | `_getBlockTypePlacements` and `_combineVoxelStates` should not scan entire world | High | ChunkLattice; spatial indexing | +| 11 | **Chunk preloading by prediction** | Load chunks in movement direction before player arrives | Medium | playground.ts, loadChunksAroundPlayers | +| 12 | **Vertex pooling** | Reuse BufferGeometry / ArrayBuffers to reduce allocations and GC | Medium | ChunkMeshManager, ChunkWorker | + +### Tier 4: Polish (Ongoing) + +| # | Fix | Impact | Effort | +|---|-----|--------|--------| +| 13 | **Occlusion culling always-on** | Not just when over face limit | Medium | +| 14 | **LOD impostors** | Billboard or simplified mesh for very far chunks | High | +| 15 | **Profiling hooks** | Tick time, chunk load time, mesh build time | Low | +| 16 | **Block/face limits** | Hard cap to avoid meltdown on weak devices | Low | + +--- + +## Part 3: Recommended Implementation Order + +### Phase 1: Stop the Bleeding (Week 1) + +1. **Time-budget collider processing** – Cap at 8 ms/tick; process as many chunks as fit. +2. **Increase CHUNKS_PER_TICK** to 2–3. +3. **Spatial collider culling** – Only create colliders for chunks within 2–3 chunks of any player. +4. **Chunk send batching** – Batch chunk sync; don’t send 10 chunks in one frame. + +### Phase 2: Main Thread Freedom (Week 2–3) + +5. **Async file I/O** – `fs.promises`, async decompress. +6. **Async chunk provider** – `getChunk()` returns `Promise`; ChunkLattice awaits. +7. **Worker terrain gen** – Move `generateChunk` to worker thread. + +### Phase 3: Render Pipeline (Week 4–5) + +8. **Greedy meshing** – Implement in ChunkWorker for opaque solids; merge adjacent same-type faces. +9. **Vertex pooling** – Reuse geometry buffers where possible. + +### Phase 4: Long-Term (Month 2+) + +10. **Incremental colliders** – Batched voxel updates. +11. **Collider locality** – Remove global scans. +12. **Occlusion always-on** – Reduce overdraw. + +--- + +## Part 4: Hardware vs. Software + +| Factor | Assessment | +|--------|------------| +| **Hardware** | Unlikely primary cause if Minecraft/Hytale run fine. | +| **Software** | Sync I/O, sync terrain gen, heavy collider work, no greedy meshing – all main-thread and render bottlenecks. | +| **Codebase** | Architecture is serviceable but lacks async pipeline and mesh optimization used by mature voxel engines. | + +--- + +## Part 5: Key Files + +| Component | Path | +|-----------|------| +| Chunk load loop | `server/src/playground.ts` | +| Collider processing | `server/src/worlds/blocks/ChunkLattice.ts` | +| Mesh generation | `client/src/workers/ChunkWorker.ts` | +| Chunk sync to client | `server/src/networking/NetworkSynchronizer.ts` | +| Disk I/O | `server/src/worlds/maps/RegionFileFormat.ts` | +| Terrain generation | `server/src/worlds/maps/TerrainGenerator.ts`, `ProceduralChunkProvider.ts` | +| Client chunk handling | `client/src/chunks/ChunkManager.ts` | + +--- + +## Part 6: Success Metrics + +| Metric | Current (Est.) | Target | +|--------|----------------|--------| +| Lag spikes when walking | Every ~5 steps | None within preload radius | +| Tick time (p99) | 50–200 ms | < 16 ms | +| Chunk load time | 20–100 ms (blocking) | < 5 ms (async) | +| Vertices per chunk (flat) | ~6000 (no greedy) | ~200–500 (greedy) | +| Frame time (client) | Spikes on new chunks | Stable 16 ms (60 fps) | + +--- + +## References + +- `docs/CHUNK_LOADING_ARCHITECTURE.md` +- `docs/VOXEL_RENDERING_RESEARCH.md` +- `docs/OPTIMIZATION_STRATEGY.md` +- [0fps Greedy Meshing](https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/) +- [mikolalysenko/greedy-mesher](https://github.com/mikolalysenko/greedy-mesher) +- Hytale engine deep dive: variable chunks, LOD, mesh optimization diff --git a/ai-memory/docs/perf-framework-research-2026-03-05/raw/VOXEL_RENDERING_RESEARCH.md b/ai-memory/docs/perf-framework-research-2026-03-05/raw/VOXEL_RENDERING_RESEARCH.md new file mode 100644 index 00000000..5be2a42d --- /dev/null +++ b/ai-memory/docs/perf-framework-research-2026-03-05/raw/VOXEL_RENDERING_RESEARCH.md @@ -0,0 +1,190 @@ +# Voxel World Smoothness: Research on Minecraft, Hytale, and bloxd + +Deep research into how popular voxel games keep worlds lag-free and smooth during flight/movement. + +--- + +## Summary: What These Games Do + +| Technique | Minecraft | Hytale | bloxd | Hytopia (Current) | +|-----------|-----------|--------|-------|-------------------| +| **Face culling** | ✅ | ✅ | ✅ | ✅ (ChunkWorker) | +| **Greedy meshing** | ✅ (approximation) | ✅ | ✅ | ❌ | +| **Chunk batching** | ✅ (16×16×16) | Variable sizes | ✅ | ✅ (2×2×2 batches) | +| **Async mesh generation** | ✅ (worker) | ✅ | ✅ | ✅ (ChunkWorker) | +| **View distance** | ✅ | ✅ | ✅ | ✅ | +| **LOD (distant simplification)** | ✅ | ✅ | ✅ | ❌ | +| **Occlusion / cave culling** | ✅ (advanced) | Partial | Partial | ❌ | +| **Vertex pooling** | — | — | ✅ | ❌ | +| **Block/face limits** | Implicit | — | — | ❌ | + +--- + +## 1. Face Culling (Already Implemented ✅) + +**What it does:** Only render faces that are visible—i.e. faces where the adjacent block is empty or transparent. Interior faces between solid blocks are never drawn. + +**0fps comparison:** On a solid 8×8×8 cube: +- Stupid method: 3,072 quads (6 per block) +- Culling: 384 quads (1 per surface face) +- **~8× reduction** + +**Hytopia status:** Already in `ChunkWorker.ts` (lines 962–985). Neighbor check per face; solid opaque neighbors → face is culled. **No change needed.** + +--- + +## 2. Greedy Meshing / Greedy Quad Merging (Not Implemented ❌) + +**What it does:** Merge adjacent faces with the same texture/material into larger quads. Instead of many small quads, you get fewer large quads covering the same surface. + +**0fps example:** Same 8×8×8 solid cube: +- Culling: 384 quads +- Greedy: **6 quads** (one per side) +- **64× reduction over culling** + +**Algorithm (0fps):** +1. Sweep the 3D volume in 3 directions (X, Y, Z) +2. For each 2D slice, identify visible faces +3. Greedily merge adjacent same-type faces into rectangles +4. Order: top-to-bottom, left-to-right; pick the lexicographically minimal mesh + +**Multiple block types:** Group by (block type, normal direction). Mesh each group separately. + +**Performance trade-off:** +- Greedy is slower to *build* than culling (more passes, more logic) +- But produces far fewer vertices → faster rendering and less GPU memory +- Modern bottleneck is often CPU→GPU transfer; fewer vertices = less data = smoother + +**Hytopia status:** ChunkWorker emits one quad per visible face. No merging. + +**Recommendation:** High impact. Implement greedy meshing in ChunkWorker for opaque solid blocks first. Reference: [0fps greedy meshing](https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/), [mikolalysenko/greedy-mesher](https://github.com/mikolalysenko/greedy-mesher). + +--- + +## 3. Occlusion / Cave Culling (Not Implemented ❌) + +**What it does:** Don’t render chunks (or chunk sections) that are completely hidden behind solid terrain. E.g. caves behind a mountain. + +**Minecraft (Tommo’s Advanced Cave Culling, 2014):** +- Works on 16×16×16 chunk sections +- Builds a connectivity graph of transparent/air paths +- BFS from camera to find visible sections +- Culls sections unreachable through air/transparent blocks +- ~14% frame time improvement + +**Hytopia status:** No occlusion culling. All loaded chunks in view distance are rendered if in frustum. + +**Recommendation:** Medium impact, higher complexity. Consider chunk-section visibility BFS. Less urgent than greedy meshing. + +--- + +## 4. Level of Detail (LOD) (Not Implemented ❌) + +**What it does:** Render distant chunks with simpler geometry—fewer quads, lower resolution, or simplified shapes. + +**Hytale:** Variable chunk sizes; LOD where distant chunks use lower-detail meshes. + +**Typical approach:** +- Near: Full detail +- Mid: Merged/simplified mesh +- Far: Very low poly or impostors + +**Hytopia status:** No LOD. All chunks use the same mesh quality. + +**Recommendation:** Medium impact. Could start with “skip every other block” or similar for distant batches. More complex: proper LOD meshes. + +--- + +## 5. Async Mesh Generation (Already Implemented ✅) + +**What it does:** Build chunk meshes in a worker thread so the main thread stays responsive. + +**Hytopia status:** `ChunkWorker.ts` runs in a Web Worker. Mesh building is off the main thread. **Already good.** + +--- + +## 6. Block / Face Limits + +**What it does:** Cap total blocks or faces to avoid overload. E.g. stop loading chunks if face count exceeds a threshold. + +**Hytopia status:** No hard limit. Chunk count is bounded by view distance, but no per-frame or total face limit. + +**Recommendation:** Low priority. Could add a safety cap (e.g. max 500K faces) to avoid extreme lag on weak devices. + +--- + +## 7. Vertex Pooling (bloxd / High-Performance Engines) + +**What it does:** Reuse vertex buffers instead of allocating new ones per chunk. Reduces allocations and GC. + +**Impact:** Can improve frame times by tens of percent in allocation-heavy setups. + +**Hytopia status:** New geometry per batch. No pooling. + +**Recommendation:** Lower priority. Consider if profiling shows allocation/GC as a bottleneck. + +--- + +## 8. Server-Side Optimizations (Already Addressed) + +- **View distance:** Reduced default, `/view` command +- **Chunk load/unload:** With grace period +- **Prioritize by view direction:** Load chunks in front first +- **Unload distant chunks:** Keeps memory bounded + +--- + +## Prioritized Implementation Plan + +| Priority | Technique | Impact | Complexity | Effort | +|----------|-----------|--------|------------|--------| +| 1 | **Greedy meshing** | High | Medium | 2–3 days | +| 2 | **LOD for distant chunks** | Medium | Medium | 1–2 days | +| 3 | **Occlusion / cave culling** | Medium | High | 3+ days | +| 4 | **Block/face limit cap** | Low (safety) | Low | <1 day | +| 5 | **Vertex pooling** | Low–Medium | Medium | 1–2 days | + +--- + +## Greedy Meshing Implementation Sketch + +For `ChunkWorker._createChunkBatchGeometries`: + +1. **Current flow:** Per block → per face → if visible → emit quad. +2. **New flow (opaque solids):** + - Collect visible faces with (normal, blockTypeId, textureUri, AO, light) as keys + - For each direction (±X, ±Y, ±Z), build a 2D grid of visible faces + - Run greedy merge per slice (0fps algorithm) + - Emit merged quads instead of per-face quads +3. **Transparent blocks:** Can stay as-is (per-face) or use a separate greedy pass with transparency grouping. +4. **Trimesh blocks:** Keep current logic (no greedy). + +**References:** +- [0fps Part 1](https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/) +- [0fps Part 2 (multiple types)](https://0fps.net/2012/07/07/meshing-minecraft-part-2/) +- [mikolalysenko/greedy-mesher](https://github.com/mikolalysenko/greedy-mesher) (JS) +- [Vercidium greedy voxel meshing gist](https://gist.github.com/Vercidium/a3002bd083cce2bc854c9ff8f0118d33) + +--- + +## Other Considerations + +- **Runs-based meshing:** Alternative to full greedy; ~20% more triangles but ~4× faster build. Good compromise. +- **GPU-driven rendering:** Modern engines use compute shaders for mesh generation. WebGL limits this; workers are the main option. +- **Chunk size:** Hytopia uses 16³ chunks and 2×2×2 batches (32³). Matches common practice. + +--- + +## Implemented (Hytopia) + +- **LOD:** Distant chunks use step 2 or 4 (half/quarter detail). Underground batches get +1 LOD. +- **Block/face limits:** When total faces > 800K, view distance shrinks to 25% and occlusion runs. +- **Vertex pooling:** Mesh updates reuse existing BufferAttributes when size matches (avoids GPU realloc). +- **Occlusion culling:** BFS from camera through air/liquid; only visible batches rendered when over face limit. +- **Underground LOD:** Batches below Y=40 use one extra LOD step (reduces cave geometry; partial greedy benefit). + +## Conclusion + +The largest missing optimization is **full greedy meshing** (quad merging). Face culling is in place, but merging adjacent same-type faces into larger quads can cut vertex/quad count by roughly 2–10× depending on geometry, which directly reduces GPU work and often improves smoothness when flying. + +LOD and occlusion culling are useful next steps; block limits and vertex pooling are refinements for later. diff --git a/ai-memory/feature/perf-external-notes-verification-20260305-2249094/decisions.md b/ai-memory/feature/perf-external-notes-verification-20260305-2249094/decisions.md new file mode 100644 index 00000000..64853c2d --- /dev/null +++ b/ai-memory/feature/perf-external-notes-verification-20260305-2249094/decisions.md @@ -0,0 +1,13 @@ +# Decisions + +## Game SDK Compatibility — RESOLVED +Both games had API breaks with our local SDK build. Fixed by adding missing APIs: +- **HyFire2**: Restored map compression codecs (WorldMapCodec, WorldMapChunkCacheCodec, WorldMapFileLoader, WorldMapArtifacts) from `feature/map-compression` branch. Also updated World.loadMap() to accept compressed formats. +- **Zoo Game**: Added missing Entity methods: `setModelAnimationsPlaybackRate`, `startModelLoopedAnimations`, `startModelOneshotAnimations`, `setModelNodeEmissiveColor`, `setModelNodeEmissiveIntensity`. +- Both games also needed `@fails-components/webtransport` installed locally (our SDK marks it as external). +- Both now run with full PerfHarness via `npm link hytopia`. + +## ProcessMonitor Design +Initially monitored single PID. But `spawn(cmd, {shell:true, detached:true})` creates a shell +process — the actual node server is a child. Fixed by scanning `/proc` for all PIDs in the +same process group (PGID) and aggregating CPU/RSS/threads/FDs across all of them. diff --git a/ai-memory/feature/perf-external-notes-verification-20260305-2249094/init.md b/ai-memory/feature/perf-external-notes-verification-20260305-2249094/init.md new file mode 100644 index 00000000..8c32d9e1 --- /dev/null +++ b/ai-memory/feature/perf-external-notes-verification-20260305-2249094/init.md @@ -0,0 +1,4 @@ +# Real Game Benchmarking + OS-Level Monitoring + +Add OS-level CPU/memory/GC monitoring to benchmark runner (works without PerfHarness). +Benchmark real games (HyFire2 + Zoo Game) against modified SDK. diff --git a/ai-memory/feature/perf-external-notes-verification-20260305-2249094/plan.md b/ai-memory/feature/perf-external-notes-verification-20260305-2249094/plan.md new file mode 100644 index 00000000..f1740040 --- /dev/null +++ b/ai-memory/feature/perf-external-notes-verification-20260305-2249094/plan.md @@ -0,0 +1,12 @@ +# Plan + +1. ProcessMonitor.ts — OS-level CPU/RSS/FD monitoring +2. MetricCollector — add process snapshot support +3. BenchmarkRunner — integrate ProcessMonitor, graceful PerfHarness fallback, log capture +4. ConsoleReporter — display process metrics with CPU% thresholds +5. CLI — new options (--no-perf-api, --log-file) +6. Scripts — link-sdk.sh, setup-game.sh +7. Presets — hyfire2-bots.yaml, zoo-game-bots.yaml +8. HyFire2 setup — npm link, test launch, fix API breaks +9. Zoo game setup — extract loadtest files, npm link, test launch +10. Run benchmarks — both games, native + throttled diff --git a/ai-memory/feature/perf-external-notes-verification-20260305-2249094/progress.md b/ai-memory/feature/perf-external-notes-verification-20260305-2249094/progress.md new file mode 100644 index 00000000..81b78849 --- /dev/null +++ b/ai-memory/feature/perf-external-notes-verification-20260305-2249094/progress.md @@ -0,0 +1,35 @@ +# Progress + +- [x] ProcessMonitor.ts — reads /proc//stat + status for CPU%, RSS, threads, FDs +- [x] ProcessMonitor fix — aggregate all child processes in process group (shell=true+detached) +- [x] MetricCollector — ProcessSnapshotEntry type + addProcessSnapshot method +- [x] BenchmarkRunner — ProcessMonitor integration, PerfHarness fallback, log capture +- [x] ConsoleReporter — process metrics section with CPU% threshold warnings +- [x] CLI — --no-perf-api, --log-file options +- [x] Scripts — link-sdk.sh, setup-game.sh (chmod +x) +- [x] Presets — hyfire2-bots.yaml, zoo-game-bots.yaml +- [x] Build verification — tsc passes clean +- [x] Verified: idle benchmark — CPU avg=1.3%, RSS 168MB, 12 threads, 37 FDs +- [x] Verified: stress benchmark — CPU avg=2.9% max=13%, RSS 196MB +- [x] Verified: --no-perf-api mode — OS-only monitoring works +- [x] Verified: --log-file option — server output captured to file +- [x] HyFire2 — restored map compression codecs from feature/map-compression branch +- [x] Zoo game — added missing Entity methods (setModelAnimationsPlaybackRate, startModelLoopedAnimations, startModelOneshotAnimations, setModelNodeEmissiveColor, setModelNodeEmissiveIntensity) +- [x] HyFire2 benchmark — PASS: avg tick 0.61ms, p99 1.34ms, 431MB heap, 1.2GB RSS +- [x] Zoo Game benchmark (full PerfHarness) — PASS: avg tick 0.25ms, p99 0.85ms, 313MB heap, 782MB RSS +- [x] Client PerfBridge — window.__HYTOPIA_PERF__.snapshot() exposes FPS, draw calls, triangles, entities, chunks, GLTF, memory +- [x] HeadlessClient updated — snapshot(), waitForPerfReady(), Chrome flags, auto ?perf=1&join= +- [x] BenchmarkRunner — HeadlessClient lifecycle + client metrics polling +- [x] ConsoleReporter — rich client section (FPS, draw calls, triangles, entities, chunks, heap) +- [x] Client thresholds — fps_min, fps_avg, draw_calls_max, triangles_max, frame_time_ms_max +- [x] CLI — --with-client, --client-dev-url flags +- [x] HeadlessClient fix — waitUntil 'load' instead of 'networkidle2' (game keeps persistent connections) +- [x] BenchmarkRunner — graceful HeadlessClient error handling (try-catch around launch) +- [x] Verified: client metrics pipeline — FPS avg=22.6, draw calls avg=11, triangles avg=22, JS heap avg=22.9MB (idle scene, no entities/chunks in headless) +- [x] Fixed: HeadlessClient ignoreHTTPSErrors + CDP Security.setIgnoreCertificateErrors +- [x] Fixed: Patched fetch() to strip unsupported targetAddressSpace (Chrome PNA API) +- [x] Fixed: warmCert step to pre-accept self-signed HTTPS cert +- [x] Fixed: Replaced --disable-gpu with --use-gl=swiftshader for WebGL rendering +- [x] Fixed: BaselineComparer operations null-safety + loadBaseline nested format support +- [x] A/B Benchmark PR #2 (blob shadows) — COMPLETED with real client metrics +- [x] All pushed to origin diff --git a/ai-memory/feature/perf-notes-external-review-24a295d/init.md b/ai-memory/feature/perf-notes-external-review-24a295d/init.md new file mode 100644 index 00000000..4a48755b --- /dev/null +++ b/ai-memory/feature/perf-notes-external-review-24a295d/init.md @@ -0,0 +1,19 @@ +# Performance Framework Research - Init + +## Request +Research all performance-related code/branches in HyFire2 repo and HYTOPIA SDK to create a comprehensive performance benchmarking framework for HYTOPIA. + +## Scope +- HyFire2 performance branches (100+ branches identified) +- HYTOPIA SDK server performance (Telemetry, WorldLoop, NetworkSynchronizer) +- HYTOPIA client performance (PerformanceMetricsManager, ChunkStats, EntityStats) +- Network monitoring, device profiling, headless testing +- Repeatable benchmarks, stress testing, game scenario simulation +- F12/DevTools performance log parsing +- Cross-device testing + +## Key Locations +- HyFire2: ~/GitHub/games/hyfire2 (100+ perf branches) +- HYTOPIA SDK: /home/ab/GitHub/hytopia/work1/server/src/ +- HYTOPIA Client: /home/ab/GitHub/hytopia/work1/client/src/ +- Protocol: /home/ab/GitHub/hytopia/work1/protocol/ diff --git a/ai-memory/feature/perf-notes-external-review-24a295d/progress.md b/ai-memory/feature/perf-notes-external-review-24a295d/progress.md new file mode 100644 index 00000000..e4125fb3 --- /dev/null +++ b/ai-memory/feature/perf-notes-external-review-24a295d/progress.md @@ -0,0 +1,43 @@ +# Progress + +- [x] Set up ai-memory directory +- [x] Survey all HyFire2 locations and branches (100+ perf branches found) +- [x] Read HYTOPIA SDK CODEBASE_DOCUMENTATION.md +- [x] Launch 6 parallel research agents + - [x] Agent 1: HyFire2 analysis/docs branches (11 branches) → hyfire2-perf-analysis-branches.md + - [x] Agent 2: HyFire2 master branch perf code → hyfire2-master-perf-code.md + - [x] Agent 3: HyFire2 feature perf branches (18 branches) → hyfire2-feature-perf-branches.md + - [x] Agent 4: HyFire2 perf/* and fix/*performance* branches (27 branches) → hyfire2-perf-fix-branches.md + - [x] Agent 5: HYTOPIA SDK perf infrastructure (server + client + protocol) → hytopia-sdk-perf-systems.md + - [x] Agent 6: Headless/automation testing branches (15+ branches) → headless-automation-research.md +- [x] Recover 3 research files dropped by parallel git conflicts +- [x] Push all research to feature/perf-notes-external-review +- [x] Synthesis agent consolidated into SYNTHESIS-perf-framework-spec.md (965 lines) +- [x] Quality review of synthesis document + +## Phase 1: SDK Performance Module Implementation +- [x] PerformanceMonitor.ts - core singleton profiler (CircularBuffer, percentiles, spike detection) +- [x] Monitor.ts - @Monitor decorator, @MonitorClass, monitorBlock, monitorAsyncBlock +- [x] NetworkMetrics.ts - bandwidth/packet/serialization tracking +- [x] CpuProfiler.ts - V8 Inspector CPU profile + heap snapshot capture +- [x] WorldLoop.ts integration - beginTick/recordPhase/endTick per tick +- [x] EntityManager.ts integration - opt-in per-entity profiling +- [x] Telemetry.ts integration - dual-path (Sentry + PerformanceMonitor) +- [x] index.ts - all new exports added + +## Phase 2: Bot System +- [x] BotPlayer.ts, BotManager.ts, 4 behaviors (Idle/RandomWalk/Chase/Interact) + +## Phase 3: Benchmark Runner CLI (packages/perf-tools/) +- [x] ScenarioLoader, BenchmarkRunner, MetricCollector, HeadlessClient, BaselineComparer +- [x] ConsoleReporter, JsonReporter, CLI entry point, 5 YAML presets + +## Phase 4: Trace Analysis Tools +- [x] TraceParser, CpuProfileAnalyzer, SpikeCorrelator, NoiseFilter + +## Phase 5: CI/CD +- [x] perf-gate.yml, perf-baseline-update.yml + +## Verification +- [x] SDK build passes (tsc + api-extractor) +- [x] SDK size increase: ~13KB diff --git a/client/src/Game.ts b/client/src/Game.ts index f420af88..d0a2634c 100644 --- a/client/src/Game.ts +++ b/client/src/Game.ts @@ -23,12 +23,14 @@ import Renderer from './core/Renderer'; import SettingsManager from './settings/SettingsManager'; import UIManager from './ui/UIManager'; import ChunkWorkerClient from './workers/ChunkWorkerClient'; +import PerfBridge from './core/PerfBridge'; const DEBUG_QUERY_STRINGS = 'debug'; export default class Game { private static _instance: Game | undefined; readonly inDebugMode = new URLSearchParams(window.location.search).has(DEBUG_QUERY_STRINGS); + readonly inPerfMode = new URLSearchParams(window.location.search).get('perf') === '1'; private _arrowManager: ArrowManager; private _audioManager: AudioManager; @@ -66,6 +68,10 @@ export default class Game { this._renderer = new Renderer(this); this._chunkWorkerClient = new ChunkWorkerClient(); + if (this.inPerfMode) { + new PerfBridge(this); + } + this._arrowManager = new ArrowManager(this); this._audioManager = new AudioManager(this); this._blockTextureAtlasManager = new BlockTextureAtlasManager(this); diff --git a/client/src/core/PerfBridge.ts b/client/src/core/PerfBridge.ts new file mode 100644 index 00000000..0d3cfe18 --- /dev/null +++ b/client/src/core/PerfBridge.ts @@ -0,0 +1,66 @@ +import EntityStats from '../entities/EntityStats'; +import ChunkStats from '../chunks/ChunkStats'; +import GLTFStats from '../gltf/GLTFStats'; +import type Game from '../Game'; + +export default class PerfBridge { + private _game: Game; + + constructor(game: Game) { + this._game = game; + + const self = this; + const perf: any = { + snapshot: () => self._snapshot(), + get fps() { return game.performanceMetricsManager.fps; }, + get frameTimeMs() { return game.performanceMetricsManager.deltaTime * 1000; }, + get drawCalls() { return game.renderer.webGLRenderer.info.render.calls; }, + get triangles() { return game.renderer.webGLRenderer.info.render.triangles; }, + get textureMemoryMb() { return 0; }, + }; + + (window as any).__HYTOPIA_PERF__ = perf; + + // Expose game instance for headless client control (camera, input) + (window as any).__HYTOPIA_GAME__ = game; + } + + private _snapshot() { + const perf = this._game.performanceMetricsManager; + const info = this._game.renderer.webGLRenderer.info; + + return { + fps: perf.fps, + frameTimeMs: perf.deltaTime * 1000, + drawCalls: info.render.calls, + triangles: info.render.triangles, + geometries: info.memory.geometries, + textures: info.memory.textures, + programs: (info as any).programs?.length ?? 0, + textureMemoryMb: 0, + usedMemoryMb: perf.usedMemory / (1024 * 1024), + totalMemoryMb: perf.totalMemory / (1024 * 1024), + entities: { + count: EntityStats.count, + inViewDistance: EntityStats.inViewDistanceCount, + frustumCulled: EntityStats.frustumCulledCount, + staticEnvironment: EntityStats.staticEnvironmentCount, + }, + chunks: { + count: ChunkStats.count, + visible: ChunkStats.visibleCount, + blocks: ChunkStats.blockCount, + opaqueFaces: ChunkStats.opaqueFaceCount, + transparentFaces: ChunkStats.transparentFaceCount, + liquidFaces: ChunkStats.liquidFaceCount, + }, + gltf: { + files: GLTFStats.fileCount, + sourceMeshes: GLTFStats.sourceMeshCount, + clonedMeshes: GLTFStats.clonedMeshCount, + instancedMeshes: GLTFStats.instancedMeshCount, + drawCallsSaved: GLTFStats.drawCallsSaved, + }, + }; + } +} diff --git a/client/src/network/NetworkManager.ts b/client/src/network/NetworkManager.ts index 361281ef..4599af68 100644 --- a/client/src/network/NetworkManager.ts +++ b/client/src/network/NetworkManager.ts @@ -572,7 +572,7 @@ export default class NetworkManager { private async _reconnect(): Promise { // Check if server is still up - if not, it's an unexpected disconnect (crash) - const serverHealthy = await Servers.isCurrentServerHealthy().catch(() => false); + await Servers.isCurrentServerHealthy().catch(() => false); const url = new URL(window.location.href); @@ -593,4 +593,4 @@ export default class NetworkManager { this._syncStartTimeS = performance.now() / 1000; this.sendPacket(protocol.createPacket(protocol.syncRequestPacketDefinition, null)); } -} \ No newline at end of file +} diff --git a/packages/perf-tools/overlays/legacy-server/PerfHarness.ts b/packages/perf-tools/overlays/legacy-server/PerfHarness.ts new file mode 100644 index 00000000..06a3dc1d --- /dev/null +++ b/packages/perf-tools/overlays/legacy-server/PerfHarness.ts @@ -0,0 +1,263 @@ +import type http from 'http'; +import PerformanceBaseline from '@/metrics/PerformanceBaseline'; +import Telemetry from '@/metrics/Telemetry'; +import PlayerManager from '@/players/PlayerManager'; + +const PERF_PREFIX = '/__perf'; +const DEFAULT_BUDGET_MS = 1000 / 60; +const MAX_BODY_BYTES = 1024 * 1024; + +type LegacyStatsWindowSnapshot = { + average: number; + count: number; + max: number; + min: number; + p50: number; + p95: number; + sampleCount: number; +}; + +type LegacyPerformanceBaselineSnapshot = { + generatedAt: string; + packets?: { + batches?: { + compressedBatches?: number; + rawBytes?: number; + reliableBatches?: number; + totalBatches?: number; + unreliableBatches?: number; + wireBytes?: number; + }; + families?: Record; + }; + spans?: Record; +}; + +let previousNetworkSample: + | { timestamp: number; bytesSentTotal: number; packetsSentTotal: number } + | undefined; + +function isPerfToolsEnabled(): boolean { + const value = process.env.HYTOPIA_PERF_TOOLS; + + return value === '1' || value === 'true'; +} + +function isAuthorized(req: http.IncomingMessage): boolean { + const token = process.env.HYTOPIA_PERF_TOOLS_TOKEN; + + if (!token) { + return true; + } + + return req.headers['x-hytopia-perf-token'] === token; +} + +function respondJson(res: http.ServerResponse, status: number, body: unknown): void { + res.writeHead(status, { + 'content-type': 'application/json', + 'cache-control': 'no-store', + 'access-control-allow-origin': '*', + }); + res.end(JSON.stringify(body)); +} + +function drainBody(req: http.IncomingMessage, onDone: () => void): void { + let received = 0; + + req.on('data', chunk => { + received += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(String(chunk)); + + if (received > MAX_BODY_BYTES) { + req.destroy(); + } + }); + + req.on('error', onDone); + req.on('end', onDone); +} + +function findTickWindow( + spans: Record, +): LegacyStatsWindowSnapshot | undefined { + const preferred = ['world_tick', 'ticker_tick']; + + for (const name of preferred) { + if (spans[name]) { + return spans[name]; + } + } + + for (const [name, snapshot] of Object.entries(spans)) { + if (name.includes('tick')) { + return snapshot; + } + } + + return undefined; +} + +function sumLegacyPacketCount(snapshot: LegacyPerformanceBaselineSnapshot): number { + return Object.values(snapshot.packets?.families ?? {}).reduce((total, family) => { + return total + (family.packetCount ?? 0); + }, 0); +} + +function getSerializationAverageMs( + spans: Record, +): number { + return spans.serialize_packets?.average + ?? spans.serialize_packets_encode?.average + ?? 0; +} + +function toOperationSnapshot(snapshot: LegacyStatsWindowSnapshot) { + return { + count: snapshot.count || snapshot.sampleCount || 0, + avgMs: snapshot.average || 0, + p95Ms: snapshot.p95 || 0, + p99Ms: snapshot.max || snapshot.p95 || 0, + maxMs: snapshot.max || 0, + }; +} + +function toModernSnapshot( + legacySnapshot: LegacyPerformanceBaselineSnapshot, + processStats: Record, + playerCount: number, +) { + const generatedAtMs = Date.parse(legacySnapshot.generatedAt); + const timestamp = Number.isFinite(generatedAtMs) ? generatedAtMs : Date.now(); + const spans = legacySnapshot.spans ?? {}; + const tickWindow = findTickWindow(spans); + const bytesSentTotal = legacySnapshot.packets?.batches?.wireBytes ?? 0; + const packetsSentTotal = sumLegacyPacketCount(legacySnapshot); + let bytesSentPerSecond = 0; + let packetsSentPerSecond = 0; + + if (previousNetworkSample && timestamp > previousNetworkSample.timestamp) { + const elapsedSeconds = (timestamp - previousNetworkSample.timestamp) / 1000; + + if (elapsedSeconds > 0) { + bytesSentPerSecond = Math.max(0, (bytesSentTotal - previousNetworkSample.bytesSentTotal) / elapsedSeconds); + packetsSentPerSecond = Math.max(0, (packetsSentTotal - previousNetworkSample.packetsSentTotal) / elapsedSeconds); + } + } + + previousNetworkSample = { + timestamp, + bytesSentTotal, + packetsSentTotal, + }; + + return { + source: 'legacy_perf_api', + timestamp, + avgTickMs: tickWindow?.average ?? 0, + maxTickMs: tickWindow?.max ?? 0, + p95TickMs: tickWindow?.p95 ?? 0, + p99TickMs: tickWindow?.max ?? tickWindow?.p95 ?? 0, + ticksOverBudget: 0, + totalTicks: tickWindow?.count ?? tickWindow?.sampleCount ?? 0, + budgetMs: DEFAULT_BUDGET_MS, + operations: Object.fromEntries( + Object.entries(spans).map(([name, snapshot]) => [name, toOperationSnapshot(snapshot)]), + ), + memory: { + heapUsedMb: Number(processStats.jsHeapSizeMb ?? processStats.processHeapSizeMb ?? 0), + heapTotalMb: Number(processStats.jsHeapCapacityMb ?? 0), + rssMb: Number(processStats.rssSizeMb ?? 0), + }, + network: { + connectedPlayers: playerCount, + bytesSentTotal, + bytesReceivedTotal: 0, + bytesSentPerSecond, + bytesReceivedPerSecond: 0, + packetsSentPerSecond, + packetsReceivedPerSecond: 0, + avgSerializationMs: getSerializationAverageMs(spans), + compressionCount: legacySnapshot.packets?.batches?.compressedBatches ?? 0, + }, + }; +} + +export default class PerfHarness { + public static enableIfConfigured(): void {} + + public static handleWebRequest(req: http.IncomingMessage, res: http.ServerResponse): boolean { + if (!isPerfToolsEnabled()) { + return false; + } + + const reqPath = req.url?.split('?')[0] ?? '/'; + + if (!reqPath.startsWith(PERF_PREFIX)) { + return false; + } + + if (!isAuthorized(req)) { + respondJson(res, 401, { ok: false, error: 'Unauthorized' }); + return true; + } + + if (req.method === 'OPTIONS') { + res.writeHead(204, { + 'access-control-allow-origin': '*', + 'access-control-allow-methods': 'GET,POST,DELETE,OPTIONS', + 'access-control-allow-headers': 'content-type,x-hytopia-perf-token', + }); + res.end(); + return true; + } + + if ((reqPath === `${PERF_PREFIX}/reset`) && (req.method === 'POST' || req.method === 'DELETE')) { + previousNetworkSample = undefined; + PerformanceBaseline.reset(); + res.writeHead(204, { 'access-control-allow-origin': '*' }); + res.end(); + return true; + } + + if ((reqPath === `${PERF_PREFIX}/snapshot` || reqPath === PERF_PREFIX) && req.method === 'GET') { + const processStats = Telemetry.getProcessStats() as Record; + const legacySnapshot = PerformanceBaseline.snapshot() as LegacyPerformanceBaselineSnapshot; + + if (reqPath === PERF_PREFIX) { + respondJson(res, 200, { + playerCount: PlayerManager.instance.playerCount, + process: processStats, + snapshot: legacySnapshot, + }); + } else { + respondJson(res, 200, toModernSnapshot( + legacySnapshot, + processStats, + PlayerManager.instance.playerCount, + )); + } + + return true; + } + + if (reqPath === `${PERF_PREFIX}/action` && req.method === 'POST') { + drainBody(req, () => { + respondJson(res, 501, { + ok: false, + error: 'Perf actions are not available on this legacy overlay target.', + }); + }); + return true; + } + + respondJson(res, 404, { ok: false, error: 'Perf endpoint not found' }); + return true; + } +} diff --git a/packages/perf-tools/overlays/minimal-server/PerfHarness.ts b/packages/perf-tools/overlays/minimal-server/PerfHarness.ts new file mode 100644 index 00000000..7d39fc8e --- /dev/null +++ b/packages/perf-tools/overlays/minimal-server/PerfHarness.ts @@ -0,0 +1,136 @@ +import type http from 'http'; +import ErrorHandler from '@/errors/ErrorHandler'; +import NetworkMetrics from '@/metrics/NetworkMetrics'; +import PerformanceMonitor from '@/metrics/PerformanceMonitor'; + +const PERF_PREFIX = '/__perf'; +const MAX_BODY_BYTES = 1024 * 1024; + +function isPerfToolsEnabled(): boolean { + const value = process.env.HYTOPIA_PERF_TOOLS; + + return value === '1' || value === 'true'; +} + +function isAuthorized(req: http.IncomingMessage): boolean { + const token = process.env.HYTOPIA_PERF_TOOLS_TOKEN; + + if (!token) { + return true; + } + + return req.headers['x-hytopia-perf-token'] === token; +} + +function respondJson(res: http.ServerResponse, status: number, body: unknown): void { + res.writeHead(status, { + 'content-type': 'application/json', + 'cache-control': 'no-store', + 'access-control-allow-origin': '*', + }); + res.end(JSON.stringify(body)); +} + +function drainBody(req: http.IncomingMessage, onDone: () => void): void { + let received = 0; + + req.on('data', chunk => { + received += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(String(chunk)); + + if (received > MAX_BODY_BYTES) { + req.destroy(); + } + }); + + req.on('error', onDone); + req.on('end', onDone); +} + +export default class PerfHarness { + public static enableIfConfigured(): void { + if (!isPerfToolsEnabled()) { + return; + } + + try { + if (!PerformanceMonitor.instance.isEnabled) { + PerformanceMonitor.instance.enable({ snapshotIntervalMs: 0 }); + } + + if (!NetworkMetrics.instance.isEnabled) { + NetworkMetrics.instance.enable(); + } + } catch (error) { + ErrorHandler.warning(`PerfHarness.enableIfConfigured(): Failed to enable perf tools. Error: ${String(error)}`); + } + } + + public static handleWebRequest(req: http.IncomingMessage, res: http.ServerResponse): boolean { + if (!isPerfToolsEnabled()) { + return false; + } + + const reqPath = req.url?.split('?')[0] ?? '/'; + + if (!reqPath.startsWith(PERF_PREFIX)) { + return false; + } + + if (!isAuthorized(req)) { + respondJson(res, 401, { ok: false, error: 'Unauthorized' }); + return true; + } + + if (req.method === 'OPTIONS') { + res.writeHead(204, { + 'access-control-allow-origin': '*', + 'access-control-allow-methods': 'GET,POST,DELETE,OPTIONS', + 'access-control-allow-headers': 'content-type,x-hytopia-perf-token', + }); + res.end(); + return true; + } + + if ((reqPath === `${PERF_PREFIX}/reset`) && (req.method === 'POST' || req.method === 'DELETE')) { + PerformanceMonitor.instance.resetStats(); + NetworkMetrics.instance.reset(); + res.writeHead(204, { 'access-control-allow-origin': '*' }); + res.end(); + return true; + } + + if ((reqPath === `${PERF_PREFIX}/snapshot` || reqPath === PERF_PREFIX) && req.method === 'GET') { + const snapshot = PerformanceMonitor.instance.getSnapshot(); + const network = NetworkMetrics.instance.getSnapshot(); + + respondJson(res, 200, { + source: 'perf_harness', + timestamp: Date.now(), + avgTickMs: snapshot.avgTickMs, + maxTickMs: snapshot.maxTickMs, + p95TickMs: snapshot.p95TickMs, + p99TickMs: snapshot.p99TickMs, + ticksOverBudget: snapshot.ticksOverBudget, + totalTicks: snapshot.totalTicks, + budgetMs: snapshot.budgetMs, + operations: snapshot.operations, + memory: snapshot.memory, + network, + }); + return true; + } + + if (reqPath === `${PERF_PREFIX}/action` && req.method === 'POST') { + drainBody(req, () => { + respondJson(res, 501, { + ok: false, + error: 'Perf actions are not available on this overlay target.', + }); + }); + return true; + } + + respondJson(res, 404, { ok: false, error: 'Perf endpoint not found' }); + return true; + } +} diff --git a/packages/perf-tools/package-lock.json b/packages/perf-tools/package-lock.json new file mode 100644 index 00000000..d2395e0c --- /dev/null +++ b/packages/perf-tools/package-lock.json @@ -0,0 +1,1286 @@ +{ + "name": "@hytopia/perf-tools", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@hytopia/perf-tools", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "commander": "^12.0.0", + "js-yaml": "^4.1.0", + "puppeteer": "^23.0.0", + "ws": "^8.18.3" + }, + "bin": { + "hytopia-bench": "dist/cli.js" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.0.0", + "@types/ws": "^8.18.1", + "typescript": "^5.4.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "hytopia": "*" + }, + "peerDependenciesMeta": { + "hytopia": { + "optional": true + } + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.1.tgz", + "integrity": "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.0", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", + "integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz", + "integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.7.1.tgz", + "integrity": "sha512-ebvMaS5BgZKmJlvuWh14dg9rbUI84QeV3WlWn6Ph6lFI8jJoh7ADtVTyD2c93euwbe+zgi0DVrl4YmqXeM9aIA==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.0.tgz", + "integrity": "sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==", + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.21.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.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/basic-ftp": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "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-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chromium-bidi": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.11.0.tgz", + "integrity": "sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "3.0.1", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "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/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1367902", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz", + "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "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/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "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/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "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/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "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/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "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/puppeteer": { + "version": "23.11.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-23.11.1.tgz", + "integrity": "sha512-53uIX3KR5en8l7Vd8n5DUv90Ae9QDQsyIthaUFVzwV6yU750RjqRznEtNMBT20VthqAdemnJN+hxVdmMHKt7Zw==", + "deprecated": "< 24.15.0 is no longer supported", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.6.1", + "chromium-bidi": "0.11.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1367902", + "puppeteer-core": "23.11.1", + "typed-query-selector": "^2.12.0" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "23.11.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.11.1.tgz", + "integrity": "sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.6.1", + "chromium-bidi": "0.11.0", + "debug": "^4.4.0", + "devtools-protocol": "0.0.1367902", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "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==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typed-query-selector": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz", + "integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "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" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "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 + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/packages/perf-tools/package.json b/packages/perf-tools/package.json new file mode 100644 index 00000000..abafc8e1 --- /dev/null +++ b/packages/perf-tools/package.json @@ -0,0 +1,41 @@ +{ + "name": "@hytopia/perf-tools", + "version": "0.1.0", + "description": "Performance benchmarking and analysis tools for HYTOPIA games", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "bin": { + "hytopia-bench": "dist/cli.js" + }, + "scripts": { + "build": "tsc && node scripts/copy-assets.mjs", + "dev": "tsc --watch", + "lint": "tsc --noEmit" + }, + "dependencies": { + "js-yaml": "^4.1.0", + "puppeteer": "^23.0.0", + "commander": "^12.0.0", + "chalk": "^5.3.0", + "ws": "^8.18.3" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.0.0", + "@types/ws": "^8.18.1", + "typescript": "^5.4.0" + }, + "peerDependencies": { + "hytopia": "*" + }, + "peerDependenciesMeta": { + "hytopia": { + "optional": true + } + }, + "engines": { + "node": ">=18.0.0" + }, + "license": "MIT" +} diff --git a/packages/perf-tools/perf-results/blocks-10m-dense.json b/packages/perf-tools/perf-results/blocks-10m-dense.json new file mode 100644 index 00000000..a42f9a10 --- /dev/null +++ b/packages/perf-tools/perf-results/blocks-10m-dense.json @@ -0,0 +1,101 @@ +{ + "timestamp": "2026-03-05T13:47:23.411Z", + "scenario": "blocks-10m-dense", + "durationMs": 77309, + "baseline": { + "avgTickMs": 0.16117925346605497, + "maxTickMs": 87.1524989999998, + "p95TickMs": 0.06417813333331045, + "p99TickMs": 1.5407672000005428, + "ticksOverBudgetPct": 0.054851124905153266, + "avgMemoryMb": 55.660138575236004, + "operations": { + "serialize_packets": { + "avgMs": 33.65801499999907, + "p95Ms": 66.72433100000126 + }, + "send_packets": { + "avgMs": 37.353925499999605, + "p95Ms": 73.78263700000025 + }, + "entities_tick": { + "avgMs": 0.0009822978624383662, + "p95Ms": 0.0014590499999030727 + }, + "physics_step": { + "avgMs": 0.01972773740296246, + "p95Ms": 0.03086626666554366 + }, + "physics_cleanup": { + "avgMs": 0.002075294411069032, + "p95Ms": 0.0030863499998304176 + }, + "simulation_step": { + "avgMs": 0.024010915136226423, + "p95Ms": 0.03764851666649823 + }, + "entities_emit_updates": { + "avgMs": 0.00041965276066853566, + "p95Ms": 0.0006702166658821322 + }, + "send_all_packets": { + "avgMs": 0.21162622516955648, + "p95Ms": 0.00613846666644046 + }, + "network_synchronize_cleanup": { + "avgMs": 0.002881797564949829, + "p95Ms": 0.0038139000006898035 + }, + "network_synchronize": { + "avgMs": 0.2561948408059067, + "p95Ms": 0.017406783333050648 + }, + "world_tick": { + "avgMs": 0.15971886314477954, + "p95Ms": 0.05990831666737601 + }, + "ticker_tick": { + "avgMs": 0.19212393717690157, + "p95Ms": 0.1026831833337686 + } + }, + "network": { + "totalBytesSent": 57474, + "totalBytesReceived": 0, + "maxConnectedPlayers": 1, + "avgBytesSentPerSecond": 949.4818163586549, + "maxBytesSentPerSecond": 56968.90898151929, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 0.0991211834595109, + "maxPacketsSentPerSecond": 5.947271007570654, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 33.62722399999984, + "compressionCountTotal": 1 + } + }, + "phases": [ + { + "name": "setup-world", + "durationMs": 16, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 13686, + "collected": false + }, + { + "name": "join-and-measure", + "durationMs": 57562, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/packages/perf-tools/perf-results/blocks-1m-multi-world.json b/packages/perf-tools/perf-results/blocks-1m-multi-world.json new file mode 100644 index 00000000..c3905e83 --- /dev/null +++ b/packages/perf-tools/perf-results/blocks-1m-multi-world.json @@ -0,0 +1,101 @@ +{ + "timestamp": "2026-03-05T13:48:01.634Z", + "scenario": "blocks-1m-multi-world", + "durationMs": 116725, + "baseline": { + "avgTickMs": 0.03921316894355409, + "maxTickMs": 76.47973099999945, + "p95TickMs": 0.0397304555555618, + "p99TickMs": 0.059530444444761896, + "ticksOverBudgetPct": 0.02738469332566562, + "avgMemoryMb": 43.984555901421444, + "operations": { + "entities_tick": { + "avgMs": 0.0004325694904328935, + "p95Ms": 0.0006662222222480017 + }, + "physics_step": { + "avgMs": 0.009219510851331304, + "p95Ms": 0.01768245555568784 + }, + "physics_cleanup": { + "avgMs": 0.0009630207720016963, + "p95Ms": 0.0015721999998883499 + }, + "simulation_step": { + "avgMs": 0.011594284617616978, + "p95Ms": 0.021482744445772067 + }, + "entities_emit_updates": { + "avgMs": 0.0001926413783360981, + "p95Ms": 0.00031212222214283735 + }, + "world_tick": { + "avgMs": 0.04490596976384134, + "p95Ms": 0.036641233332961244 + }, + "ticker_tick": { + "avgMs": 0.061596693273831084, + "p95Ms": 0.06615804444477867 + }, + "serialize_packets": { + "avgMs": 2.617033107142953, + "p95Ms": 5.835788000000321 + }, + "send_packets": { + "avgMs": 2.2704686874998288, + "p95Ms": 6.619578999998339 + }, + "send_all_packets": { + "avgMs": 0.046972241007496744, + "p95Ms": 0.004221966667157378 + }, + "network_synchronize_cleanup": { + "avgMs": 0.0013214479156872455, + "p95Ms": 0.0029271222212426235 + }, + "network_synchronize": { + "avgMs": 0.05924993211701668, + "p95Ms": 0.011533511111136048 + } + }, + "network": { + "totalBytesSent": 257844, + "totalBytesReceived": 0, + "maxConnectedPlayers": 40, + "avgBytesSentPerSecond": 171.30217136836123, + "maxBytesSentPerSecond": 15417.19542315251, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 0.20661708356820535, + "maxPacketsSentPerSecond": 18.595537521138482, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 2.614322202380663, + "compressionCountTotal": 40 + } + }, + "phases": [ + { + "name": "setup-worlds", + "durationMs": 17, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 8685, + "collected": false + }, + { + "name": "joins-and-measure", + "durationMs": 101959, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 90, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/packages/perf-tools/perf-results/client-test.json b/packages/perf-tools/perf-results/client-test.json new file mode 100644 index 00000000..95797fe7 --- /dev/null +++ b/packages/perf-tools/perf-results/client-test.json @@ -0,0 +1,110 @@ +{ + "timestamp": "2026-03-06T03:50:19.546Z", + "scenario": "idle-baseline", + "durationMs": 38015, + "baseline": { + "avgTickMs": 0.10509636761701698, + "maxTickMs": 1.5870759999997972, + "p95TickMs": 0.19848203333367564, + "p99TickMs": 0.4574418000003789, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 40.64117304484049, + "avgFps": 22.633333333333333, + "client": { + "avgFps": 22.633333333333333, + "minFps": 3, + "avgFrameTimeMs": 120.36333333353201, + "avgDrawCalls": 10.533333333333333, + "maxDrawCalls": 18, + "avgTriangles": 21.533333333333335, + "maxTriangles": 29, + "avgGeometries": 3, + "avgEntities": 0, + "avgVisibleChunks": 0, + "avgUsedMemoryMb": 22.929159450531007 + }, + "operations": { + "entities_tick": { + "avgMs": 0.002355539196527502, + "p95Ms": 0.00393376666658393 + }, + "physics_step": { + "avgMs": 0.05508578752695078, + "p95Ms": 0.10080239999982345 + }, + "physics_cleanup": { + "avgMs": 0.005681067880410075, + "p95Ms": 0.009226033332864366 + }, + "simulation_step": { + "avgMs": 0.06782869143775062, + "p95Ms": 0.12544263333335645 + }, + "entities_emit_updates": { + "avgMs": 0.0011049729863573338, + "p95Ms": 0.0015884333342304066 + }, + "world_tick": { + "avgMs": 0.10100038014902117, + "p95Ms": 0.19790933333339733 + }, + "ticker_tick": { + "avgMs": 0.16309557437579641, + "p95Ms": 0.3085689999996854 + }, + "send_all_packets": { + "avgMs": 0.009582162449672945, + "p95Ms": 0.013340566666738597 + }, + "network_synchronize_cleanup": { + "avgMs": 0.005140343541354979, + "p95Ms": 0.007651700000678829 + }, + "network_synchronize": { + "avgMs": 0.03168433978267145, + "p95Ms": 0.06539170000044882 + }, + "serialize_packets": { + "avgMs": 0.04629544951281564, + "p95Ms": 0.1200052333334194 + }, + "send_packets": { + "avgMs": 0.22306792835377331, + "p95Ms": 0.353266766666214 + } + }, + "network": { + "totalBytesSent": 863, + "totalBytesReceived": 82, + "maxConnectedPlayers": 1, + "avgBytesSentPerSecond": 24.58973985157189, + "maxBytesSentPerSecond": 49.53961423158536, + "avgBytesReceivedPerSecond": 2.313008765227393, + "maxBytesReceivedPerSecond": 6.799554894531324, + "avgPacketsSentPerSecond": 0.7066455575496745, + "maxPacketsSentPerSecond": 1.9427299698660927, + "avgPacketsReceivedPerSecond": 0.7066455575496745, + "maxPacketsReceivedPerSecond": 1.9427299698660927, + "avgSerializationMs": 0.04293827827864953, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "warmup", + "durationMs": 5001, + "collected": false + }, + { + "name": "measure", + "durationMs": 29588, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 30, + "clientSnapshotCount": 30 + } +} \ No newline at end of file diff --git a/packages/perf-tools/perf-results/combined.json b/packages/perf-tools/perf-results/combined.json new file mode 100644 index 00000000..435e3935 --- /dev/null +++ b/packages/perf-tools/perf-results/combined.json @@ -0,0 +1,106 @@ +{ + "timestamp": "2026-03-05T13:45:43.688Z", + "scenario": "combined-stress", + "durationMs": 146785, + "baseline": { + "avgTickMs": 1.7280708450244475, + "maxTickMs": 123.70900199999596, + "p95TickMs": 1.3665292583329878, + "p99TickMs": 4.8515477750025635, + "ticksOverBudgetPct": 0.8491702029423979, + "avgMemoryMb": 63.49357452392578, + "operations": { + "entities_tick": { + "avgMs": 0.0708545694069393, + "p95Ms": 0.10835589166660307 + }, + "physics_step": { + "avgMs": 1.391738573064377, + "p95Ms": 0.6263938333336228 + }, + "physics_cleanup": { + "avgMs": 0.0032384939332779903, + "p95Ms": 0.004199883332967147 + }, + "simulation_step": { + "avgMs": 1.3982958194776118, + "p95Ms": 0.6345872500000345 + }, + "entities_emit_updates": { + "avgMs": 0.06155322409601348, + "p95Ms": 0.09060742500081082 + }, + "world_tick": { + "avgMs": 1.7484909096397752, + "p95Ms": 1.310201499998766 + }, + "ticker_tick": { + "avgMs": 1.7966917024387188, + "p95Ms": 1.3858299083341081 + }, + "serialize_packets": { + "avgMs": 0.023483883889599252, + "p95Ms": 0.03418720000039078 + }, + "send_packets": { + "avgMs": 0.03744001242885334, + "p95Ms": 0.09914118333338896 + }, + "send_all_packets": { + "avgMs": 0.39571000627088804, + "p95Ms": 0.6249327583315486 + }, + "network_synchronize_cleanup": { + "avgMs": 0.0041347866884364525, + "p95Ms": 0.005549675000141482 + }, + "network_synchronize": { + "avgMs": 0.4210369733809286, + "p95Ms": 0.6538261250005538 + } + }, + "network": { + "totalBytesSent": 32854240, + "totalBytesReceived": 0, + "maxConnectedPlayers": 10, + "avgBytesSentPerSecond": 265666.37092414434, + "maxBytesSentPerSecond": 390981.65550306404, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 294.44562784679397, + "maxPacketsSentPerSecond": 309.40488476594874, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0.021366040567236247, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "load-world", + "durationMs": 6592, + "collected": false + }, + { + "name": "spawn-all", + "durationMs": 743, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 8470, + "collected": false + }, + { + "name": "measure", + "durationMs": 119889, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 120, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/packages/perf-tools/perf-results/hyfire2-bots.json b/packages/perf-tools/perf-results/hyfire2-bots.json new file mode 100644 index 00000000..db980d54 --- /dev/null +++ b/packages/perf-tools/perf-results/hyfire2-bots.json @@ -0,0 +1,83 @@ +{ + "timestamp": "2026-03-06T01:53:07.938Z", + "scenario": "hyfire2-5v5-bots", + "durationMs": 127350, + "baseline": { + "avgTickMs": 0.6116902903423317, + "maxTickMs": 4.948454999997921, + "p95TickMs": 0.9835920000003474, + "p99TickMs": 1.344567283334133, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 431.2618334452311, + "operations": { + "entities_tick": { + "avgMs": 0.12454038554854553, + "p95Ms": 0.2443331750001562 + }, + "physics_step": { + "avgMs": 0.3350736570206461, + "p95Ms": 0.5066266583330313 + }, + "physics_cleanup": { + "avgMs": 0.027295186411969395, + "p95Ms": 0.0715296166665515 + }, + "simulation_step": { + "avgMs": 0.3755244878669753, + "p95Ms": 0.5872315416670669 + }, + "entities_emit_updates": { + "avgMs": 0.060598101468648656, + "p95Ms": 0.12497561666705223 + }, + "world_tick": { + "avgMs": 0.60986325870331, + "p95Ms": 0.9896887249994203 + }, + "ticker_tick": { + "avgMs": 0.7101404462850199, + "p95Ms": 1.1395172916672587 + }, + "send_all_packets": { + "avgMs": 0.010002776939347363, + "p95Ms": 0.046211291667653615 + }, + "network_synchronize_cleanup": { + "avgMs": 0.009550561731046783, + "p95Ms": 0.022571483332770488 + }, + "network_synchronize": { + "avgMs": 0.058481402331304756, + "p95Ms": 0.11953782499943676 + } + }, + "network": { + "totalBytesSent": 0, + "totalBytesReceived": 0, + "maxConnectedPlayers": 0, + "avgBytesSentPerSecond": 0, + "maxBytesSentPerSecond": 0, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 0, + "maxPacketsSentPerSecond": 0, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "gameplay", + "durationMs": 108694, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 120, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/packages/perf-tools/perf-results/join-storm.json b/packages/perf-tools/perf-results/join-storm.json new file mode 100644 index 00000000..cb745ec8 --- /dev/null +++ b/packages/perf-tools/perf-results/join-storm.json @@ -0,0 +1,101 @@ +{ + "timestamp": "2026-03-05T13:44:51.781Z", + "scenario": "join-storm", + "durationMs": 89392, + "baseline": { + "avgTickMs": 4.398019454782057, + "maxTickMs": 1253.5548879999988, + "p95TickMs": 16.257491950000077, + "p99TickMs": 71.5386668666663, + "ticksOverBudgetPct": 3.0303795550392687, + "avgMemoryMb": 54.052302551269534, + "operations": { + "entities_tick": { + "avgMs": 0.0008242613497229731, + "p95Ms": 0.0014116666662327285 + }, + "physics_step": { + "avgMs": 0.13038264330007215, + "p95Ms": 0.1911503000019972 + }, + "physics_cleanup": { + "avgMs": 0.002381567335808261, + "p95Ms": 0.0037820333329364074 + }, + "simulation_step": { + "avgMs": 0.1356069471166377, + "p95Ms": 0.1999653000001975 + }, + "entities_emit_updates": { + "avgMs": 0.0004993073215346867, + "p95Ms": 0.0008000000009512102 + }, + "world_tick": { + "avgMs": 4.443075425890186, + "p95Ms": 13.897077450000506 + }, + "ticker_tick": { + "avgMs": 5.278235663595949, + "p95Ms": 16.3232648166689 + }, + "serialize_packets": { + "avgMs": 16.157164799227957, + "p95Ms": 45.913698000000295 + }, + "send_packets": { + "avgMs": 1.1252793703437065, + "p95Ms": 0.03624300000228686 + }, + "send_all_packets": { + "avgMs": 7.966924639559886, + "p95Ms": 30.552953049999026 + }, + "network_synchronize_cleanup": { + "avgMs": 0.0042317085845608535, + "p95Ms": 0.00900646666668763 + }, + "network_synchronize": { + "avgMs": 8.605954788672712, + "p95Ms": 32.88858343333292 + } + }, + "network": { + "totalBytesSent": 36996784, + "totalBytesReceived": 0, + "maxConnectedPlayers": 100, + "avgBytesSentPerSecond": 46894.35447301847, + "maxBytesSentPerSecond": 2813661.2683811085, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 5.906667234759272, + "maxPacketsSentPerSecond": 354.40003408555634, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 16.153889374517483, + "compressionCountTotal": 100 + } + }, + "phases": [ + { + "name": "preload-world", + "durationMs": 5149, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 8476, + "collected": false + }, + { + "name": "join-and-measure", + "durationMs": 69669, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/packages/perf-tools/perf-results/many-players.json b/packages/perf-tools/perf-results/many-players.json new file mode 100644 index 00000000..6f6609c1 --- /dev/null +++ b/packages/perf-tools/perf-results/many-players.json @@ -0,0 +1,101 @@ +{ + "timestamp": "2026-03-05T13:44:36.247Z", + "scenario": "many-players", + "durationMs": 78465, + "baseline": { + "avgTickMs": 0.9632497683267228, + "maxTickMs": 9.146430999997392, + "p95TickMs": 2.5837638333334327, + "p99TickMs": 3.5789994000003085, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 48.73876393636068, + "operations": { + "entities_tick": { + "avgMs": 0.060091419729391554, + "p95Ms": 0.10528306666619755 + }, + "physics_step": { + "avgMs": 0.04613177805421052, + "p95Ms": 0.06573823333195226 + }, + "physics_cleanup": { + "avgMs": 0.0029735286044631827, + "p95Ms": 0.004278350000034455 + }, + "simulation_step": { + "avgMs": 0.0518406738490113, + "p95Ms": 0.07411623333373427 + }, + "entities_emit_updates": { + "avgMs": 0.04731222575373152, + "p95Ms": 0.0767718000009457 + }, + "serialize_packets": { + "avgMs": 0.025321724149494487, + "p95Ms": 0.04465421666639789 + }, + "send_packets": { + "avgMs": 0.03014869914520092, + "p95Ms": 0.06580306666583055 + }, + "send_all_packets": { + "avgMs": 1.5563268080091588, + "p95Ms": 2.601990399999583 + }, + "network_synchronize_cleanup": { + "avgMs": 0.005281785766588462, + "p95Ms": 0.0080505999999635 + }, + "network_synchronize": { + "avgMs": 1.5889405909843992, + "p95Ms": 2.64567691666701 + }, + "world_tick": { + "avgMs": 0.9611789836723366, + "p95Ms": 2.437558566666788 + }, + "ticker_tick": { + "avgMs": 0.9942680602057624, + "p95Ms": 2.46962699999937 + } + }, + "network": { + "totalBytesSent": 61868350, + "totalBytesReceived": 0, + "maxConnectedPlayers": 50, + "avgBytesSentPerSecond": 1028137.2208681963, + "maxBytesSentPerSecond": 1463934.4796269767, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 1499.7729670304736, + "maxPacketsSentPerSecond": 1546.7655600840778, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0.02314558385562619, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "connect-clients", + "durationMs": 10004, + "collected": false + }, + { + "name": "spawn-bots", + "durationMs": 14, + "collected": false + }, + { + "name": "measure", + "durationMs": 57278, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/packages/perf-tools/perf-results/mobile-baseline.json b/packages/perf-tools/perf-results/mobile-baseline.json new file mode 100644 index 00000000..026924cb --- /dev/null +++ b/packages/perf-tools/perf-results/mobile-baseline.json @@ -0,0 +1,107 @@ +{ + "timestamp": "2026-03-06T05:37:22.354Z", + "scenario": "mobile-stress", + "durationMs": 53540, + "baseline": { + "avgTickMs": 0.4587460180765039, + "maxTickMs": 15.124867999998969, + "p95TickMs": 0.9381503000001733, + "p99TickMs": 2.281670333333265, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 44.646557871500654, + "avgFps": 11, + "client": { + "avgFps": 11, + "minFps": 2, + "avgFrameTimeMs": 113.79666666438182, + "avgDrawCalls": 29.033333333333335, + "maxDrawCalls": 82, + "avgTriangles": 288556.26666666666, + "maxTriangles": 585503, + "avgGeometries": 67.96666666666667, + "avgEntities": 16.033333333333335, + "avgVisibleChunks": 37, + "avgUsedMemoryMb": 154.76837107340495 + }, + "operations": { + "entities_tick": { + "avgMs": 0.100145550230758, + "p95Ms": 0.22139043333239292 + }, + "physics_step": { + "avgMs": 0.12324488336618562, + "p95Ms": 0.21948773333291077 + }, + "physics_cleanup": { + "avgMs": 0.006021806708011858, + "p95Ms": 0.008220166667403344 + }, + "simulation_step": { + "avgMs": 0.13603819024276229, + "p95Ms": 0.25110573333270925 + }, + "entities_emit_updates": { + "avgMs": 0.18667054487873366, + "p95Ms": 0.39165366666651 + }, + "world_tick": { + "avgMs": 0.4523834474370553, + "p95Ms": 0.923272233333167 + }, + "ticker_tick": { + "avgMs": 0.5255798218460593, + "p95Ms": 1.0511126000004878 + }, + "send_all_packets": { + "avgMs": 0.005485181556029348, + "p95Ms": 0.006105533333660181 + }, + "network_synchronize_cleanup": { + "avgMs": 0.0038300627479962318, + "p95Ms": 0.005125833333052773 + }, + "network_synchronize": { + "avgMs": 0.02166705592282186, + "p95Ms": 0.03459496666703975 + } + }, + "network": { + "totalBytesSent": 0, + "totalBytesReceived": 0, + "maxConnectedPlayers": 0, + "avgBytesSentPerSecond": 0, + "maxBytesSentPerSecond": 0, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 0, + "maxPacketsSentPerSecond": 0, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "setup", + "durationMs": 579, + "collected": false + }, + { + "name": "wait-for-world", + "durationMs": 8933, + "collected": false + }, + { + "name": "measure", + "durationMs": 35430, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 30, + "clientSnapshotCount": 30 + } +} \ No newline at end of file diff --git a/packages/perf-tools/perf-results/mobile-blob-shadows.json b/packages/perf-tools/perf-results/mobile-blob-shadows.json new file mode 100644 index 00000000..ce2e9c94 --- /dev/null +++ b/packages/perf-tools/perf-results/mobile-blob-shadows.json @@ -0,0 +1,107 @@ +{ + "timestamp": "2026-03-06T05:38:29.905Z", + "scenario": "mobile-stress", + "durationMs": 54150, + "baseline": { + "avgTickMs": 0.46583130939201245, + "maxTickMs": 3.803404000005685, + "p95TickMs": 0.9453849333345715, + "p99TickMs": 2.2402389666673117, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 44.757807922363284, + "avgFps": 11.8, + "client": { + "avgFps": 11.8, + "minFps": 4, + "avgFrameTimeMs": 123.43000000069539, + "avgDrawCalls": 22.366666666666667, + "maxDrawCalls": 46, + "avgTriangles": 235624, + "maxTriangles": 474495, + "avgGeometries": 68.86666666666666, + "avgEntities": 16.133333333333333, + "avgVisibleChunks": 41.733333333333334, + "avgUsedMemoryMb": 163.46395737330118 + }, + "operations": { + "entities_tick": { + "avgMs": 0.12282506250936144, + "p95Ms": 0.22073986666740286 + }, + "physics_step": { + "avgMs": 0.12221092788338694, + "p95Ms": 0.2147771333332154 + }, + "physics_cleanup": { + "avgMs": 0.0059001934679510605, + "p95Ms": 0.00745206666639812 + }, + "simulation_step": { + "avgMs": 0.13515172358959354, + "p95Ms": 0.2382533333326137 + }, + "entities_emit_updates": { + "avgMs": 0.1728750027192132, + "p95Ms": 0.3198051000001821 + }, + "world_tick": { + "avgMs": 0.4596198044790182, + "p95Ms": 0.9313487666674821 + }, + "ticker_tick": { + "avgMs": 0.5311698951797701, + "p95Ms": 1.063469966667132 + }, + "send_all_packets": { + "avgMs": 0.004984616450122214, + "p95Ms": 0.006341833333378114 + }, + "network_synchronize_cleanup": { + "avgMs": 0.004518206802690285, + "p95Ms": 0.005636066665829276 + }, + "network_synchronize": { + "avgMs": 0.02275348790528069, + "p95Ms": 0.03206923333236773 + } + }, + "network": { + "totalBytesSent": 0, + "totalBytesReceived": 0, + "maxConnectedPlayers": 0, + "avgBytesSentPerSecond": 0, + "maxBytesSentPerSecond": 0, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 0, + "maxPacketsSentPerSecond": 0, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "setup", + "durationMs": 467, + "collected": false + }, + { + "name": "wait-for-world", + "durationMs": 10980, + "collected": false + }, + { + "name": "measure", + "durationMs": 34670, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 30, + "clientSnapshotCount": 30 + } +} \ No newline at end of file diff --git a/packages/perf-tools/perf-results/rzdesign-prs-vs-main-zoo-report-2026-03-09.md b/packages/perf-tools/perf-results/rzdesign-prs-vs-main-zoo-report-2026-03-09.md new file mode 100644 index 00000000..80dafa89 --- /dev/null +++ b/packages/perf-tools/perf-results/rzdesign-prs-vs-main-zoo-report-2026-03-09.md @@ -0,0 +1,81 @@ +# RZDESIGN PR Sweep vs `upstream/main` on Zoo Game + +Date: 2026-03-09 + +Baseline: +- Engine ref: `upstream/main` +- Commit: `44f2a42979999ef76413a6afdad02b416aecc000` +- Scenario: `zoo-game-full` +- CPU throttle: none + +Baseline metrics: +- Avg tick: `0.91ms` +- Avg FPS: `17.02` +- Min FPS: `15` +- Avg frame time: `60.47ms` + +## Summary + +- Open `RZDESIGN` PRs tested one-by-one against the same Zoo baseline: `18` +- `FAIL`: `13` +- `WARN`: `5` +- `PASS`: `0` + +Closest to acceptable: +- `#30` `WARN` +- `#31` `WARN` +- `#33` `WARN` +- `#9` `WARN` +- `#34` `WARN` + +Most concerning regressions: +- `#23` strong server and memory regression +- `#24` strongest `p99` regression in the sweep +- `#29` and `#32` both dropped min FPS from `15` to `7` +- `#14` regressed both server tick and client frame time materially + +Interesting mixed cases: +- `#22` improved average FPS and average frame time, but still failed on server tick cost and min FPS stability +- `#26` was roughly flat on client averages, but still failed on server tick thresholds +- `#32` improved average FPS, but failed badly on server metrics and min FPS + +## Important Note + +The initial tail of the sweep for PRs `#14+` was invalid because `run-owned-stack-suite.sh` resolved `pr:` through `origin` only. Those PRs were rerun after fixing the resolver to fall back to `upstream`. Final verdicts below use the successful reruns. + +Framework fix: +- Commit: `bec6537` +- PR: https://github.com/web3dev1337/hytopia-source/pull/11 + +Raw outputs: +- Initial batch: `packages/perf-tools/perf-results/rzdesign-pr-zoo-20260309-083752/` +- Rerun batch: `packages/perf-tools/perf-results/rzdesign-pr-zoo-rerun-20260309-0850-fix/` + +## Results + +| PR | Title | Overall | Avg Tick | Avg FPS | Min FPS | Avg Frame Time | +| --- | --- | --- | --- | --- | --- | --- | +| #2 | [Add configurable blob shadows for entities with quality-based performance controls](https://github.com/hytopiagg/hytopia-source/pull/2) | FAIL FAIL | 0.91 -> 1.05 (+15.3%) | 17.02 -> 15.71 (+7.7%) | 15.00 -> 14.00 (+6.7%) | 60.47 -> 63.38 (+4.8%) | +| #9 | [Improve movement/camera smoothness with deterministic prediction; ack-aware replay; and tick-aligned input application](https://github.com/hytopiagg/hytopia-source/pull/9) | WARN WARNING | 0.91 -> 0.95 (+3.6%) | 17.02 -> 16.44 (+3.4%) | 15.00 -> 14.00 (+6.7%) | 60.47 -> 59.68 (-1.3%) | +| #10 | [client(camera): smooth fixed-camera world-space follow and eliminate jitter](https://github.com/hytopiagg/hytopia-source/pull/10) | FAIL FAIL | 0.91 -> 1.01 (+11.0%) | 17.02 -> 16.40 (+3.7%) | 15.00 -> 14.00 (+6.7%) | 60.47 -> 62.20 (+2.9%) | +| #12 | [Update ThreeJS to 0.183 ](https://github.com/hytopiagg/hytopia-source/pull/12) | FAIL FAIL | 0.91 -> 1.02 (+11.9%) | 17.02 -> 16.31 (+4.2%) | 15.00 -> 11.00 (+26.7%) | 60.47 -> 68.09 (+12.6%) | +| #13 | [chore(client): update all dependencies and resolve compatibility changes](https://github.com/hytopiagg/hytopia-source/pull/13) | FAIL FAIL | 0.91 -> 0.98 (+6.7%) | 17.02 -> 16.18 (+5.0%) | 15.00 -> 13.00 (+13.3%) | 60.47 -> 61.64 (+1.9%) | +| #14 | [Enhance Local Server Discovery UI & Mobile Testing Flow](https://github.com/hytopiagg/hytopia-source/pull/14) | FAIL FAIL | 0.91 -> 1.18 (+29.2%) | 17.02 -> 14.16 (+16.8%) | 15.00 -> 12.00 (+20.0%) | 60.47 -> 71.37 (+18.0%) | +| #22 | [Improve adaptive render resolution for high-DPI displays.](https://github.com/hytopiagg/hytopia-source/pull/22) | FAIL FAIL | 0.91 -> 1.18 (+29.0%) | 17.02 -> 21.76 (-27.8%) | 15.00 -> 12.00 (+20.0%) | 60.47 -> 52.17 (-13.7%) | +| #23 | [Optimize chunk visibility with incremental culling updates.](https://github.com/hytopiagg/hytopia-source/pull/23) | FAIL FAIL | 0.91 -> 1.23 (+35.0%) | 17.02 -> 14.58 (+14.4%) | 15.00 -> 12.00 (+20.0%) | 60.47 -> 72.01 (+19.1%) | +| #24 | [Optimize GLTF instancing and outline rendering hot paths.](https://github.com/hytopiagg/hytopia-source/pull/24) | FAIL FAIL | 0.91 -> 1.24 (+36.0%) | 17.02 -> 14.36 (+15.7%) | 15.00 -> 11.00 (+26.7%) | 60.47 -> 70.21 (+16.1%) | +| #26 | [perf(client): skip GPU uploads for unchanged GLTF instance attributes](https://github.com/hytopiagg/hytopia-source/pull/26) | FAIL FAIL | 0.91 -> 1.05 (+14.9%) | 17.02 -> 17.16 (-0.8%) | 15.00 -> 15.00 (0.0%) | 60.47 -> 58.99 (-2.4%) | +| #27 | [perf(client): reuse chunk mesh geometry instead of dispose/recreate cycle](https://github.com/hytopiagg/hytopia-source/pull/27) | FAIL FAIL | 0.91 -> 1.05 (+15.1%) | 17.02 -> 15.82 (+7.0%) | 15.00 -> 13.00 (+13.3%) | 60.47 -> 65.08 (+7.6%) | +| #28 | [perf(client): quick-win settings — discrete GPU; faster quality ramp-up](https://github.com/hytopiagg/hytopia-source/pull/28) | FAIL FAIL | 0.91 -> 0.95 (+4.1%) | 17.02 -> 16.04 (+5.7%) | 15.00 -> 11.00 (+26.7%) | 60.47 -> 63.83 (+5.6%) | +| #29 | [perf(server): reuse Entity.tick() event payload to eliminate per-tick allocations](https://github.com/hytopiagg/hytopia-source/pull/29) | FAIL FAIL | 0.91 -> 1.11 (+21.7%) | 17.02 -> 16.93 (+0.5%) | 15.00 -> 7.00 (+53.3%) | 60.47 -> 68.99 (+14.1%) | +| #30 | [perf(client): render bloom pass at quarter resolution (~16× less fill)](https://github.com/hytopiagg/hytopia-source/pull/30) | WARN WARNING | 0.91 -> 0.97 (+6.1%) | 17.02 -> 16.56 (+2.7%) | 15.00 -> 15.00 (0.0%) | 60.47 -> 60.40 (-0.1%) | +| #31 | [perf(client): cache parsed chunk/batch origin coordinates](https://github.com/hytopiagg/hytopia-source/pull/31) | WARN WARNING | 0.91 -> 0.97 (+5.7%) | 17.02 -> 17.16 (-0.8%) | 15.00 -> 15.00 (0.0%) | 60.47 -> 59.95 (-0.9%) | +| #32 | [perf(server): reuse position/rotation arrays in network entity sync](https://github.com/hytopiagg/hytopia-source/pull/32) | FAIL FAIL | 0.91 -> 1.09 (+18.9%) | 17.02 -> 19.60 (-15.1%) | 15.00 -> 7.00 (+53.3%) | 60.47 -> 64.20 (+6.2%) | +| #33 | [Optimize network hot paths on client and server](https://github.com/hytopiagg/hytopia-source/pull/33) | WARN WARNING | 0.91 -> 0.97 (+6.5%) | 17.02 -> 16.60 (+2.5%) | 15.00 -> 15.00 (0.0%) | 60.47 -> 60.90 (+0.7%) | +| #34 | [feat(client): add gamepad controller support](https://github.com/hytopiagg/hytopia-source/pull/34) | WARN WARNING | 0.91 -> 1.01 (+10.3%) | 17.02 -> 16.27 (+4.4%) | 15.00 -> 13.00 (+13.3%) | 60.47 -> 62.14 (+2.8%) | + +## Bottom Line + +- On this Zoo scenario, no open `RZDESIGN` PR beat `upstream/main` cleanly. +- The safest-looking PRs from a perf perspective were `#30`, `#31`, `#33`, `#9`, and `#34`, but all still registered warnings. +- The strongest regressions were concentrated in `#14`, `#23`, `#24`, `#29`, and `#32`. diff --git a/packages/perf-tools/perf-results/stress-baseline-no-shadows.json b/packages/perf-tools/perf-results/stress-baseline-no-shadows.json new file mode 100644 index 00000000..4a7f9f46 --- /dev/null +++ b/packages/perf-tools/perf-results/stress-baseline-no-shadows.json @@ -0,0 +1,115 @@ +{ + "timestamp": "2026-03-06T04:26:31.503Z", + "scenario": "stress-test", + "durationMs": 72899, + "baseline": { + "avgTickMs": 0.8349998947705856, + "maxTickMs": 12.012511000000814, + "p95TickMs": 2.072938316667266, + "p99TickMs": 4.238928266666274, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 50.96484451293945, + "avgFps": 27.766666666666666, + "client": { + "avgFps": 27.766666666666666, + "minFps": 3, + "avgFrameTimeMs": 78.53666666746139, + "avgDrawCalls": 20.55, + "maxDrawCalls": 34, + "avgTriangles": 15433.466666666667, + "maxTriangles": 15947, + "avgGeometries": 18.983333333333334, + "avgEntities": 160, + "avgVisibleChunks": 6, + "avgUsedMemoryMb": 43.06479082107544 + }, + "operations": { + "entities_tick": { + "avgMs": 0.23867919862251075, + "p95Ms": 0.5874405166665383 + }, + "physics_step": { + "avgMs": 0.17312055879655402, + "p95Ms": 0.2684500000008484 + }, + "physics_cleanup": { + "avgMs": 0.007628609808494264, + "p95Ms": 0.007743966666870013 + }, + "simulation_step": { + "avgMs": 0.18813084050636156, + "p95Ms": 0.2958727833336828 + }, + "entities_emit_updates": { + "avgMs": 0.12927259610653663, + "p95Ms": 0.20604898333376695 + }, + "world_tick": { + "avgMs": 0.8291072575136396, + "p95Ms": 2.0896416499992787 + }, + "ticker_tick": { + "avgMs": 0.8990740486842234, + "p95Ms": 2.161601066666359 + }, + "serialize_packets": { + "avgMs": 0.05005582133096542, + "p95Ms": 0.06791426666650295 + }, + "send_packets": { + "avgMs": 0.2031820219195983, + "p95Ms": 0.3274495666664734 + }, + "send_all_packets": { + "avgMs": 0.4511723627023677, + "p95Ms": 0.9337175666661399 + }, + "network_synchronize_cleanup": { + "avgMs": 0.01083250275368758, + "p95Ms": 0.010753250000016123 + }, + "network_synchronize": { + "avgMs": 0.5143490774603025, + "p95Ms": 1.066514683332995 + } + }, + "network": { + "totalBytesSent": 5032278, + "totalBytesReceived": 381, + "maxConnectedPlayers": 3, + "avgBytesSentPerSecond": 74639.01842524527, + "maxBytesSentPerSecond": 112504.69959902228, + "avgBytesReceivedPerSecond": 5.6677308601674525, + "maxBytesReceivedPerSecond": 12.614810201718903, + "avgPacketsSentPerSecond": 82.37081361141468, + "maxPacketsSentPerSecond": 99.33494328345606, + "avgPacketsReceivedPerSecond": 1.7251234329295149, + "maxPacketsReceivedPerSecond": 3.881480062067355, + "avgSerializationMs": 0.04555379460987558, + "compressionCountTotal": 2 + } + }, + "phases": [ + { + "name": "spawn-entities", + "durationMs": 283, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 2079, + "collected": false + }, + { + "name": "measure", + "durationMs": 62049, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 60 + } +} \ No newline at end of file diff --git a/packages/perf-tools/perf-results/stress-with-blob-shadows.json b/packages/perf-tools/perf-results/stress-with-blob-shadows.json new file mode 100644 index 00000000..aaf88f62 --- /dev/null +++ b/packages/perf-tools/perf-results/stress-with-blob-shadows.json @@ -0,0 +1,115 @@ +{ + "timestamp": "2026-03-06T04:27:59.625Z", + "scenario": "stress-test", + "durationMs": 73358, + "baseline": { + "avgTickMs": 0.7359029286488398, + "maxTickMs": 13.290815000000293, + "p95TickMs": 1.829545699999653, + "p99TickMs": 3.758559049999955, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 65.29272626241048, + "avgFps": 26.133333333333333, + "client": { + "avgFps": 26.133333333333333, + "minFps": 3, + "avgFrameTimeMs": 83.7683333337307, + "avgDrawCalls": 21.933333333333334, + "maxDrawCalls": 34, + "avgTriangles": 15564.7, + "maxTriangles": 16717, + "avgGeometries": 18.983333333333334, + "avgEntities": 160, + "avgVisibleChunks": 6, + "avgUsedMemoryMb": 42.87554248174032 + }, + "operations": { + "entities_tick": { + "avgMs": 0.23253740997715816, + "p95Ms": 0.6180427999991783 + }, + "physics_step": { + "avgMs": 0.17034304841306572, + "p95Ms": 0.28114455000083277 + }, + "physics_cleanup": { + "avgMs": 0.005363463158733647, + "p95Ms": 0.007633716666593197 + }, + "simulation_step": { + "avgMs": 0.18412275955659105, + "p95Ms": 0.31188256666694847 + }, + "entities_emit_updates": { + "avgMs": 0.13922205929656295, + "p95Ms": 0.22354569999891585 + }, + "world_tick": { + "avgMs": 0.7279635695853309, + "p95Ms": 1.7226242000005791 + }, + "ticker_tick": { + "avgMs": 0.7914679062679743, + "p95Ms": 1.8152888833331418 + }, + "serialize_packets": { + "avgMs": 0.04350473671018177, + "p95Ms": 0.06910131666694118 + }, + "send_packets": { + "avgMs": 0.2378755490273082, + "p95Ms": 0.3898857000008017 + }, + "send_all_packets": { + "avgMs": 0.25720112163234377, + "p95Ms": 0.42160113333417637 + }, + "network_synchronize_cleanup": { + "avgMs": 0.007394239075581019, + "p95Ms": 0.011449733333120094 + }, + "network_synchronize": { + "avgMs": 0.3093806637599563, + "p95Ms": 0.49408704999987096 + } + }, + "network": { + "totalBytesSent": 1968736, + "totalBytesReceived": 158, + "maxConnectedPlayers": 1, + "avgBytesSentPerSecond": 28946.035070796195, + "maxBytesSentPerSecond": 39833.57688983931, + "avgBytesReceivedPerSecond": 2.3142748083937827, + "maxBytesReceivedPerSecond": 6.7732385632630665, + "avgPacketsSentPerSecond": 31.198995516308436, + "maxPacketsSentPerSecond": 33.44922483163599, + "avgPacketsReceivedPerSecond": 0.7040805818582495, + "maxPacketsReceivedPerSecond": 1.9352110180751618, + "avgSerializationMs": 0.039125146425009125, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "spawn-entities", + "durationMs": 268, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 2045, + "collected": false + }, + { + "name": "measure", + "durationMs": 62098, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 60 + } +} \ No newline at end of file diff --git a/packages/perf-tools/perf-results/throttled/block-churn.json b/packages/perf-tools/perf-results/throttled/block-churn.json new file mode 100644 index 00000000..b9b0d130 --- /dev/null +++ b/packages/perf-tools/perf-results/throttled/block-churn.json @@ -0,0 +1,96 @@ +{ + "timestamp": "2026-03-05T22:17:19.107Z", + "scenario": "block-churn", + "durationMs": 60704, + "baseline": { + "avgTickMs": 1.7920962972298742, + "maxTickMs": 818.4425780000001, + "p95TickMs": 15.938466400000395, + "p99TickMs": 32.30030700000055, + "ticksOverBudgetPct": 0.08977197917290083, + "avgMemoryMb": 59.6778865814209, + "operations": { + "entities_tick": { + "avgMs": 0.0017992915526633703, + "p95Ms": 0.00264800000007502 + }, + "physics_step": { + "avgMs": 0.07971446459713513, + "p95Ms": 0.21731668333321372 + }, + "physics_cleanup": { + "avgMs": 0.006041949423665551, + "p95Ms": 0.00842779999996613 + }, + "simulation_step": { + "avgMs": 0.09204405101249048, + "p95Ms": 0.23842736666712577 + }, + "entities_emit_updates": { + "avgMs": 0.0007704882554027713, + "p95Ms": 0.0011542333334394546 + }, + "world_tick": { + "avgMs": 1.7866339137612697, + "p95Ms": 15.922663583332769 + }, + "ticker_tick": { + "avgMs": 2.0766720433910724, + "p95Ms": 16.25997188333313 + }, + "serialize_packets": { + "avgMs": 35.30756422500021, + "p95Ms": 215.65989150000087 + }, + "send_packets": { + "avgMs": 8.101761554166256, + "p95Ms": 74.23772149999877 + }, + "send_all_packets": { + "avgMs": 3.170143889461697, + "p95Ms": 14.861828183333076 + }, + "network_synchronize_cleanup": { + "avgMs": 0.005061964011218121, + "p95Ms": 0.005593783333127552 + }, + "network_synchronize": { + "avgMs": 3.4166130860493653, + "p95Ms": 15.827658950000462 + } + }, + "network": { + "totalBytesSent": 700074, + "totalBytesReceived": 0, + "maxConnectedPlayers": 11, + "avgBytesSentPerSecond": 7203.236313753144, + "maxBytesSentPerSecond": 371429.9225781149, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 1.1557572928010482, + "maxPacketsSentPerSecond": 50.93357640406447, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 35.302880750000625, + "compressionCountTotal": 2 + } + }, + "phases": [ + { + "name": "setup", + "durationMs": 391, + "collected": false + }, + { + "name": "churn-and-measure", + "durationMs": 54738, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/packages/perf-tools/perf-results/throttled/blocks-10m-dense.json b/packages/perf-tools/perf-results/throttled/blocks-10m-dense.json new file mode 100644 index 00000000..293953a4 --- /dev/null +++ b/packages/perf-tools/perf-results/throttled/blocks-10m-dense.json @@ -0,0 +1,101 @@ +{ + "timestamp": "2026-03-05T22:17:31.467Z", + "scenario": "blocks-10m-dense", + "durationMs": 74631, + "baseline": { + "avgTickMs": 0.3678607531131963, + "maxTickMs": 122.04515399999946, + "p95TickMs": 2.1842488333336103, + "p99TickMs": 4.440352749999935, + "ticksOverBudgetPct": 0.055807204710128074, + "avgMemoryMb": 50.483299255371094, + "operations": { + "serialize_packets": { + "avgMs": 26.083742666666982, + "p95Ms": 78.19489600000088 + }, + "send_packets": { + "avgMs": 7.842397916666117, + "p95Ms": 93.62325499999861 + }, + "entities_tick": { + "avgMs": 0.0017024739391461282, + "p95Ms": 0.0024962333337195256 + }, + "physics_step": { + "avgMs": 0.04807215796251588, + "p95Ms": 0.06922056666602051 + }, + "physics_cleanup": { + "avgMs": 0.004988607348668802, + "p95Ms": 0.0073067333335833 + }, + "simulation_step": { + "avgMs": 0.0587658567192885, + "p95Ms": 0.08550433333281642 + }, + "entities_emit_updates": { + "avgMs": 0.0007269236479577146, + "p95Ms": 0.0011208500004916762 + }, + "send_all_packets": { + "avgMs": 0.4331703633853387, + "p95Ms": 1.5901372000000265 + }, + "network_synchronize_cleanup": { + "avgMs": 0.004934105890258781, + "p95Ms": 0.00476849999983339 + }, + "network_synchronize": { + "avgMs": 0.5776717590318386, + "p95Ms": 2.082377566667007 + }, + "world_tick": { + "avgMs": 0.3630098159640884, + "p95Ms": 2.169407649999812 + }, + "ticker_tick": { + "avgMs": 0.45042879628732363, + "p95Ms": 2.2408463999991 + } + }, + "network": { + "totalBytesSent": 59405, + "totalBytesReceived": 0, + "maxConnectedPlayers": 11, + "avgBytesSentPerSecond": 0, + "maxBytesSentPerSecond": 0, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 0, + "maxPacketsSentPerSecond": 0, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 26.07868633333419, + "compressionCountTotal": 1 + } + }, + "phases": [ + { + "name": "setup-world", + "durationMs": 829, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 11063, + "collected": false + }, + { + "name": "join-and-measure", + "durationMs": 54952, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/packages/perf-tools/perf-results/throttled/combined.json b/packages/perf-tools/perf-results/throttled/combined.json new file mode 100644 index 00000000..fab4ed57 --- /dev/null +++ b/packages/perf-tools/perf-results/throttled/combined.json @@ -0,0 +1,106 @@ +{ + "timestamp": "2026-03-05T22:15:59.694Z", + "scenario": "combined-stress", + "durationMs": 159407, + "baseline": { + "avgTickMs": 2.2210433833768275, + "maxTickMs": 244.87248999997973, + "p95TickMs": 2.0417600833325804, + "p99TickMs": 5.831250199999583, + "ticksOverBudgetPct": 0.8563761521409403, + "avgMemoryMb": 54.1039103825887, + "operations": { + "entities_tick": { + "avgMs": 0.13170391823024324, + "p95Ms": 0.3108036583330128 + }, + "physics_step": { + "avgMs": 1.6669555470551904, + "p95Ms": 0.9186607750001107 + }, + "physics_cleanup": { + "avgMs": 0.004834618755810993, + "p95Ms": 0.007338025000596341 + }, + "simulation_step": { + "avgMs": 1.6767478551536559, + "p95Ms": 0.9357361166668852 + }, + "entities_emit_updates": { + "avgMs": 0.09113548881103865, + "p95Ms": 0.1698119666651716 + }, + "world_tick": { + "avgMs": 2.1868956408808007, + "p95Ms": 2.1068056583339057 + }, + "ticker_tick": { + "avgMs": 2.2532712923935927, + "p95Ms": 2.2136142833334818 + }, + "serialize_packets": { + "avgMs": 0.03149973891560877, + "p95Ms": 0.048125941667240116 + }, + "send_packets": { + "avgMs": 0.047645785149837215, + "p95Ms": 0.14509697499943286 + }, + "send_all_packets": { + "avgMs": 0.5059881515560963, + "p95Ms": 0.9331871166657948 + }, + "network_synchronize_cleanup": { + "avgMs": 0.00815659531033403, + "p95Ms": 0.009510408334305491 + }, + "network_synchronize": { + "avgMs": 0.5519809230840689, + "p95Ms": 1.0122887249999621 + } + }, + "network": { + "totalBytesSent": 32793740, + "totalBytesReceived": 0, + "maxConnectedPlayers": 10, + "avgBytesSentPerSecond": 263583.04981156316, + "maxBytesSentPerSecond": 395882.4175598577, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 289.8093361202148, + "maxPacketsSentPerSecond": 309.3024036889567, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0.02869727538360874, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "load-world", + "durationMs": 20610, + "collected": false + }, + { + "name": "spawn-all", + "durationMs": 3261, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 10001, + "collected": false + }, + { + "name": "measure", + "durationMs": 113673, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 120, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/packages/perf-tools/perf-results/throttled/idle.json b/packages/perf-tools/perf-results/throttled/idle.json new file mode 100644 index 00000000..5e749365 --- /dev/null +++ b/packages/perf-tools/perf-results/throttled/idle.json @@ -0,0 +1,88 @@ +{ + "timestamp": "2026-03-05T22:13:14.135Z", + "scenario": "idle-baseline", + "durationMs": 34499, + "baseline": { + "avgTickMs": 0.06873839472150463, + "maxTickMs": 2.656435999997484, + "p95TickMs": 0.10843789999983831, + "p99TickMs": 0.23720813333302432, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 40.57544199625651, + "operations": { + "entities_tick": { + "avgMs": 0.0014293007559212406, + "p95Ms": 0.0024829333336735242 + }, + "physics_step": { + "avgMs": 0.03581650559019222, + "p95Ms": 0.054915733333261114 + }, + "physics_cleanup": { + "avgMs": 0.004280903038759899, + "p95Ms": 0.006948233333302293 + }, + "simulation_step": { + "avgMs": 0.043721700564098104, + "p95Ms": 0.0693393666666149 + }, + "entities_emit_updates": { + "avgMs": 0.0004996976603057125, + "p95Ms": 0.0008938333335208881 + }, + "world_tick": { + "avgMs": 0.0653713565377379, + "p95Ms": 0.10413693333333261 + }, + "ticker_tick": { + "avgMs": 0.11021069374160467, + "p95Ms": 0.17285916666666404 + }, + "send_all_packets": { + "avgMs": 0.004280311159438012, + "p95Ms": 0.005457633333298873 + }, + "network_synchronize_cleanup": { + "avgMs": 0.0027704331924936795, + "p95Ms": 0.0036384666669619036 + }, + "network_synchronize": { + "avgMs": 0.019889677746238336, + "p95Ms": 0.02417540000005829 + } + }, + "network": { + "totalBytesSent": 0, + "totalBytesReceived": 0, + "maxConnectedPlayers": 0, + "avgBytesSentPerSecond": 0, + "maxBytesSentPerSecond": 0, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 0, + "maxPacketsSentPerSecond": 0, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "warmup", + "durationMs": 5002, + "collected": false + }, + { + "name": "measure", + "durationMs": 27445, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 30, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/packages/perf-tools/perf-results/throttled/join-storm.json b/packages/perf-tools/perf-results/throttled/join-storm.json new file mode 100644 index 00000000..64896598 --- /dev/null +++ b/packages/perf-tools/perf-results/throttled/join-storm.json @@ -0,0 +1,101 @@ +{ + "timestamp": "2026-03-05T22:18:21.680Z", + "scenario": "join-storm", + "durationMs": 127100, + "baseline": { + "avgTickMs": 14.464908799974616, + "maxTickMs": 889.6479410000029, + "p95TickMs": 52.32250618333346, + "p99TickMs": 376.2388852000005, + "ticksOverBudgetPct": 4.134977479140516, + "avgMemoryMb": 172.07372512817383, + "operations": { + "entities_tick": { + "avgMs": 0.0007735137187064894, + "p95Ms": 0.0012794166645714237 + }, + "physics_step": { + "avgMs": 0.17200653391165643, + "p95Ms": 0.22557224999909523 + }, + "physics_cleanup": { + "avgMs": 0.0034315212480463086, + "p95Ms": 0.004959150000164906 + }, + "simulation_step": { + "avgMs": 0.18072331332917793, + "p95Ms": 0.23946373333443868 + }, + "entities_emit_updates": { + "avgMs": 0.0005254315965799393, + "p95Ms": 0.0008232833320410767 + }, + "send_all_packets": { + "avgMs": 25.83929444550733, + "p95Ms": 115.75569449999979 + }, + "network_synchronize_cleanup": { + "avgMs": 0.007352429604151781, + "p95Ms": 0.010367683332515299 + }, + "network_synchronize": { + "avgMs": 28.901835215490784, + "p95Ms": 131.68631768333398 + }, + "world_tick": { + "avgMs": 14.652898023898821, + "p95Ms": 42.212484316668636 + }, + "ticker_tick": { + "avgMs": 17.626167811172035, + "p95Ms": 65.38415256666667 + }, + "serialize_packets": { + "avgMs": 53.38229595035458, + "p95Ms": 633.5850970000029 + }, + "send_packets": { + "avgMs": 3.546311573742193, + "p95Ms": 0.07468499999959022 + } + }, + "network": { + "totalBytesSent": 37009296, + "totalBytesReceived": 0, + "maxConnectedPlayers": 100, + "avgBytesSentPerSecond": 13743.984988798014, + "maxBytesSentPerSecond": 824639.0993278809, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 1.9051063006584565, + "maxPacketsSentPerSecond": 114.3063780395074, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 53.37751157801402, + "compressionCountTotal": 100 + } + }, + "phases": [ + { + "name": "preload-world", + "durationMs": 13599, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 10007, + "collected": false + }, + { + "name": "join-and-measure", + "durationMs": 96112, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/packages/perf-tools/perf-results/throttled/many-players.json b/packages/perf-tools/perf-results/throttled/many-players.json new file mode 100644 index 00000000..92dd592f --- /dev/null +++ b/packages/perf-tools/perf-results/throttled/many-players.json @@ -0,0 +1,101 @@ +{ + "timestamp": "2026-03-05T22:14:39.001Z", + "scenario": "many-players", + "durationMs": 77087, + "baseline": { + "avgTickMs": 1.4590587241113593, + "maxTickMs": 95.00327599999946, + "p95TickMs": 4.452570316666727, + "p99TickMs": 11.73098763333328, + "ticksOverBudgetPct": 0.4448811375856328, + "avgMemoryMb": 42.65193405151367, + "operations": { + "entities_tick": { + "avgMs": 0.10576290050770412, + "p95Ms": 0.20400351666661057 + }, + "physics_step": { + "avgMs": 0.07345848340204558, + "p95Ms": 0.08515465000091353 + }, + "physics_cleanup": { + "avgMs": 0.004846111421254792, + "p95Ms": 0.006297650000366654 + }, + "simulation_step": { + "avgMs": 0.08184788601875806, + "p95Ms": 0.09878523333360742 + }, + "entities_emit_updates": { + "avgMs": 0.13243129281036803, + "p95Ms": 0.14672096666636208 + }, + "world_tick": { + "avgMs": 1.4560245726125276, + "p95Ms": 3.954584216665838 + }, + "serialize_packets": { + "avgMs": 0.055834169535996514, + "p95Ms": 0.06272613333373253 + }, + "send_packets": { + "avgMs": 0.042127080877416165, + "p95Ms": 0.07195404999990083 + }, + "send_all_packets": { + "avgMs": 2.194892022245168, + "p95Ms": 5.136805466666313 + }, + "network_synchronize_cleanup": { + "avgMs": 0.027337910491300656, + "p95Ms": 0.010607350000282168 + }, + "network_synchronize": { + "avgMs": 2.2545784772858304, + "p95Ms": 5.270788466666393 + }, + "ticker_tick": { + "avgMs": 1.6182136562381628, + "p95Ms": 4.45939291666679 + } + }, + "network": { + "totalBytesSent": 59247050, + "totalBytesReceived": 0, + "maxConnectedPlayers": 50, + "avgBytesSentPerSecond": 981925.7710636476, + "maxBytesSentPerSecond": 1481842.24530106, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 1495.9960818070504, + "maxPacketsSentPerSecond": 1545.8347297706246, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0.05209213276093013, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "connect-clients", + "durationMs": 10004, + "collected": false + }, + { + "name": "spawn-bots", + "durationMs": 26, + "collected": false + }, + { + "name": "measure", + "durationMs": 54810, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/packages/perf-tools/perf-results/throttled/stress.json b/packages/perf-tools/perf-results/throttled/stress.json new file mode 100644 index 00000000..c5ab5ad9 --- /dev/null +++ b/packages/perf-tools/perf-results/throttled/stress.json @@ -0,0 +1,93 @@ +{ + "timestamp": "2026-03-05T22:14:25.417Z", + "scenario": "stress-test", + "durationMs": 67064, + "baseline": { + "avgTickMs": 0.4664462714793657, + "maxTickMs": 124.68226300000242, + "p95TickMs": 1.0065098166666455, + "p99TickMs": 3.230148583333236, + "ticksOverBudgetPct": 0.03895386232074429, + "avgMemoryMb": 47.25280558268229, + "operations": { + "entities_tick": { + "avgMs": 0.15120736404654064, + "p95Ms": 0.317036599999877 + }, + "physics_step": { + "avgMs": 0.16594821015365768, + "p95Ms": 0.16640125000015663 + }, + "physics_cleanup": { + "avgMs": 0.004367642500187419, + "p95Ms": 0.006311950000751191 + }, + "simulation_step": { + "avgMs": 0.1754781371539642, + "p95Ms": 0.18472536666664988 + }, + "entities_emit_updates": { + "avgMs": 0.09463284273395711, + "p95Ms": 0.14569283333321437 + }, + "send_all_packets": { + "avgMs": 0.00472118470044643, + "p95Ms": 0.005706349999460751 + }, + "network_synchronize_cleanup": { + "avgMs": 0.017371922555600192, + "p95Ms": 0.009056049999981042 + }, + "network_synchronize": { + "avgMs": 0.060705054480927854, + "p95Ms": 0.06329793333370617 + }, + "world_tick": { + "avgMs": 0.4628283030346499, + "p95Ms": 0.8850007000005462 + }, + "ticker_tick": { + "avgMs": 0.5156509428049243, + "p95Ms": 0.9869928500000545 + } + }, + "network": { + "totalBytesSent": 0, + "totalBytesReceived": 0, + "maxConnectedPlayers": 0, + "avgBytesSentPerSecond": 0, + "maxBytesSentPerSecond": 0, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 0, + "maxPacketsSentPerSecond": 0, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "spawn-entities", + "durationMs": 67, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 5035, + "collected": false + }, + { + "name": "measure", + "durationMs": 54910, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/packages/perf-tools/perf-results/walkthrough-baseline.json b/packages/perf-tools/perf-results/walkthrough-baseline.json new file mode 100644 index 00000000..f3d72081 --- /dev/null +++ b/packages/perf-tools/perf-results/walkthrough-baseline.json @@ -0,0 +1,107 @@ +{ + "timestamp": "2026-03-06T05:23:50.026Z", + "scenario": "stress-walkthrough", + "durationMs": 94602, + "baseline": { + "avgTickMs": 0.5391012166361615, + "maxTickMs": 17.94537400000263, + "p95TickMs": 1.0751982444444568, + "p99TickMs": 3.3784996888897796, + "ticksOverBudgetPct": 0.006376105191566539, + "avgMemoryMb": 66.61536475287544, + "avgFps": 12.755555555555556, + "client": { + "avgFps": 12.755555555555556, + "minFps": 8, + "avgFrameTimeMs": 83.55111111071375, + "avgDrawCalls": 25, + "maxDrawCalls": 25, + "avgTriangles": 263769, + "maxTriangles": 263769, + "avgGeometries": 68, + "avgEntities": 16, + "avgVisibleChunks": 30, + "avgUsedMemoryMb": 152.89682466718887 + }, + "operations": { + "entities_tick": { + "avgMs": 0.12939661985288534, + "p95Ms": 0.2722047333333952 + }, + "physics_step": { + "avgMs": 0.1328637783304221, + "p95Ms": 0.23616346666741367 + }, + "physics_cleanup": { + "avgMs": 0.005769868592307142, + "p95Ms": 0.007910844444510682 + }, + "simulation_step": { + "avgMs": 0.14639699397135983, + "p95Ms": 0.2683269555539785 + }, + "entities_emit_updates": { + "avgMs": 0.22037241875926325, + "p95Ms": 0.46857908888980293 + }, + "send_all_packets": { + "avgMs": 0.005150242018657108, + "p95Ms": 0.005713133334615527 + }, + "network_synchronize_cleanup": { + "avgMs": 0.002902808306821984, + "p95Ms": 0.004927022222141709 + }, + "network_synchronize": { + "avgMs": 0.02167050894632742, + "p95Ms": 0.025265599999369847 + }, + "world_tick": { + "avgMs": 0.5284478310519916, + "p95Ms": 1.256651333332411 + }, + "ticker_tick": { + "avgMs": 0.6083506673469862, + "p95Ms": 1.5245236666667754 + } + }, + "network": { + "totalBytesSent": 0, + "totalBytesReceived": 0, + "maxConnectedPlayers": 0, + "avgBytesSentPerSecond": 0, + "maxBytesSentPerSecond": 0, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 0, + "maxPacketsSentPerSecond": 0, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "spawn-entities", + "durationMs": 283, + "collected": false + }, + { + "name": "wait-for-world", + "durationMs": 8504, + "collected": false + }, + { + "name": "walkthrough", + "durationMs": 77149, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 45, + "clientSnapshotCount": 45 + } +} \ No newline at end of file diff --git a/packages/perf-tools/perf-results/walkthrough-blob-shadows.json b/packages/perf-tools/perf-results/walkthrough-blob-shadows.json new file mode 100644 index 00000000..570210c2 --- /dev/null +++ b/packages/perf-tools/perf-results/walkthrough-blob-shadows.json @@ -0,0 +1,107 @@ +{ + "timestamp": "2026-03-06T05:25:52.669Z", + "scenario": "stress-walkthrough", + "durationMs": 93623, + "baseline": { + "avgTickMs": 0.4463646582871844, + "maxTickMs": 9.024755000005825, + "p95TickMs": 0.920134800000071, + "p99TickMs": 1.746577422222577, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 43.593108622233075, + "avgFps": 23.727272727272727, + "client": { + "avgFps": 23.727272727272727, + "minFps": 0, + "avgFrameTimeMs": 152.19772727347234, + "avgDrawCalls": 17.75, + "maxDrawCalls": 82, + "avgTriangles": 155875.47727272726, + "maxTriangles": 584747, + "avgGeometries": 63, + "avgEntities": 15.545454545454545, + "avgVisibleChunks": 64.06818181818181, + "avgUsedMemoryMb": 220.8743019104004 + }, + "operations": { + "entities_tick": { + "avgMs": 0.12082475065899774, + "p95Ms": 0.212825199998608 + }, + "physics_step": { + "avgMs": 0.12011465469852715, + "p95Ms": 0.1786375777785401 + }, + "physics_cleanup": { + "avgMs": 0.006925774357740608, + "p95Ms": 0.007451844444989951 + }, + "simulation_step": { + "avgMs": 0.13415876547377642, + "p95Ms": 0.20245715555631452 + }, + "entities_emit_updates": { + "avgMs": 0.15868798814720403, + "p95Ms": 0.2651034888884169 + }, + "world_tick": { + "avgMs": 0.44518506949526604, + "p95Ms": 0.8249018000006294 + }, + "ticker_tick": { + "avgMs": 0.5173913805869171, + "p95Ms": 0.9369601777788679 + }, + "send_all_packets": { + "avgMs": 0.0055072860737605586, + "p95Ms": 0.00635031110970077 + }, + "network_synchronize_cleanup": { + "avgMs": 0.005068940954863205, + "p95Ms": 0.0048210888887600355 + }, + "network_synchronize": { + "avgMs": 0.02610076192554791, + "p95Ms": 0.02581884444480238 + } + }, + "network": { + "totalBytesSent": 0, + "totalBytesReceived": 0, + "maxConnectedPlayers": 0, + "avgBytesSentPerSecond": 0, + "maxBytesSentPerSecond": 0, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 0, + "maxPacketsSentPerSecond": 0, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "spawn-entities", + "durationMs": 232, + "collected": false + }, + { + "name": "wait-for-world", + "durationMs": 5576, + "collected": false + }, + { + "name": "walkthrough", + "durationMs": 79685, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 45, + "clientSnapshotCount": 44 + } +} \ No newline at end of file diff --git a/packages/perf-tools/perf-results/zoo-game-bots-full.json b/packages/perf-tools/perf-results/zoo-game-bots-full.json new file mode 100644 index 00000000..52e5fa9b --- /dev/null +++ b/packages/perf-tools/perf-results/zoo-game-bots-full.json @@ -0,0 +1,83 @@ +{ + "timestamp": "2026-03-06T02:14:26.782Z", + "scenario": "zoo-game-18-bots", + "durationMs": 129558, + "baseline": { + "avgTickMs": 0.2549325841085261, + "maxTickMs": 2.073420000000624, + "p95TickMs": 0.6464003333326218, + "p99TickMs": 0.8528039666671854, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 313.16300671895345, + "operations": { + "entities_tick": { + "avgMs": 0.04462002311489062, + "p95Ms": 0.13042361666618188 + }, + "physics_step": { + "avgMs": 0.11469806608628241, + "p95Ms": 0.2668681666667908 + }, + "physics_cleanup": { + "avgMs": 0.0021119560521985978, + "p95Ms": 0.0033652583335727587 + }, + "simulation_step": { + "avgMs": 0.11956885418414905, + "p95Ms": 0.2744475249994442 + }, + "entities_emit_updates": { + "avgMs": 0.06976556052305873, + "p95Ms": 0.20276147499901828 + }, + "send_all_packets": { + "avgMs": 0.0018747430761021815, + "p95Ms": 0.003443375000976327 + }, + "network_synchronize_cleanup": { + "avgMs": 0.003389591343193421, + "p95Ms": 0.005195633333914884 + }, + "network_synchronize": { + "avgMs": 0.027452826833688247, + "p95Ms": 0.06792570833273809 + }, + "world_tick": { + "avgMs": 0.25370135807671634, + "p95Ms": 0.6378724583342167 + }, + "ticker_tick": { + "avgMs": 0.2883600087509416, + "p95Ms": 0.6806504083335312 + } + }, + "network": { + "totalBytesSent": 0, + "totalBytesReceived": 0, + "maxConnectedPlayers": 0, + "avgBytesSentPerSecond": 0, + "maxBytesSentPerSecond": 0, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 0, + "maxPacketsSentPerSecond": 0, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "gameplay", + "durationMs": 108824, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 120, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/packages/perf-tools/perf-results/zoo-game-bots.json b/packages/perf-tools/perf-results/zoo-game-bots.json new file mode 100644 index 00000000..16bfcaf2 --- /dev/null +++ b/packages/perf-tools/perf-results/zoo-game-bots.json @@ -0,0 +1,27 @@ +{ + "timestamp": "2026-03-06T01:56:48.515Z", + "scenario": "zoo-game-18-bots", + "durationMs": 128451, + "baseline": { + "avgTickMs": 0, + "maxTickMs": 0, + "p95TickMs": 0, + "p99TickMs": 0, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 0, + "operations": {} + }, + "phases": [ + { + "name": "gameplay", + "durationMs": 108389, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 0, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/packages/perf-tools/scripts/apply-instrumentation-overlay.sh b/packages/perf-tools/scripts/apply-instrumentation-overlay.sh new file mode 100755 index 00000000..7928da75 --- /dev/null +++ b/packages/perf-tools/scripts/apply-instrumentation-overlay.sh @@ -0,0 +1,688 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +DEFAULT_SOURCE_REPO="$(cd "$SCRIPT_DIR/../../.." && pwd)" +SOURCE_ENGINE_REPO="$DEFAULT_SOURCE_REPO" +TARGET_ENGINE_REPO="" + +usage() { + cat <<'EOF' +Usage: + apply-instrumentation-overlay.sh --target-engine-repo [options] + +Options: + --source-engine-repo Repo providing the current overlay sources + --target-engine-repo Engine repo/worktree to patch temporarily + -h, --help Show this help +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --source-engine-repo) + SOURCE_ENGINE_REPO="$(cd "$2" && pwd)" + shift 2 + ;; + --target-engine-repo) + TARGET_ENGINE_REPO="$(cd "$2" && pwd)" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [[ -z "$TARGET_ENGINE_REPO" ]]; then + usage >&2 + exit 1 +fi + +if [[ ! -d "$SOURCE_ENGINE_REPO" || ! -d "$TARGET_ENGINE_REPO" ]]; then + echo "Error: source and target engine repos must both exist" >&2 + exit 1 +fi + +SOURCE_SERVER_PKG="$SOURCE_ENGINE_REPO/server/package.json" +TARGET_SERVER_PKG="$TARGET_ENGINE_REPO/server/package.json" +TARGET_GAME_TS="$TARGET_ENGINE_REPO/client/src/Game.ts" +TARGET_WEB_SERVER_TS="$TARGET_ENGINE_REPO/server/src/networking/WebServer.ts" +TARGET_WORLD_LOOP_TS="$TARGET_ENGINE_REPO/server/src/worlds/WorldLoop.ts" +TARGET_CONNECTION_TS="$TARGET_ENGINE_REPO/server/src/networking/Connection.ts" +TARGET_PLAYER_MANAGER_TS="$TARGET_ENGINE_REPO/server/src/players/PlayerManager.ts" +TARGET_PERF_BRIDGE_TS="$TARGET_ENGINE_REPO/client/src/core/PerfBridge.ts" +TARGET_PERF_HARNESS_ENTRY_TS="$TARGET_ENGINE_REPO/server/src/perf/perf-harness.ts" +TARGET_PERF_HARNESS_TS="$TARGET_ENGINE_REPO/server/src/perf/PerfHarness.ts" +TARGET_PERF_MONITOR_TS="$TARGET_ENGINE_REPO/server/src/metrics/PerformanceMonitor.ts" +TARGET_NETWORK_METRICS_TS="$TARGET_ENGINE_REPO/server/src/metrics/NetworkMetrics.ts" +MANIFEST_PATH="$TARGET_ENGINE_REPO/.perf-tools-overlay.json" + +TARGET_ENGINE_REPO="$TARGET_ENGINE_REPO" \ +SOURCE_ENGINE_REPO="$SOURCE_ENGINE_REPO" \ +SOURCE_SERVER_PKG="$SOURCE_SERVER_PKG" \ +TARGET_SERVER_PKG="$TARGET_SERVER_PKG" \ +TARGET_GAME_TS="$TARGET_GAME_TS" \ +TARGET_WEB_SERVER_TS="$TARGET_WEB_SERVER_TS" \ +TARGET_WORLD_LOOP_TS="$TARGET_WORLD_LOOP_TS" \ +TARGET_CONNECTION_TS="$TARGET_CONNECTION_TS" \ +TARGET_PLAYER_MANAGER_TS="$TARGET_PLAYER_MANAGER_TS" \ +TARGET_PERF_BRIDGE_TS="$TARGET_PERF_BRIDGE_TS" \ +TARGET_PERF_HARNESS_ENTRY_TS="$TARGET_PERF_HARNESS_ENTRY_TS" \ +TARGET_PERF_HARNESS_TS="$TARGET_PERF_HARNESS_TS" \ +TARGET_PERF_MONITOR_TS="$TARGET_PERF_MONITOR_TS" \ +TARGET_NETWORK_METRICS_TS="$TARGET_NETWORK_METRICS_TS" \ +MANIFEST_PATH="$MANIFEST_PATH" \ +node <<'NODE' +const fs = require('fs'); +const path = require('path'); + +const sourceRepo = process.env.SOURCE_ENGINE_REPO; +const targetRepo = process.env.TARGET_ENGINE_REPO; +const sourceServerPkgPath = process.env.SOURCE_SERVER_PKG; +const targetServerPkgPath = process.env.TARGET_SERVER_PKG; +const targetGamePath = process.env.TARGET_GAME_TS; +const targetWebServerPath = process.env.TARGET_WEB_SERVER_TS; +const targetWorldLoopPath = process.env.TARGET_WORLD_LOOP_TS; +const targetConnectionPath = process.env.TARGET_CONNECTION_TS; +const targetPlayerManagerPath = process.env.TARGET_PLAYER_MANAGER_TS; +const targetPerfBridgePath = process.env.TARGET_PERF_BRIDGE_TS; +const targetPerfHarnessEntryPath = process.env.TARGET_PERF_HARNESS_ENTRY_TS; +const targetPerfHarnessPath = process.env.TARGET_PERF_HARNESS_TS; +const targetPerfMonitorPath = process.env.TARGET_PERF_MONITOR_TS; +const targetNetworkMetricsPath = process.env.TARGET_NETWORK_METRICS_TS; +const manifestPath = process.env.MANIFEST_PATH; + +const sourcePerfBridgePath = path.join(sourceRepo, 'client/src/core/PerfBridge.ts'); +const sourcePerfHarnessEntryPath = path.join(sourceRepo, 'server/src/perf/perf-harness.ts'); +const legacyPerfHarnessPath = path.join(sourceRepo, 'packages/perf-tools/overlays/legacy-server/PerfHarness.ts'); +const minimalPerfHarnessPath = path.join(sourceRepo, 'packages/perf-tools/overlays/minimal-server/PerfHarness.ts'); +const sourcePerfMonitorPath = path.join(sourceRepo, 'server/src/metrics/PerformanceMonitor.ts'); +const sourceNetworkMetricsPath = path.join(sourceRepo, 'server/src/metrics/NetworkMetrics.ts'); + +const manifest = { + sourceEngineRepo: sourceRepo, + targetEngineRepo: targetRepo, + applied: false, + client: { + perfBridge: 'none', + gamePatched: false, + }, + server: { + perfHarness: 'none', + perfHarnessEntry: 'none', + mode: 'none', + webServerPatched: false, + worldLoopPatched: false, + connectionPatched: false, + playerManagerPatched: false, + buildScriptPatched: false, + snapshotApi: false, + actionApi: false, + }, +}; + +function exists(filePath) { + return filePath && fs.existsSync(filePath); +} + +function read(filePath) { + return fs.readFileSync(filePath, 'utf8'); +} + +function write(filePath, content) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, content, 'utf8'); +} + +function copyIfMissing(sourcePath, targetPath) { + if (exists(targetPath)) { + return false; + } + + write(targetPath, read(sourcePath)); + return true; +} + +function replaceOnce(text, searchValue, replaceValue) { + if (!text.includes(searchValue)) { + return { text, changed: false }; + } + + return { + text: text.replace(searchValue, replaceValue), + changed: true, + }; +} + +function replaceRegexOnce(text, pattern, replaceValue) { + const nextText = text.replace(pattern, replaceValue); + + return { + text: nextText, + changed: nextText !== text, + }; +} + +function patchGame() { + if (!exists(targetGamePath)) { + return false; + } + + let text = read(targetGamePath); + let changed = false; + + if (!text.includes("import PerfBridge from './core/PerfBridge';")) { + const replaced = replaceRegexOnce( + text, + /(import PerformanceMetricsManager from '\.\/core\/PerformanceMetricsManager';\n)/, + `$1import PerfBridge from './core/PerfBridge';\n`, + ); + text = replaced.text; + changed = changed || replaced.changed; + } + + if (!text.includes('readonly inPerfMode')) { + const replaced = replaceRegexOnce( + text, + /( readonly inDebugMode = [^\n]+;\n)/, + `$1 readonly inPerfMode = new URLSearchParams(window.location.search).get('perf') === '1';\n`, + ); + text = replaced.text; + changed = changed || replaced.changed; + } + + if (!text.includes('new PerfBridge(this);')) { + const replaced = replaceRegexOnce( + text, + /( this\._chunkWorkerClient = new ChunkWorkerClient\(\);\n)/, + `$1\n if (this.inPerfMode) {\n new PerfBridge(this);\n }\n`, + ); + text = replaced.text; + changed = changed || replaced.changed; + } + + if (changed) { + write(targetGamePath, text); + } + + return changed; +} + +function patchWebServer() { + if (!exists(targetWebServerPath)) { + return false; + } + + let text = read(targetWebServerPath); + let changed = false; + + if (!text.includes("import PerfHarness from '@/perf/PerfHarness';")) { + const replaced = replaceRegexOnce( + text, + /(import PlayerManager from '@\/players\/PlayerManager';\n)/, + `$1import PerfHarness from '@/perf/PerfHarness';\n`, + ); + text = replaced.text; + changed = changed || replaced.changed; + } + + if (!text.includes('PerfHarness.enableIfConfigured();')) { + const replaced = replaceRegexOnce( + text, + /( this\._server = http2\.createSecureServer\(\{ key: SSL_KEY, cert: SSL_CERT, allowHTTP1: true \}\);\n)/, + ` PerfHarness.enableIfConfigured();\n\n$1`, + ); + text = replaced.text; + changed = changed || replaced.changed; + } + + if (!text.includes('PerfHarness.handleWebRequest(req, res)')) { + const replaced = replaceRegexOnce( + text, + /( \/\/ Health check\n)/, + ` if (PerfHarness.handleWebRequest(req, res)) {\n return;\n }\n\n$1`, + ); + text = replaced.text; + changed = changed || replaced.changed; + } + + if (changed) { + write(targetWebServerPath, text); + } + + return changed; +} + +function patchBuildScript() { + if (!exists(sourceServerPkgPath) || !exists(targetServerPkgPath)) { + return false; + } + + const sourcePkg = JSON.parse(read(sourceServerPkgPath)); + const targetPkg = JSON.parse(read(targetServerPkgPath)); + const buildScript = sourcePkg.scripts?.['build:perf-harness']; + + if (!buildScript || targetPkg.scripts?.['build:perf-harness']) { + return false; + } + + targetPkg.scripts = targetPkg.scripts || {}; + targetPkg.scripts['build:perf-harness'] = buildScript; + write(targetServerPkgPath, `${JSON.stringify(targetPkg, null, 2)}\n`); + return true; +} + +function hasModernServerPerfHarness() { + if (!exists(targetPerfHarnessPath)) { + return false; + } + + const text = read(targetPerfHarnessPath); + return text.includes('/__perf/action') && text.includes('/__perf/snapshot'); +} + +function hasLegacyPerformanceBaseline() { + return exists(path.join(targetRepo, 'server/src/metrics/PerformanceBaseline.ts')); +} + +function patchWorldLoop() { + if (!exists(targetWorldLoopPath)) { + return false; + } + + let text = read(targetWorldLoopPath); + let changed = false; + + if (!text.includes("import PerformanceMonitor from '@/metrics/PerformanceMonitor';")) { + const replaced = replaceRegexOnce( + text, + /(import PlayerManager from '@\/players\/PlayerManager';\n)/, + `$1import PerformanceMonitor from '@/metrics/PerformanceMonitor';\n`, + ); + text = replaced.text; + changed = changed || replaced.changed; + } + + if (!text.includes('const perfMon = PerformanceMonitor.instance;')) { + const replaced = replaceRegexOnce( + text, + /( const tickStart = performance\.now\(\);\n)/, + `${[ + '$1', + ' const perfMon = PerformanceMonitor.instance;', + ' const profiling = perfMon.isEnabled;', + '', + ' if (profiling) {', + ' perfMon.beginTick(', + ' this._currentTick,', + ' this._world.entityManager.entityCount,', + ' PlayerManager.instance.playerCount,', + ' this._world.id,', + ' );', + ' }', + '', + ].join('\n')}`, + ); + text = replaced.text; + changed = changed || replaced.changed; + } + + if (!text.includes("perfMon.recordPhase('entities_tick'")) { + const replaced = replaceOnce( + text, + " }, () => this._world.entityManager.tickEntities(tickDeltaMs));", + [ + " }, () => {", + ' const phaseStart = profiling ? performance.now() : 0;', + ' this._world.entityManager.tickEntities(tickDeltaMs);', + " if (profiling) perfMon.recordPhase('entities_tick', performance.now() - phaseStart, this._world.id);", + ' });', + ].join('\n'), + ); + text = replaced.text; + changed = changed || replaced.changed; + } + + if (!text.includes("perfMon.recordPhase('simulation_step'")) { + const replaced = replaceOnce( + text, + " }, () => this._world.simulation.step(tickDeltaMs));", + [ + " }, () => {", + ' const phaseStart = profiling ? performance.now() : 0;', + ' this._world.simulation.step(tickDeltaMs);', + " if (profiling) perfMon.recordPhase('simulation_step', performance.now() - phaseStart, this._world.id);", + ' });', + ].join('\n'), + ); + text = replaced.text; + changed = changed || replaced.changed; + } + + if (!text.includes("perfMon.recordPhase('entities_emit_updates'")) { + const replaced = replaceOnce( + text, + " }, () => this._world.entityManager.checkAndEmitUpdates());", + [ + " }, () => {", + ' const phaseStart = profiling ? performance.now() : 0;', + ' this._world.entityManager.checkAndEmitUpdates();', + " if (profiling) perfMon.recordPhase('entities_emit_updates', performance.now() - phaseStart, this._world.id);", + ' });', + ].join('\n'), + ); + text = replaced.text; + changed = changed || replaced.changed; + } + + if (!text.includes("perfMon.recordPhase('network_synchronize'")) { + const replaced = replaceOnce( + text, + " }, () => this._world.networkSynchronizer.synchronize());", + [ + ' }, () => {', + ' const phaseStart = profiling ? performance.now() : 0;', + ' this._world.networkSynchronizer.synchronize();', + " if (profiling) perfMon.recordPhase('network_synchronize', performance.now() - phaseStart, this._world.id);", + ' });', + ].join('\n'), + ); + text = replaced.text; + changed = changed || replaced.changed; + } + + if (!text.includes('perfMon.endTick(this._world.id);')) { + const replaced = replaceRegexOnce( + text, + /(\n this\._currentTick\+\+;\n)/, + `\n if (profiling) {\n perfMon.endTick(this._world.id);\n }$1`, + ); + text = replaced.text; + changed = changed || replaced.changed; + } + + if (changed) { + write(targetWorldLoopPath, text); + } + + return changed; +} + +function patchConnection() { + if (!exists(targetConnectionPath)) { + return false; + } + + let text = read(targetConnectionPath); + let changed = false; + + if (!text.includes("import NetworkMetrics from '@/metrics/NetworkMetrics';")) { + const replaced = replaceRegexOnce( + text, + /(import EventRouter from '@\/events\/EventRouter';\n)/, + `$1import NetworkMetrics from '@/metrics/NetworkMetrics';\n`, + ); + text = replaced.text; + changed = changed || replaced.changed; + } + + if (!text.includes('netMetrics.recordSerialization(')) { + let replaced = replaceOnce( + text, + " }, span => {\n let outputBuffer = msgpackr.pack(packets);", + [ + ' }, span => {', + ' const netMetrics = NetworkMetrics.instance;', + ' const recordNetwork = netMetrics.isEnabled;', + ' const start = recordNetwork ? performance.now() : 0;', + '', + ' let outputBuffer = msgpackr.pack(packets);', + ].join('\n'), + ); + text = replaced.text; + changed = changed || replaced.changed; + + replaced = replaceOnce( + text, + ' if (outputBuffer.byteLength > 64 * 1024) { // Compress packets larger than 64kb, mainly chunks.', + [ + ' const shouldCompress = outputBuffer.byteLength > 64 * 1024;', + '', + ' if (shouldCompress) { // Compress packets larger than 64kb, mainly chunks.', + ].join('\n'), + ); + text = replaced.text; + changed = changed || replaced.changed; + + replaced = replaceOnce( + text, + '\n return outputBuffer;\n });', + [ + '', + ' if (recordNetwork) {', + ' netMetrics.recordSerialization(performance.now() - start);', + ' if (shouldCompress) {', + ' netMetrics.recordCompression();', + ' }', + ' }', + '', + ' return outputBuffer;', + ' });', + ].join('\n'), + ); + text = replaced.text; + changed = changed || replaced.changed; + } + + if (!text.includes('netMetrics.recordBytesSent(bytesSent);')) { + let replaced = replaceOnce( + text, + ' if (wtConnected) {', + [ + ' const netMetrics = NetworkMetrics.instance;', + ' const recordNetwork = netMetrics.isEnabled;', + '', + ' let bytesSent = serializedBuffer.byteLength;', + '', + ' if (wtConnected) {', + ].join('\n'), + ); + text = replaced.text; + changed = changed || replaced.changed; + + replaced = replaceOnce( + text, + ' this._wtReliableWriter?.write(protocol.framePacketBuffer(serializedBuffer)).catch(() => {', + [ + ' const framed = protocol.framePacketBuffer(serializedBuffer);', + ' bytesSent = framed.byteLength;', + '', + ' this._wtReliableWriter?.write(framed).catch(() => {', + ].join('\n'), + ); + text = replaced.text; + changed = changed || replaced.changed; + + replaced = replaceOnce( + text, + '\n this.emitWithGlobal(ConnectionEvent.PACKETS_SENT, {', + [ + '', + ' if (recordNetwork) {', + ' netMetrics.recordBytesSent(bytesSent);', + ' for (let i = 0; i < packets.length; i++) {', + ' netMetrics.recordPacketSent();', + ' }', + ' }', + '', + ' this.emitWithGlobal(ConnectionEvent.PACKETS_SENT, {', + ].join('\n'), + ); + text = replaced.text; + changed = changed || replaced.changed; + } + + if (!text.includes('netMetrics.recordBytesReceived(data.byteLength);')) { + const replaced = replaceOnce( + text, + " private _onMessage = (data: Buffer): void => {\n try {\n const packet = this._deserialize(data);", + [ + ' private _onMessage = (data: Buffer): void => {', + ' const netMetrics = NetworkMetrics.instance;', + ' const recordNetwork = netMetrics.isEnabled;', + '', + ' if (recordNetwork) {', + ' netMetrics.recordBytesReceived(data.byteLength);', + ' netMetrics.recordPacketReceived();', + ' }', + '', + ' try {', + ' const packet = this._deserialize(data);', + ].join('\n'), + ); + text = replaced.text; + changed = changed || replaced.changed; + } + + if (changed) { + write(targetConnectionPath, text); + } + + return changed; +} + +function patchPlayerManager() { + if (!exists(targetPlayerManagerPath)) { + return false; + } + + let text = read(targetPlayerManagerPath); + let changed = false; + + if (!text.includes("import NetworkMetrics from '@/metrics/NetworkMetrics';")) { + const replaced = replaceRegexOnce( + text, + /(import ErrorHandler from '@\/errors\/ErrorHandler';\n)/, + `$1import NetworkMetrics from '@/metrics/NetworkMetrics';\n`, + ); + text = replaced.text; + changed = changed || replaced.changed; + } + + if (!text.includes('NetworkMetrics.instance.setConnectedPlayers(this.playerCount);')) { + let replaced = replaceOnce( + text, + ' this._connectionPlayers.set(connection, player);\n', + ' this._connectionPlayers.set(connection, player);\n NetworkMetrics.instance.setConnectedPlayers(this.playerCount);\n', + ); + text = replaced.text; + changed = changed || replaced.changed; + + replaced = replaceOnce( + text, + ' this._connectionPlayers.delete(connection);\n', + ' this._connectionPlayers.delete(connection);\n NetworkMetrics.instance.setConnectedPlayers(this.playerCount);\n', + ); + text = replaced.text; + changed = changed || replaced.changed; + } + + if (changed) { + write(targetPlayerManagerPath, text); + } + + return changed; +} + +if (path.resolve(sourceRepo) === path.resolve(targetRepo)) { + console.log('Instrumentation overlay manifest: none (source and target are the same checkout)'); + process.exit(0); +} + +if (hasModernServerPerfHarness()) { + manifest.server.perfHarness = 'existing'; + manifest.server.mode = 'modern-existing'; + manifest.server.snapshotApi = true; + manifest.server.actionApi = true; +} else if (hasLegacyPerformanceBaseline()) { + write(targetPerfHarnessPath, read(legacyPerfHarnessPath)); + manifest.applied = true; + manifest.server.perfHarness = 'overlay'; + manifest.server.mode = 'legacy-baseline'; + + if (patchWebServer()) { + manifest.applied = true; + manifest.server.webServerPatched = true; + } + + manifest.server.snapshotApi = true; + manifest.server.actionApi = false; +} else { + write(targetPerfHarnessPath, read(minimalPerfHarnessPath)); + write(targetPerfMonitorPath, read(sourcePerfMonitorPath)); + write(targetNetworkMetricsPath, read(sourceNetworkMetricsPath)); + manifest.applied = true; + manifest.server.perfHarness = 'overlay'; + manifest.server.mode = 'telemetry-minimal'; + + if (patchWebServer()) { + manifest.applied = true; + manifest.server.webServerPatched = true; + } + + if (patchWorldLoop()) { + manifest.applied = true; + manifest.server.worldLoopPatched = true; + } + + if (patchConnection()) { + manifest.applied = true; + manifest.server.connectionPatched = true; + } + + if (patchPlayerManager()) { + manifest.applied = true; + manifest.server.playerManagerPatched = true; + } + + manifest.server.snapshotApi = true; + manifest.server.actionApi = false; +} + +if (exists(sourcePerfHarnessEntryPath)) { + if (copyIfMissing(sourcePerfHarnessEntryPath, targetPerfHarnessEntryPath)) { + manifest.applied = true; + manifest.server.perfHarnessEntry = 'overlay'; + } else if (exists(targetPerfHarnessEntryPath)) { + manifest.server.perfHarnessEntry = 'existing'; + } +} + +if (patchBuildScript()) { + manifest.applied = true; + manifest.server.buildScriptPatched = true; +} + +if (exists(targetPerfBridgePath)) { + manifest.client.perfBridge = 'existing'; +} else if (exists(sourcePerfBridgePath)) { + write(targetPerfBridgePath, read(sourcePerfBridgePath)); + manifest.applied = true; + manifest.client.perfBridge = 'overlay'; +} + +if (patchGame()) { + manifest.applied = true; + manifest.client.gamePatched = true; +} + +write(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); +console.log(`Instrumentation overlay manifest: ${manifestPath}`); +NODE diff --git a/packages/perf-tools/scripts/copy-assets.mjs b/packages/perf-tools/scripts/copy-assets.mjs new file mode 100644 index 00000000..3a13edd2 --- /dev/null +++ b/packages/perf-tools/scripts/copy-assets.mjs @@ -0,0 +1,20 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const pkgRoot = path.resolve(__dirname, '..'); +const srcPresets = path.join(pkgRoot, 'src', 'presets'); +const distPresets = path.join(pkgRoot, 'dist', 'presets'); + +fs.mkdirSync(distPresets, { recursive: true }); + +for (const file of fs.readdirSync(srcPresets)) { + if (!file.endsWith('.yaml') && !file.endsWith('.yml')) continue; + + fs.copyFileSync( + path.join(srcPresets, file), + path.join(distPresets, file), + ); +} + diff --git a/packages/perf-tools/scripts/ensure-node-modules.sh b/packages/perf-tools/scripts/ensure-node-modules.sh new file mode 100644 index 00000000..3a7c8aaa --- /dev/null +++ b/packages/perf-tools/scripts/ensure-node-modules.sh @@ -0,0 +1,123 @@ +#!/bin/bash +set -euo pipefail + +SOURCE_REPO="" +TARGET_REPO="" +PACKAGES="server,client,protocol" + +usage() { + cat <<'EOF' +Usage: + ensure-node-modules.sh --source-repo --target-repo [options] + +Options: + --source-repo Reference repo used for lockfile-compatible symlink reuse + --target-repo Engine repo/worktree whose dependencies should be prepared + --packages Comma-separated package dirs to prepare (default: server,client,protocol) + -h, --help Show this help +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --source-repo) + SOURCE_REPO="$(cd "$2" && pwd)" + shift 2 + ;; + --target-repo) + TARGET_REPO="$(cd "$2" && pwd)" + shift 2 + ;; + --packages) + PACKAGES="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [[ -z "$SOURCE_REPO" || -z "$TARGET_REPO" ]]; then + usage >&2 + exit 1 +fi + +if [[ ! -d "$SOURCE_REPO" || ! -d "$TARGET_REPO" ]]; then + echo "Error: source and target repos must both exist" >&2 + exit 1 +fi + +ensure_package_deps() { + local package_rel="$1" + local source_dir="$SOURCE_REPO/$package_rel" + local target_dir="$TARGET_REPO/$package_rel" + local source_manifest="$source_dir/package.json" + local target_manifest="$target_dir/package.json" + local source_lock="$source_dir/package-lock.json" + local target_lock="$target_dir/package-lock.json" + local target_node_modules="$target_dir/node_modules" + local can_reuse="false" + + if [[ ! -f "$target_manifest" ]]; then + return + fi + + if [[ -d "$source_dir/node_modules" ]]; then + if [[ -f "$source_lock" && -f "$target_lock" ]] && cmp -s "$source_lock" "$target_lock"; then + can_reuse="true" + elif [[ ! -f "$source_lock" && ! -f "$target_lock" && -f "$source_manifest" ]] && cmp -s "$source_manifest" "$target_manifest"; then + can_reuse="true" + fi + fi + + if [[ -L "$target_node_modules" ]]; then + local target_link + target_link="$(readlink "$target_node_modules")" + + if [[ -n "$target_link" && "$target_link" == "$source_dir/node_modules" && "$can_reuse" == "true" ]]; then + return + fi + + rm -f "$target_node_modules" + fi + + if [[ -d "$target_node_modules" ]]; then + return + fi + + if [[ "$can_reuse" == "true" ]]; then + mkdir -p "$target_dir" + ln -s "$source_dir/node_modules" "$target_node_modules" + echo "Reused $package_rel/node_modules from $SOURCE_REPO" + return + fi + + echo "Installing dependencies in $target_dir" + + if [[ -f "$target_lock" ]]; then + ( + cd "$target_dir" + npm ci --no-audit --no-fund + ) + else + ( + cd "$target_dir" + npm install --no-audit --no-fund + ) + fi +} + +IFS=',' read -r -a PACKAGE_ARRAY <<< "$PACKAGES" + +for package_rel in "${PACKAGE_ARRAY[@]}"; do + package_rel="${package_rel// /}" + [[ -z "$package_rel" ]] && continue + ensure_package_deps "$package_rel" +done diff --git a/packages/perf-tools/scripts/link-sdk.sh b/packages/perf-tools/scripts/link-sdk.sh new file mode 100755 index 00000000..9030faa7 --- /dev/null +++ b/packages/perf-tools/scripts/link-sdk.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Build modified SDK and npm-link it for use by external games +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +DEFAULT_REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +REPO_ROOT="$DEFAULT_REPO_ROOT" + +while [[ $# -gt 0 ]]; do + case "$1" in + --engine-repo) + REPO_ROOT="$(cd "$2" && pwd)" + shift 2 + ;; + -h|--help) + echo "Usage: $0 [--engine-repo ]" + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + exit 1 + ;; + esac +done + +bash "$SCRIPT_DIR/ensure-node-modules.sh" \ + --source-repo "$DEFAULT_REPO_ROOT" \ + --target-repo "$REPO_ROOT" \ + --packages "server,protocol,sdk" + +echo "Building SDK from $REPO_ROOT/server ..." +cd "$REPO_ROOT/server" + +SERVER_BUILD_SCRIPT=$(node - <<'NODE' +const pkg = require('./package.json'); + +if (pkg.scripts && pkg.scripts['build:server']) { + console.log('build:server'); + process.exit(0); +} + +console.log('build'); +NODE +) + +npm run "$SERVER_BUILD_SCRIPT" + +echo "Linking SDK from $REPO_ROOT/sdk ..." +cd "$REPO_ROOT/sdk" +npm link + +SDK_VERSION=$(node -e "console.log(require('./package.json').version)") +echo "" +echo "SDK v${SDK_VERSION} linked globally." +echo "In game directories, run: npm link hytopia" diff --git a/packages/perf-tools/scripts/run-external-game-benchmark.sh b/packages/perf-tools/scripts/run-external-game-benchmark.sh new file mode 100755 index 00000000..3e203550 --- /dev/null +++ b/packages/perf-tools/scripts/run-external-game-benchmark.sh @@ -0,0 +1,213 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +INVOCATION_CWD="$(pwd)" + +GAME_DIR="" +PRESET="" +CLIENT_URL="" +ENGINE_REPO="$REPO_ROOT" +SERVER_CMD="npm start" +PORT="9091" +CPU_THROTTLE="" +OUTPUT="" +VERBOSE="false" +INSTRUMENTATION_OVERLAY="true" + +usage() { + cat <<'EOF' +Usage: + run-external-game-benchmark.sh --game-dir --preset --client-url [options] + +Required: + --game-dir External game repo/worktree to benchmark + --preset Built-in perf preset to run + --client-url Client dev/prod URL used by the benchmark browser + +Options: + --engine-repo Engine repo/worktree whose SDK/client is under test + --server-cmd Command used to start the external game server (default: npm start) + --port HTTPS port for the external game server (default: 9091) + --cpu-throttle Browser CPU throttle rate (example: 4, 16) + --output Write benchmark JSON to this path + --no-instrumentation-overlay + Do not patch older engine refs with temporary perf hooks + --verbose Enable verbose benchmark logging + +Examples: + bash packages/perf-tools/scripts/run-external-game-benchmark.sh \ + --engine-repo /home/ab/GitHub/hytopia/work1 \ + --game-dir /home/ab/GitHub/games/hyfire2-sdk-compat \ + --preset hyfire2-bots \ + --client-url http://localhost:4173 \ + --server-cmd "AUTO_START_WITH_BOTS=true hytopia start" \ + --port 8082 \ + --output perf-results/hyfire2-under-test.json + + bash packages/perf-tools/scripts/run-external-game-benchmark.sh \ + --engine-repo /home/ab/GitHub/hytopia/work1 \ + --game-dir /home/ab/GitHub/games/hytopia/zoo-game/work1 \ + --preset zoo-game-full \ + --client-url http://localhost:4173 \ + --output perf-results/zoo-pr2.json + + bash packages/perf-tools/scripts/run-external-game-benchmark.sh \ + --engine-repo /home/ab/GitHub/hytopia/work1 \ + --game-dir /home/ab/GitHub/games/hytopia/zoo-game/work1 \ + --preset zoo-game-observe \ + --client-url http://localhost:4173 \ + --cpu-throttle 4 \ + --verbose + +For a long manual HyFire2 observation run instead of a measured benchmark: + cd /home/ab/GitHub/games/hyfire2-sdk-compat + PORT=8082 AUTO_START_WITH_BOTS=true hytopia start +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --game-dir) + GAME_DIR="$(cd "$2" && pwd)" + shift 2 + ;; + --preset) + PRESET="$2" + shift 2 + ;; + --client-url) + CLIENT_URL="$2" + shift 2 + ;; + --engine-repo) + ENGINE_REPO="$(cd "$2" && pwd)" + shift 2 + ;; + --server-cmd) + SERVER_CMD="$2" + shift 2 + ;; + --port) + PORT="$2" + shift 2 + ;; + --cpu-throttle) + CPU_THROTTLE="$2" + shift 2 + ;; + --output) + OUTPUT="$2" + shift 2 + ;; + --no-instrumentation-overlay) + INSTRUMENTATION_OVERLAY="false" + shift + ;; + --verbose) + VERBOSE="true" + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + echo "" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [[ -z "$GAME_DIR" || -z "$PRESET" || -z "$CLIENT_URL" ]]; then + usage >&2 + exit 1 +fi + +if [[ ! -f "$GAME_DIR/package.json" ]]; then + echo "Error: no package.json found in $GAME_DIR" >&2 + exit 1 +fi + +if [[ -n "$OUTPUT" && "$OUTPUT" != /* ]]; then + OUTPUT="$INVOCATION_CWD/$OUTPUT" +fi + +SERVER_PID="" + +cleanup() { + if [[ -n "$SERVER_PID" ]]; then + kill -TERM "-$SERVER_PID" >/dev/null 2>&1 || true + wait "$SERVER_PID" >/dev/null 2>&1 || true + fi +} + +trap cleanup EXIT INT TERM + +echo "==> Linking SDK checkout into external game" +echo "Engine repo: $ENGINE_REPO" +if [[ "$INSTRUMENTATION_OVERLAY" == "true" ]]; then + bash "$SCRIPT_DIR/apply-instrumentation-overlay.sh" \ + --source-engine-repo "$REPO_ROOT" \ + --target-engine-repo "$ENGINE_REPO" +fi +bash "$SCRIPT_DIR/link-sdk.sh" --engine-repo "$ENGINE_REPO" +bash "$SCRIPT_DIR/setup-game.sh" "$GAME_DIR" --engine-repo "$ENGINE_REPO" + +echo "" +echo "==> Starting external game server" +echo "Game dir: $GAME_DIR" +echo "Server cmd: $SERVER_CMD" +echo "Port: $PORT" + +( + cd "$GAME_DIR" + exec setsid bash -lc "PORT=$PORT HYTOPIA_PERF_TOOLS=1 $SERVER_CMD" +) & +SERVER_PID=$! + +echo "" +echo "==> Waiting for https://localhost:$PORT to become healthy" +READY="false" +for _ in $(seq 1 180); do + if curl -skf "https://localhost:$PORT/" >/dev/null 2>&1; then + READY="true" + break + fi + + sleep 1 +done + +if [[ "$READY" != "true" ]]; then + echo "Error: external game server did not become healthy on port $PORT" >&2 + exit 1 +fi + +echo "" +echo "==> Running preset $PRESET against https://localhost:$PORT" +cd "$REPO_ROOT/packages/perf-tools" + +BENCH_CMD=( + npx tsx src/cli.ts run + --preset "$PRESET" + --external-server "https://localhost:$PORT" + --with-client + --client-dev-url "$CLIENT_URL" +) + +if [[ -n "$CPU_THROTTLE" ]]; then + BENCH_CMD+=(--cpu-throttle "$CPU_THROTTLE") +fi + +if [[ -n "$OUTPUT" ]]; then + BENCH_CMD+=(--output "$OUTPUT") +fi + +if [[ "$VERBOSE" == "true" ]]; then + BENCH_CMD+=(--verbose) +fi + +"${BENCH_CMD[@]}" diff --git a/packages/perf-tools/scripts/run-owned-stack-suite.sh b/packages/perf-tools/scripts/run-owned-stack-suite.sh new file mode 100755 index 00000000..62af64c3 --- /dev/null +++ b/packages/perf-tools/scripts/run-owned-stack-suite.sh @@ -0,0 +1,656 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +TOOLS_REPO="$REPO_ROOT" +INVOCATION_CWD="$(pwd)" + +ENGINE_REPO="$REPO_ROOT" +ENGINE_REF="" +CLIENT_URL="" +CLIENT_PORT="4173" +CPU_THROTTLE="" +REPEAT_COUNT="1" +OUTPUT_ROOT="$REPO_ROOT/packages/perf-tools/perf-results/owned-stack" +INTERNAL_PRESETS="idle,stress,stress-walkthrough" +EXTERNAL_GAMES="zoo,hyfire2" +ZOO_DIR="/home/ab/GitHub/games/hytopia/zoo-game/work1" +ZOO_PRESET="zoo-game-full" +ZOO_PORT="9091" +HYFIRE2_DIR="/home/ab/GitHub/games/hyfire2-sdk-compat" +HYFIRE2_PRESET="hyfire2-bots" +HYFIRE2_PORT="8082" +HYFIRE2_SERVER_CMD="AUTO_START_WITH_BOTS=true hytopia start" +KEEP_WORKTREE="false" +INSTRUMENTATION_OVERLAY="true" + +WORKTREE_DIR="" +ACTIVE_ENGINE_REPO="$ENGINE_REPO" +RESOLVED_COMMIT="" +RESOLVED_LABEL="" +SUMMARY_PATH="" +CLIENT_SERVER_PID="" +CLIENT_SERVER_LOG="" +OVERLAY_MANIFEST="" + +declare -a SUMMARY_ROWS=() + +usage() { + cat <<'EOF' +Usage: + run-owned-stack-suite.sh [options] + +Runs the HYTOPIA perf stack against the owned games and core synthetic presets +using either the current checkout or a specific engine ref / PR. + +Options: + --engine-ref Git ref, commit, branch, or PR number / pr: + --engine-repo Engine repo to test (default: current repo) + --client-url Browser client URL for all client-side runs + --client-port Port to use when auto-launching client dev server + --cpu-throttle Browser CPU throttle rate for client runs + --repeat Run each scenario N times and aggregate via median + --output-root Root directory for suite outputs + --internal-presets Comma-separated built-in presets, or none + --external-games Comma-separated games: zoo,hyfire2, or none + --zoo-dir Zoo Game repo/worktree + --zoo-preset Zoo preset to run + --zoo-port Zoo external server port + --hyfire2-dir HyFire2 repo/worktree + --hyfire2-preset HyFire2 preset to run + --hyfire2-port HyFire2 external server port + --no-instrumentation-overlay + Do not patch older target refs with temporary perf hooks + --keep-worktree Leave the temporary engine worktree on disk + -h, --help Show this help + +Examples: + bash packages/perf-tools/scripts/run-owned-stack-suite.sh \ + --engine-ref pr:2 \ + --client-port 4173 + + bash packages/perf-tools/scripts/run-owned-stack-suite.sh \ + --engine-ref feature/blob-shadows \ + --repeat 3 \ + --cpu-throttle 4 \ + --output-root /tmp/hytopia-bench + + bash packages/perf-tools/scripts/run-owned-stack-suite.sh \ + --internal-presets idle,stress,stress-walkthrough,join-storm \ + --external-games zoo,hyfire2 +EOF +} + +sanitize_slug() { + echo "$1" | tr '/: ' '---' | tr -cd '[:alnum:]._-' | cut -c1-80 +} + +join_by() { + local sep="$1" + shift + local first="true" + + for item in "$@"; do + if [[ "$first" == "true" ]]; then + printf '%s' "$item" + first="false" + else + printf '%s%s' "$sep" "$item" + fi + done +} + +fetch_engine_ref() { + local ref="$1" + local remote + + for remote in origin upstream; do + if ! git -C "$ENGINE_REPO" remote get-url "$remote" >/dev/null 2>&1; then + continue + fi + + if git -C "$ENGINE_REPO" fetch "$remote" "$ref"; then + return 0 + fi + done + + return 1 +} + +cleanup() { + if [[ -n "$CLIENT_SERVER_PID" ]]; then + kill -TERM "-$CLIENT_SERVER_PID" >/dev/null 2>&1 || true + wait "$CLIENT_SERVER_PID" >/dev/null 2>&1 || true + fi + + if [[ -n "$WORKTREE_DIR" && -d "$WORKTREE_DIR" && "$KEEP_WORKTREE" != "true" ]]; then + git -C "$ENGINE_REPO" worktree remove --force "$WORKTREE_DIR" >/dev/null 2>&1 || true + fi +} + +trap cleanup EXIT INT TERM + +while [[ $# -gt 0 ]]; do + case "$1" in + --engine-ref) + ENGINE_REF="$2" + shift 2 + ;; + --engine-repo) + ENGINE_REPO="$(cd "$2" && pwd)" + ACTIVE_ENGINE_REPO="$ENGINE_REPO" + shift 2 + ;; + --client-url) + CLIENT_URL="$2" + shift 2 + ;; + --client-port) + CLIENT_PORT="$2" + shift 2 + ;; + --cpu-throttle) + CPU_THROTTLE="$2" + shift 2 + ;; + --repeat) + REPEAT_COUNT="$2" + shift 2 + ;; + --output-root) + OUTPUT_ROOT="$2" + shift 2 + ;; + --internal-presets) + INTERNAL_PRESETS="$2" + shift 2 + ;; + --external-games) + EXTERNAL_GAMES="$2" + shift 2 + ;; + --zoo-dir) + ZOO_DIR="$(cd "$2" && pwd)" + shift 2 + ;; + --zoo-preset) + ZOO_PRESET="$2" + shift 2 + ;; + --zoo-port) + ZOO_PORT="$2" + shift 2 + ;; + --hyfire2-dir) + HYFIRE2_DIR="$(cd "$2" && pwd)" + shift 2 + ;; + --hyfire2-preset) + HYFIRE2_PRESET="$2" + shift 2 + ;; + --hyfire2-port) + HYFIRE2_PORT="$2" + shift 2 + ;; + --keep-worktree) + KEEP_WORKTREE="true" + shift + ;; + --no-instrumentation-overlay) + INSTRUMENTATION_OVERLAY="false" + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + echo "" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [[ ! -d "$ENGINE_REPO/.git" && ! -f "$ENGINE_REPO/.git" ]]; then + echo "Error: $ENGINE_REPO is not a git repo" >&2 + exit 1 +fi + +if ! [[ "$REPEAT_COUNT" =~ ^[1-9][0-9]*$ ]]; then + echo "Error: --repeat must be a positive integer" >&2 + exit 1 +fi + +if [[ "$OUTPUT_ROOT" != /* ]]; then + OUTPUT_ROOT="$INVOCATION_CWD/$OUTPUT_ROOT" +fi + +prepare_engine_checkout() { + bash "$TOOLS_REPO/packages/perf-tools/scripts/ensure-node-modules.sh" \ + --source-repo "$ENGINE_REPO" \ + --target-repo "$ACTIVE_ENGINE_REPO" \ + --packages "server,client,protocol" +} + +apply_instrumentation_overlay() { + if [[ "$INSTRUMENTATION_OVERLAY" != "true" ]]; then + return + fi + + bash "$TOOLS_REPO/packages/perf-tools/scripts/apply-instrumentation-overlay.sh" \ + --source-engine-repo "$TOOLS_REPO" \ + --target-engine-repo "$ACTIVE_ENGINE_REPO" + + if [[ -f "$ACTIVE_ENGINE_REPO/.perf-tools-overlay.json" ]]; then + OVERLAY_MANIFEST="$ACTIVE_ENGINE_REPO/.perf-tools-overlay.json" + else + OVERLAY_MANIFEST="" + fi +} + +start_client_server() { + if [[ -n "$CLIENT_URL" ]]; then + return + fi + + while lsof -iTCP:"$CLIENT_PORT" -sTCP:LISTEN -P -n >/dev/null 2>&1; do + CLIENT_PORT="$((CLIENT_PORT + 1))" + done + + CLIENT_URL="http://localhost:$CLIENT_PORT" + CLIENT_SERVER_LOG="$OUTPUT_DIR/client-dev.log" + + ( + cd "$ACTIVE_ENGINE_REPO/client" + exec setsid npm run dev -- --host 0.0.0.0 --port "$CLIENT_PORT" --strictPort + ) >"$CLIENT_SERVER_LOG" 2>&1 & + CLIENT_SERVER_PID=$! + + for _ in $(seq 1 180); do + if curl -sf "$CLIENT_URL" >/dev/null 2>&1; then + return + fi + + sleep 1 + done + + echo "Error: client dev server did not become healthy at $CLIENT_URL" >&2 + exit 1 +} + +can_run_internal_presets() { + [[ -f "$ACTIVE_ENGINE_REPO/server/src/perf/perf-harness.ts" ]] || return 1 + + node -e "const pkg=require(process.argv[1]); process.exit(pkg.scripts && pkg.scripts['build:perf-harness'] ? 0 : 1)" \ + "$ACTIVE_ENGINE_REPO/server/package.json" >/dev/null 2>&1 +} + +server_supports_action_api() { + if [[ -n "$OVERLAY_MANIFEST" && -f "$OVERLAY_MANIFEST" ]]; then + node -e "const data=require(process.argv[1]); process.exit(data.server?.actionApi ? 0 : 1)" \ + "$OVERLAY_MANIFEST" >/dev/null 2>&1 + return + fi + + [[ -f "$ACTIVE_ENGINE_REPO/server/src/perf/PerfHarness.ts" ]] || return 1 + rg -q '/__perf/action' "$ACTIVE_ENGINE_REPO/server/src/perf/PerfHarness.ts" +} + +preset_requires_server_actions() { + local preset="$1" + local preset_path="$TOOLS_REPO/packages/perf-tools/src/presets/${preset}.yaml" + + [[ -f "$preset_path" ]] || return 1 + + rg -q 'type: (spawn_bots|despawn_bots|load_map|generate_blocks|spawn_entities|despawn_entities|start_block_churn|stop_block_churn|create_worlds|set_default_world|clear_world)' "$preset_path" +} + +resolve_engine_checkout() { + if [[ -z "$ENGINE_REF" ]]; then + ACTIVE_ENGINE_REPO="$ENGINE_REPO" + RESOLVED_COMMIT="$(git -C "$ENGINE_REPO" rev-parse HEAD)" + local branch_name + branch_name="$(git -C "$ENGINE_REPO" rev-parse --abbrev-ref HEAD)" + RESOLVED_LABEL="$(sanitize_slug "${branch_name}-$(git -C "$ENGINE_REPO" rev-parse --short HEAD)")" + return + fi + + local fetch_target="" + + if [[ "$ENGINE_REF" =~ ^pr:[0-9]+$ ]]; then + fetch_target="pull/${ENGINE_REF#pr:}/head" + elif [[ "$ENGINE_REF" =~ ^[0-9]+$ ]]; then + fetch_target="pull/${ENGINE_REF}/head" + fi + + if [[ -n "$fetch_target" ]]; then + fetch_engine_ref "$fetch_target" + RESOLVED_COMMIT="$(git -C "$ENGINE_REPO" rev-parse FETCH_HEAD)" + RESOLVED_LABEL="$(sanitize_slug "pr-${fetch_target#pull/}")" + RESOLVED_LABEL="${RESOLVED_LABEL%-head}" + elif git -C "$ENGINE_REPO" rev-parse --verify "${ENGINE_REF}^{commit}" >/dev/null 2>&1; then + RESOLVED_COMMIT="$(git -C "$ENGINE_REPO" rev-parse "${ENGINE_REF}^{commit}")" + RESOLVED_LABEL="$(sanitize_slug "${ENGINE_REF}-$(git -C "$ENGINE_REPO" rev-parse --short "$RESOLVED_COMMIT")")" + else + fetch_engine_ref "$ENGINE_REF" + RESOLVED_COMMIT="$(git -C "$ENGINE_REPO" rev-parse FETCH_HEAD)" + RESOLVED_LABEL="$(sanitize_slug "${ENGINE_REF}-$(git -C "$ENGINE_REPO" rev-parse --short "$RESOLVED_COMMIT")")" + fi + + WORKTREE_DIR="$(mktemp -d "/tmp/hytopia-owned-stack-${RESOLVED_LABEL}-XXXXXX")" + git -C "$ENGINE_REPO" worktree add --detach "$WORKTREE_DIR" "$RESOLVED_COMMIT" >/dev/null + ACTIVE_ENGINE_REPO="$WORKTREE_DIR" +} + +record_summary_row() { + local label="$1" + local category="$2" + local status="$3" + local output="$4" + + SUMMARY_ROWS+=("| $label | $category | $status | $output |") +} + +run_command() { + local label="$1" + local output_path="$2" + shift 2 + + echo "" + echo "==> Running $label" + echo "Output: $output_path" + + set +e + "$@" + local status=$? + set -e + + if [[ $status -ne 0 ]]; then + echo "WARNING: $label failed with exit code $status" >&2 + fi + + return $status +} + +aggregate_reports() { + local output_path="$1" + shift + + ( + cd "$TOOLS_REPO/packages/perf-tools" + npx tsx src/cli.ts aggregate --output "$output_path" "$@" + ) +} + +run_internal_preset() { + local preset="$1" + local output_path="$OUTPUT_DIR/${preset}.json" + local repeat_dir="$OUTPUT_DIR/repeats/${preset}" + local repeat_output + local -a repeat_outputs=() + local status=0 + local repeat_index + + if (( REPEAT_COUNT <= 1 )); then + local -a cmd=( + npx tsx src/cli.ts run + --preset "$preset" + --server-cwd "$ACTIVE_ENGINE_REPO/server" + --with-client + --client-dev-url "$CLIENT_URL" + --output "$output_path" + ) + + if [[ -n "$CPU_THROTTLE" ]]; then + cmd+=(--cpu-throttle "$CPU_THROTTLE") + fi + + if run_command "$preset" "$output_path" bash -lc "cd '$TOOLS_REPO/packages/perf-tools' && ${cmd[*]@Q}"; then + record_summary_row "$preset" "internal" "0" "$output_path" + return 0 + fi + + record_summary_row "$preset" "internal" "1" "$output_path" + return 1 + fi + + mkdir -p "$repeat_dir" + + for repeat_index in $(seq 1 "$REPEAT_COUNT"); do + repeat_output="$repeat_dir/run-$repeat_index.json" + repeat_outputs+=("$repeat_output") + + local -a repeat_cmd=( + npx tsx src/cli.ts run + --preset "$preset" + --server-cwd "$ACTIVE_ENGINE_REPO/server" + --with-client + --client-dev-url "$CLIENT_URL" + --output "$repeat_output" + ) + + if [[ -n "$CPU_THROTTLE" ]]; then + repeat_cmd+=(--cpu-throttle "$CPU_THROTTLE") + fi + + if ! run_command "$preset (run $repeat_index/$REPEAT_COUNT)" "$repeat_output" bash -lc "cd '$TOOLS_REPO/packages/perf-tools' && ${repeat_cmd[*]@Q}"; then + status=1 + fi + done + + if [[ $status -eq 0 ]]; then + echo "" + echo "==> Aggregating $preset repeats into $output_path" + if ! aggregate_reports "$output_path" "${repeat_outputs[@]}"; then + status=1 + fi + fi + + record_summary_row "$preset" "internal" "$status" "$output_path (median of $REPEAT_COUNT runs)" + return "$status" +} + +run_external_game_preset() { + local label="$1" + local preset="$2" + local output_name="$3" + shift 3 + local output_path="$OUTPUT_DIR/${output_name}.json" + local repeat_dir="$OUTPUT_DIR/repeats/${output_name}" + local repeat_output + local -a repeat_outputs=() + local status=0 + local repeat_index + + if (( REPEAT_COUNT <= 1 )); then + if run_command "$label" "$output_path" "$@" --output "$output_path"; then + record_summary_row "$label" "external" "0" "$output_path" + return 0 + fi + + record_summary_row "$label" "external" "1" "$output_path" + return 1 + fi + + mkdir -p "$repeat_dir" + + for repeat_index in $(seq 1 "$REPEAT_COUNT"); do + repeat_output="$repeat_dir/run-$repeat_index.json" + repeat_outputs+=("$repeat_output") + + if ! run_command "$label (run $repeat_index/$REPEAT_COUNT)" "$repeat_output" "$@" --output "$repeat_output"; then + status=1 + fi + done + + if [[ $status -eq 0 ]]; then + echo "" + echo "==> Aggregating $label repeats into $output_path" + if ! aggregate_reports "$output_path" "${repeat_outputs[@]}"; then + status=1 + fi + fi + + record_summary_row "$label" "external" "$status" "$output_path (median of $REPEAT_COUNT runs)" + return "$status" +} + +write_summary() { + local output_dir="$1" + local internal_display="$2" + local external_display="$3" + local resolved_ref="${ENGINE_REF:-current-checkout}" + + cat > "$SUMMARY_PATH" <> "$SUMMARY_PATH" + done + + cat >> "$SUMMARY_PATH" <&2 + SUMMARY_ROWS+=("| $preset | internal | skipped | server action API unavailable |") + continue + fi + + if ! run_internal_preset "$preset"; then + overall_status=1 + fi + done + else + echo "WARNING: skipping internal presets because $ACTIVE_ENGINE_REPO does not contain perf-harness support" >&2 + for preset in "${INTERNAL_PRESET_ARRAY[@]}"; do + [[ -z "$preset" ]] && continue + SUMMARY_ROWS+=("| $preset | internal | skipped | perf-harness missing |") + done + fi +fi + +if [[ "$EXTERNAL_GAMES" != "none" ]]; then + for game in "${EXTERNAL_GAME_ARRAY[@]}"; do + [[ -z "$game" ]] && continue + + case "$game" in + zoo) + game_cmd=( + bash "$TOOLS_REPO/packages/perf-tools/scripts/run-external-game-benchmark.sh" + --engine-repo "$ACTIVE_ENGINE_REPO" + --game-dir "$ZOO_DIR" + --preset "$ZOO_PRESET" + --client-url "$CLIENT_URL" + --port "$ZOO_PORT" + ) + + if [[ -n "$CPU_THROTTLE" ]]; then + game_cmd+=(--cpu-throttle "$CPU_THROTTLE") + fi + + if ! run_external_game_preset "zoo:$ZOO_PRESET" "$ZOO_PRESET" "$ZOO_PRESET" "${game_cmd[@]}"; then + overall_status=1 + fi + ;; + hyfire2) + game_cmd=( + bash "$TOOLS_REPO/packages/perf-tools/scripts/run-external-game-benchmark.sh" + --engine-repo "$ACTIVE_ENGINE_REPO" + --game-dir "$HYFIRE2_DIR" + --preset "$HYFIRE2_PRESET" + --client-url "$CLIENT_URL" + --server-cmd "$HYFIRE2_SERVER_CMD" + --port "$HYFIRE2_PORT" + ) + + if [[ -n "$CPU_THROTTLE" ]]; then + game_cmd+=(--cpu-throttle "$CPU_THROTTLE") + fi + + if ! run_external_game_preset "hyfire2:$HYFIRE2_PRESET" "$HYFIRE2_PRESET" "$HYFIRE2_PRESET" "${game_cmd[@]}"; then + overall_status=1 + fi + ;; + *) + echo "Unknown external game: $game" >&2 + exit 1 + ;; + esac + done +fi + +write_summary "$OUTPUT_DIR" "$internal_display" "$external_display" + +echo "" +echo "Suite summary: $SUMMARY_PATH" + +exit "$overall_status" diff --git a/packages/perf-tools/scripts/setup-game.sh b/packages/perf-tools/scripts/setup-game.sh new file mode 100755 index 00000000..5bbb2833 --- /dev/null +++ b/packages/perf-tools/scripts/setup-game.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# Usage: ./setup-game.sh +# Links our modified SDK into a game directory +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +DEFAULT_REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +REPO_ROOT="$DEFAULT_REPO_ROOT" +GAME_DIR="" + +usage() { + echo "Usage: $0 [--engine-repo ]" + echo "Examples:" + echo " $0 /home/ab/GitHub/games/hyfire2-sdk-compat" + echo " $0 /home/ab/GitHub/games/hytopia/zoo-game/work1" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --engine-repo) + REPO_ROOT="$(cd "$2" && pwd)" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + if [[ -z "$GAME_DIR" ]]; then + GAME_DIR="$(cd "$1" && pwd)" + shift + else + echo "Unknown argument: $1" >&2 + usage >&2 + exit 1 + fi + ;; + esac +done + +if [[ -z "$GAME_DIR" ]]; then + usage + exit 1 +fi + +if [ ! -f "$GAME_DIR/package.json" ]; then + echo "Error: No package.json found in $GAME_DIR" + exit 1 +fi + +echo "Linking hytopia SDK into $GAME_DIR ..." +cd "$GAME_DIR" + +npm link hytopia + +for SDK_RUNTIME_DEP in \ + "@fails-components/webtransport" \ + "@fails-components/webtransport-transport-http3-quiche" +do + SDK_RUNTIME_SOURCE="$REPO_ROOT/node_modules/$SDK_RUNTIME_DEP" + + if [ ! -e "$SDK_RUNTIME_SOURCE" ]; then + SDK_RUNTIME_SOURCE="$REPO_ROOT/server/node_modules/$SDK_RUNTIME_DEP" + fi + + if [ ! -e "$SDK_RUNTIME_SOURCE" ]; then + echo "Error: runtime dependency $SDK_RUNTIME_DEP is missing from local SDK workspace" + exit 1 + fi + + mkdir -p "$GAME_DIR/node_modules/$(dirname "$SDK_RUNTIME_DEP")" + rm -rf "$GAME_DIR/node_modules/$SDK_RUNTIME_DEP" + ln -s "$SDK_RUNTIME_SOURCE" "$GAME_DIR/node_modules/$SDK_RUNTIME_DEP" +done + +SDK_VERSION=$(node - <<'NODE' +const fs = require('node:fs'); +const path = require('node:path'); + +let dir = path.dirname(require.resolve('hytopia')); + +while (dir !== path.dirname(dir)) { + const pkgPath = path.join(dir, 'package.json'); + + if (fs.existsSync(pkgPath)) { + console.log(JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version); + process.exit(0); + } + + dir = path.dirname(dir); +} + +process.exit(1); +NODE +) +echo "" +echo "Game at $GAME_DIR now using local SDK v${SDK_VERSION}" +echo "" +echo "Launch with:" +echo " HYTOPIA_PERF_TOOLS=1 hytopia start" diff --git a/packages/perf-tools/src/analysis/CpuProfileAnalyzer.ts b/packages/perf-tools/src/analysis/CpuProfileAnalyzer.ts new file mode 100644 index 00000000..6fe23a5b --- /dev/null +++ b/packages/perf-tools/src/analysis/CpuProfileAnalyzer.ts @@ -0,0 +1,212 @@ +export interface ProfileNode { + id: number; + callFrame: { + functionName: string; + scriptId: string; + url: string; + lineNumber: number; + columnNumber: number; + }; + hitCount: number; + children?: number[]; + selfTimeMs: number; + totalTimeMs: number; +} + +export interface HotFunction { + name: string; + url: string; + line: number; + selfTimeMs: number; + totalTimeMs: number; + selfPct: number; + isWasm: boolean; + wasmModule?: string; +} + +export interface CallTreeNode { + name: string; + selfTimeMs: number; + totalTimeMs: number; + children: CallTreeNode[]; +} + +export interface CpuProfileAnalysis { + totalDurationMs: number; + hotFunctions: HotFunction[]; + callTree: CallTreeNode; + wasmFunctions: HotFunction[]; + topScripts: { url: string; selfTimeMs: number; pct: number }[]; +} + +interface RawProfile { + nodes: { + id: number; + callFrame: { + functionName: string; + scriptId: string; + url: string; + lineNumber: number; + columnNumber: number; + }; + hitCount: number; + children?: number[]; + }[]; + startTime: number; + endTime: number; + samples: number[]; + timeDeltas: number[]; +} + +export default class CpuProfileAnalyzer { + private _wasmNameMap: Map = new Map(); + + public setWasmNameMap(map: Map): void { + this._wasmNameMap = map; + } + + public analyze(profile: RawProfile): CpuProfileAnalysis { + const totalDurationMs = (profile.endTime - profile.startTime) / 1000; + const nodeMap = new Map(); + + for (const node of profile.nodes) { + nodeMap.set(node.id, { + ...node, + selfTimeMs: 0, + totalTimeMs: 0, + }); + } + + const sampleTimeUs = profile.timeDeltas; + + for (let i = 0; i < profile.samples.length; i++) { + const nodeId = profile.samples[i]; + const timeMs = (sampleTimeUs[i] ?? 0) / 1000; + const node = nodeMap.get(nodeId); + + if (node) { + node.selfTimeMs += timeMs; + } + } + + this._computeTotalTimes(nodeMap, profile.nodes[0]?.id ?? 1); + + const allNodes = Array.from(nodeMap.values()); + const hotFunctions = this._buildHotFunctions(allNodes, totalDurationMs); + const wasmFunctions = hotFunctions.filter(f => f.isWasm); + const callTree = this._buildCallTree(nodeMap, profile.nodes[0]?.id ?? 1); + const topScripts = this._buildTopScripts(allNodes, totalDurationMs); + + return { + totalDurationMs, + hotFunctions: hotFunctions.slice(0, 50), + callTree, + wasmFunctions, + topScripts, + }; + } + + private _computeTotalTimes(nodeMap: Map, rootId: number): void { + const visited = new Set(); + + const dfs = (id: number): number => { + if (visited.has(id)) return 0; + + visited.add(id); + + const node = nodeMap.get(id); + + if (!node) return 0; + + let total = node.selfTimeMs; + + if (node.children) { + for (const childId of node.children) { + total += dfs(childId); + } + } + + node.totalTimeMs = total; + + return total; + }; + + dfs(rootId); + } + + private _buildHotFunctions(nodes: ProfileNode[], totalMs: number): HotFunction[] { + return nodes + .filter(n => n.selfTimeMs > 0 && n.callFrame.functionName !== '(idle)') + .map(n => { + const isWasm = n.callFrame.url.includes('wasm') || n.callFrame.functionName.startsWith('wasm-'); + const wasmName = isWasm ? this._wasmNameMap.get(n.callFrame.functionName) : undefined; + + return { + name: wasmName ?? n.callFrame.functionName, + url: n.callFrame.url, + line: n.callFrame.lineNumber, + selfTimeMs: n.selfTimeMs, + totalTimeMs: n.totalTimeMs, + selfPct: totalMs > 0 ? (n.selfTimeMs / totalMs) * 100 : 0, + isWasm, + wasmModule: wasmName ? 'rapier3d' : undefined, + }; + }) + .sort((a, b) => b.selfTimeMs - a.selfTimeMs); + } + + private _buildCallTree(nodeMap: Map, rootId: number): CallTreeNode { + const build = (id: number, depth: number): CallTreeNode => { + const node = nodeMap.get(id); + + if (!node || depth > 20) { + return { name: '(unknown)', selfTimeMs: 0, totalTimeMs: 0, children: [] }; + } + + const children: CallTreeNode[] = []; + + if (node.children) { + for (const childId of node.children) { + const child = nodeMap.get(childId); + + if (child && child.totalTimeMs > 0) { + children.push(build(childId, depth + 1)); + } + } + } + + children.sort((a, b) => b.totalTimeMs - a.totalTimeMs); + + return { + name: node.callFrame.functionName || '(anonymous)', + selfTimeMs: node.selfTimeMs, + totalTimeMs: node.totalTimeMs, + children: children.slice(0, 10), + }; + }; + + return build(rootId, 0); + } + + private _buildTopScripts(nodes: ProfileNode[], totalMs: number): { url: string; selfTimeMs: number; pct: number }[] { + const scriptTimes = new Map(); + + for (const node of nodes) { + if (node.selfTimeMs > 0 && node.callFrame.url) { + scriptTimes.set( + node.callFrame.url, + (scriptTimes.get(node.callFrame.url) ?? 0) + node.selfTimeMs, + ); + } + } + + return Array.from(scriptTimes.entries()) + .map(([url, selfTimeMs]) => ({ + url, + selfTimeMs, + pct: totalMs > 0 ? (selfTimeMs / totalMs) * 100 : 0, + })) + .sort((a, b) => b.selfTimeMs - a.selfTimeMs) + .slice(0, 20); + } +} diff --git a/packages/perf-tools/src/analysis/NoiseFilter.ts b/packages/perf-tools/src/analysis/NoiseFilter.ts new file mode 100644 index 00000000..859a1b7e --- /dev/null +++ b/packages/perf-tools/src/analysis/NoiseFilter.ts @@ -0,0 +1,150 @@ +export interface ChangePoint { + index: number; + timestamp?: number; + beforeMean: number; + afterMean: number; + changePct: number; + significance: number; +} + +export type VarianceClassification = 'stable' | 'noisy' | 'trending_up' | 'trending_down' | 'erratic'; + +export default class NoiseFilter { + public removeOutliers(data: number[], iqrMultiplier: number = 1.5): number[] { + if (data.length < 4) return data.slice(); + + const sorted = data.slice().sort((a, b) => a - b); + const q1 = sorted[Math.floor(sorted.length * 0.25)]; + const q3 = sorted[Math.floor(sorted.length * 0.75)]; + const iqr = q3 - q1; + const lower = q1 - iqrMultiplier * iqr; + const upper = q3 + iqrMultiplier * iqr; + + return data.filter(v => v >= lower && v <= upper); + } + + public movingAverage(data: number[], windowSize: number = 5): number[] { + if (data.length === 0) return []; + if (windowSize < 1) windowSize = 1; + if (windowSize > data.length) windowSize = data.length; + + const result: number[] = []; + let windowSum = 0; + + for (let i = 0; i < windowSize; i++) { + windowSum += data[i]; + } + + result.push(windowSum / windowSize); + + for (let i = windowSize; i < data.length; i++) { + windowSum += data[i] - data[i - windowSize]; + result.push(windowSum / windowSize); + } + + return result; + } + + public detectChangePoints(data: number[], minSegmentSize: number = 10, thresholdPct: number = 10): ChangePoint[] { + if (data.length < minSegmentSize * 2) return []; + + const changePoints: ChangePoint[] = []; + + for (let i = minSegmentSize; i <= data.length - minSegmentSize; i++) { + const before = data.slice(i - minSegmentSize, i); + const after = data.slice(i, i + minSegmentSize); + + const beforeMean = this._mean(before); + const afterMean = this._mean(after); + + if (beforeMean === 0) continue; + + const changePct = ((afterMean - beforeMean) / beforeMean) * 100; + + if (Math.abs(changePct) >= thresholdPct) { + const beforeStd = this._std(before); + const afterStd = this._std(after); + const pooledStd = Math.sqrt((beforeStd * beforeStd + afterStd * afterStd) / 2); + const significance = pooledStd > 0 + ? Math.abs(afterMean - beforeMean) / pooledStd + : Math.abs(changePct) / 10; + + if (significance > 1.5) { + const lastCp = changePoints[changePoints.length - 1]; + + if (!lastCp || i - lastCp.index > minSegmentSize) { + changePoints.push({ + index: i, + beforeMean, + afterMean, + changePct, + significance, + }); + } + } + } + } + + return changePoints; + } + + public classifyVariance(data: number[]): VarianceClassification { + if (data.length < 5) return 'stable'; + + const mean = this._mean(data); + const std = this._std(data); + const cv = mean > 0 ? std / mean : 0; + + if (cv > 0.5) return 'erratic'; + + const firstHalf = data.slice(0, Math.floor(data.length / 2)); + const secondHalf = data.slice(Math.floor(data.length / 2)); + const firstMean = this._mean(firstHalf); + const secondMean = this._mean(secondHalf); + + if (firstMean > 0) { + const trendPct = ((secondMean - firstMean) / firstMean) * 100; + + if (trendPct > 10) return 'trending_up'; + if (trendPct < -10) return 'trending_down'; + } + + if (cv > 0.15) return 'noisy'; + + return 'stable'; + } + + public smoothAndAnalyze(data: number[], windowSize: number = 5): { + raw: number[]; + smoothed: number[]; + outlierCount: number; + variance: VarianceClassification; + changePoints: ChangePoint[]; + } { + const cleaned = this.removeOutliers(data); + const smoothed = this.movingAverage(cleaned, windowSize); + + return { + raw: data, + smoothed, + outlierCount: data.length - cleaned.length, + variance: this.classifyVariance(cleaned), + changePoints: this.detectChangePoints(cleaned), + }; + } + + private _mean(data: number[]): number { + if (data.length === 0) return 0; + + return data.reduce((s, v) => s + v, 0) / data.length; + } + + private _std(data: number[]): number { + if (data.length < 2) return 0; + + const mean = this._mean(data); + const variance = data.reduce((s, v) => s + (v - mean) ** 2, 0) / (data.length - 1); + + return Math.sqrt(variance); + } +} diff --git a/packages/perf-tools/src/analysis/SpikeCorrelator.ts b/packages/perf-tools/src/analysis/SpikeCorrelator.ts new file mode 100644 index 00000000..a47eba69 --- /dev/null +++ b/packages/perf-tools/src/analysis/SpikeCorrelator.ts @@ -0,0 +1,135 @@ +import type { SpikeEntry, TickReportEntry } from '../runners/MetricCollector.js'; +import type { GcEvent } from './TraceParser.js'; + +export interface SpikeCause { + type: 'entity_spawn' | 'entity_despawn' | 'gc_pause' | 'network_burst' | 'phase_overrun' | 'unknown'; + confidence: number; + details: string; +} + +export interface SpikeCorrelation { + spike: SpikeEntry; + causes: SpikeCause[]; + primaryCause: SpikeCause; +} + +export default class SpikeCorrelator { + public correlate( + spikes: SpikeEntry[], + tickReports: TickReportEntry[], + gcEvents?: GcEvent[], + ): SpikeCorrelation[] { + return spikes.map(spike => { + const causes: SpikeCause[] = []; + + const entityCause = this._checkEntityChange(spike, tickReports); + + if (entityCause) causes.push(entityCause); + + const phaseCause = this._checkPhaseOverrun(spike); + + if (phaseCause) causes.push(phaseCause); + + if (gcEvents) { + const gcCause = this._checkGcPause(spike, gcEvents); + + if (gcCause) causes.push(gcCause); + } + + if (causes.length === 0) { + causes.push({ + type: 'unknown', + confidence: 0.1, + details: `Spike of ${spike.durationMs.toFixed(1)}ms with no clear correlation`, + }); + } + + causes.sort((a, b) => b.confidence - a.confidence); + + return { + spike, + causes, + primaryCause: causes[0], + }; + }); + } + + private _checkEntityChange(spike: SpikeEntry, tickReports: TickReportEntry[]): SpikeCause | null { + const nearbyReports = tickReports.filter( + r => Math.abs(r.timestamp - spike.timestamp) < 2000, + ); + + if (nearbyReports.length < 2) return null; + + nearbyReports.sort((a, b) => a.timestamp - b.timestamp); + + for (let i = 1; i < nearbyReports.length; i++) { + const delta = nearbyReports[i].entityCount - nearbyReports[i - 1].entityCount; + + if (delta > 10) { + return { + type: 'entity_spawn', + confidence: 0.7, + details: `${delta} entities spawned near spike (${nearbyReports[i - 1].entityCount} -> ${nearbyReports[i].entityCount})`, + }; + } + + if (delta < -10) { + return { + type: 'entity_despawn', + confidence: 0.5, + details: `${Math.abs(delta)} entities despawned near spike`, + }; + } + } + + return null; + } + + private _checkPhaseOverrun(spike: SpikeEntry): SpikeCause | null { + const phases = spike.phases; + + if (!phases || Object.keys(phases).length === 0) return null; + + const entries = Object.entries(phases).sort((a, b) => b[1] - a[1]); + const topPhase = entries[0]; + + if (!topPhase) return null; + + const totalPhaseTime = entries.reduce((s, [, v]) => s + v, 0); + const topPct = totalPhaseTime > 0 ? (topPhase[1] / totalPhaseTime) * 100 : 0; + + if (topPct > 60) { + return { + type: 'phase_overrun', + confidence: 0.8, + details: `Phase "${topPhase[0]}" used ${topPct.toFixed(0)}% of tick time (${topPhase[1].toFixed(1)}ms)`, + }; + } + + return null; + } + + private _checkGcPause(spike: SpikeEntry, gcEvents: GcEvent[]): SpikeCause | null { + const spikeTimeMs = spike.timestamp; + const windowMs = 100; + + const nearbyGc = gcEvents.filter( + gc => Math.abs(gc.startMs - spikeTimeMs) < windowMs, + ); + + if (nearbyGc.length === 0) return null; + + const totalGcMs = nearbyGc.reduce((s, gc) => s + gc.durationMs, 0); + + if (totalGcMs > 2) { + return { + type: 'gc_pause', + confidence: 0.6, + details: `${nearbyGc.length} GC event(s) totaling ${totalGcMs.toFixed(1)}ms near spike`, + }; + } + + return null; + } +} diff --git a/packages/perf-tools/src/analysis/TraceParser.ts b/packages/perf-tools/src/analysis/TraceParser.ts new file mode 100644 index 00000000..5769c597 --- /dev/null +++ b/packages/perf-tools/src/analysis/TraceParser.ts @@ -0,0 +1,160 @@ +export interface TraceEvent { + name: string; + cat: string; + ph: string; + ts: number; + dur?: number; + pid: number; + tid: number; + args?: Record; +} + +export interface FrameTiming { + startMs: number; + durationMs: number; + scriptMs: number; + layoutMs: number; + paintMs: number; +} + +export interface LongTask { + startMs: number; + durationMs: number; + name: string; + category: string; +} + +export interface GcEvent { + startMs: number; + durationMs: number; + type: string; + sizeBeforeBytes?: number; + sizeAfterBytes?: number; +} + +export interface TraceAnalysis { + totalDurationMs: number; + frameCount: number; + avgFrameTimeMs: number; + p95FrameTimeMs: number; + jankFrames: number; + longTasks: LongTask[]; + gcEvents: GcEvent[]; + frameTimes: number[]; + userTimings: { name: string; startMs: number; durationMs: number }[]; +} + +export default class TraceParser { + public parse(traceData: { traceEvents: TraceEvent[] } | TraceEvent[]): TraceAnalysis { + const events = Array.isArray(traceData) ? traceData : traceData.traceEvents; + + if (!events || events.length === 0) { + return this._emptyAnalysis(); + } + + const minTs = Math.min(...events.filter(e => e.ts > 0).map(e => e.ts)); + + const frameTimes = this._extractFrameTimes(events, minTs); + const longTasks = this._extractLongTasks(events, minTs); + const gcEvents = this._extractGcEvents(events, minTs); + const userTimings = this._extractUserTimings(events, minTs); + + const sorted = frameTimes.slice().sort((a, b) => a - b); + const jankThresholdMs = 1000 / 30; + + return { + totalDurationMs: events.length > 0 + ? (Math.max(...events.map(e => e.ts + (e.dur ?? 0))) - minTs) / 1000 + : 0, + frameCount: frameTimes.length, + avgFrameTimeMs: sorted.length > 0 ? sorted.reduce((a, b) => a + b, 0) / sorted.length : 0, + p95FrameTimeMs: sorted.length > 0 ? sorted[Math.floor(sorted.length * 0.95)] : 0, + jankFrames: frameTimes.filter(t => t > jankThresholdMs).length, + longTasks, + gcEvents, + frameTimes, + userTimings, + }; + } + + private _extractFrameTimes(events: TraceEvent[], minTs: number): number[] { + const compositorFrames = events.filter( + e => e.name === 'Compositor::BeginFrame' || e.name === 'BeginFrame', + ); + + if (compositorFrames.length < 2) { + const mainFrames = events.filter(e => e.name === 'BeginMainThreadFrame'); + + if (mainFrames.length < 2) return []; + + const times: number[] = []; + + for (let i = 1; i < mainFrames.length; i++) { + times.push((mainFrames[i].ts - mainFrames[i - 1].ts) / 1000); + } + + return times; + } + + const times: number[] = []; + + for (let i = 1; i < compositorFrames.length; i++) { + times.push((compositorFrames[i].ts - compositorFrames[i - 1].ts) / 1000); + } + + return times; + } + + private _extractLongTasks(events: TraceEvent[], minTs: number): LongTask[] { + const longTaskThresholdUs = 50000; + + return events + .filter(e => e.ph === 'X' && (e.dur ?? 0) > longTaskThresholdUs) + .map(e => ({ + startMs: (e.ts - minTs) / 1000, + durationMs: (e.dur ?? 0) / 1000, + name: e.name, + category: e.cat, + })) + .sort((a, b) => b.durationMs - a.durationMs) + .slice(0, 100); + } + + private _extractGcEvents(events: TraceEvent[], minTs: number): GcEvent[] { + const gcNames = ['V8.GCScavenger', 'V8.GCFinalizeMC', 'V8.GCIncrementalMarking', 'MinorGC', 'MajorGC', 'BlinkGC.AtomicPhase']; + + return events + .filter(e => gcNames.some(n => e.name.includes(n)) && e.ph === 'X') + .map(e => ({ + startMs: (e.ts - minTs) / 1000, + durationMs: (e.dur ?? 0) / 1000, + type: e.name, + sizeBeforeBytes: e.args?.usedHeapSizeBefore as number | undefined, + sizeAfterBytes: e.args?.usedHeapSizeAfter as number | undefined, + })); + } + + private _extractUserTimings(events: TraceEvent[], minTs: number): { name: string; startMs: number; durationMs: number }[] { + return events + .filter(e => e.cat === 'blink.user_timing' && e.ph === 'X') + .map(e => ({ + name: e.name, + startMs: (e.ts - minTs) / 1000, + durationMs: (e.dur ?? 0) / 1000, + })); + } + + private _emptyAnalysis(): TraceAnalysis { + return { + totalDurationMs: 0, + frameCount: 0, + avgFrameTimeMs: 0, + p95FrameTimeMs: 0, + jankFrames: 0, + longTasks: [], + gcEvents: [], + frameTimes: [], + userTimings: [], + }; + } +} diff --git a/packages/perf-tools/src/cli.ts b/packages/perf-tools/src/cli.ts new file mode 100644 index 00000000..6c26a9cb --- /dev/null +++ b/packages/perf-tools/src/cli.ts @@ -0,0 +1,360 @@ +#!/usr/bin/env node + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Command } from 'commander'; +import { loadScenario } from './runners/ScenarioLoader.js'; +import BenchmarkRunner from './runners/BenchmarkRunner.js'; +import BaselineComparer from './runners/BaselineComparer.js'; +import BenchmarkSeriesAggregator from './runners/BenchmarkSeriesAggregator.js'; +import ConsoleReporter from './reporters/ConsoleReporter.js'; +import JsonReporter from './reporters/JsonReporter.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const program = new Command(); + +program + .name('hytopia-bench') + .description('Performance benchmarking tools for HYTOPIA games') + .version('0.1.0'); + +program + .command('run') + .description('Run a benchmark scenario') + .argument('[scenario]', 'Path to scenario YAML/JSON file') + .option('--preset ', 'Use a built-in preset (idle, stress, large-world, many-players, combined, join-storm, block-churn, entity-density, multi-world, blocks-10k-dense, blocks-500k-dense, blocks-1m-dense, blocks-10m-dense, blocks-1m-multi-world, hyfire2-bots, zoo-game-bots, zoo-game-full, zoo-game-observe)') + .option('--output ', 'Write results to JSON file') + .option('--full-data', 'Include raw metric data in output') + .option('--baseline ', 'Compare results against a baseline JSON') + .option('--server-cmd ', 'Command to start the game server') + .option('--server-cwd ', 'Working directory for server') + .option('--client-url ', 'Server base URL (used for health + perf endpoints)', 'https://local.hytopiahosting.com:8080') + .option('--no-headless', 'Run browser in visible mode') + .option('--no-perf-api', 'Skip PerfHarness API, use only OS-level monitoring') + .option('--log-file ', 'Capture server stdout/stderr to file') + .option('--with-client', 'Launch headless browser client and collect client-side metrics') + .option('--client-dev-url ', 'URL for the Vite client dev server', 'http://localhost:4173') + .option('--cpu-throttle ', 'Apply browser CPU throttle rate (1=no throttle, 4=mid mobile, 16=low-end)', parseFloat) + .option('--external-server ', 'Use an already-running external server (skip server startup)') + .option('--verbose', 'Enable verbose logging') + .action(async (scenarioPath, options) => { + let scenario; + + if (options.preset) { + const presetPath = path.join(__dirname, 'presets', `${options.preset}.yaml`); + + if (!fs.existsSync(presetPath)) { + console.error('Unknown preset: %s. Available: idle, stress, large-world, many-players, combined, join-storm, block-churn, entity-density, multi-world, blocks-10k-dense, blocks-500k-dense, blocks-1m-dense, blocks-10m-dense, blocks-1m-multi-world, hyfire2-bots, zoo-game-bots, zoo-game-full, zoo-game-observe', options.preset); + process.exit(1); + } + + scenario = loadScenario(presetPath); + } else if (scenarioPath) { + scenario = loadScenario(scenarioPath); + } else { + console.error('Provide a scenario file or --preset name'); + process.exit(1); + } + + const runner = new BenchmarkRunner({ + serverCommand: options.serverCmd, + serverCwd: options.serverCwd, + clientUrl: options.clientUrl, + clientDevUrl: options.clientDevUrl, + cpuThrottle: options.cpuThrottle, + withClient: options.withClient ?? false, + headless: options.headless !== false, + verbose: options.verbose, + noPerfApi: options.perfApi === false, + logFile: options.logFile, + externalServerUrl: options.externalServer, + }); + + console.log(`Running benchmark: ${scenario.name}`); + + const result = await runner.run(scenario); + + const consoleReporter = new ConsoleReporter(); + + consoleReporter.reportBenchmark(result); + + if (!result.validation.valid) { + process.exitCode = 1; + } + + if (options.baseline && result.validation.valid) { + const baseline = BaselineComparer.loadBaseline(options.baseline); + const comparer = new BaselineComparer(); + const comparison = comparer.compare(baseline, result.baseline, scenario.name); + + consoleReporter.reportComparison(comparison); + + if (comparison.overallStatus === 'fail') { + process.exitCode = 1; + } + } else if (options.baseline && !result.validation.valid) { + console.error('Skipping baseline comparison because the new run is invalid.'); + } + + if (options.output) { + const jsonReporter = new JsonReporter(); + + if (options.fullData) { + jsonReporter.writeFullData(result, options.output); + } else { + const report = jsonReporter.generateReport(result); + + jsonReporter.writeReport(report, options.output); + } + + console.log(`Results written to: ${options.output}`); + } + }); + +program + .command('aggregate') + .description('Aggregate repeated benchmark JSONs into a median report') + .argument('', 'Benchmark report JSON files to aggregate') + .requiredOption('--output ', 'Write the aggregated report to JSON file') + .action((reportPaths, options) => { + const aggregator = new BenchmarkSeriesAggregator(); + const outputPath = options.output; + const absoluteOutputPath = path.isAbsolute(outputPath) ? outputPath : path.resolve(process.cwd(), outputPath); + const report = aggregator.aggregateFiles(reportPaths); + + fs.mkdirSync(path.dirname(absoluteOutputPath), { recursive: true }); + fs.writeFileSync(absoluteOutputPath, JSON.stringify(report, null, 2), 'utf-8'); + + console.log(`Aggregated ${reportPaths.length} reports into: ${absoluteOutputPath}`); + }); + +program + .command('compare') + .description('Compare two baseline files') + .argument('', 'Path to baseline (before) JSON') + .argument('', 'Path to baseline (after) JSON') + .option('--warn ', 'Warning threshold percentage', '5') + .option('--fail ', 'Failure threshold percentage', '15') + .option('--fail-on-regression', 'Exit with code 1 if any metric regresses beyond fail threshold') + .action((beforePath, afterPath, options) => { + const beforeInput = BaselineComparer.loadInput(beforePath); + const afterInput = BaselineComparer.loadInput(afterPath); + const before = beforeInput.baseline; + const after = afterInput.baseline; + + const comparer = new BaselineComparer({ + warningThresholdPct: parseFloat(options.warn), + failThresholdPct: parseFloat(options.fail), + }); + + if (beforeInput.validation?.valid === false || afterInput.validation?.valid === false) { + console.error('Cannot compare invalid benchmark reports.'); + + if (beforeInput.validation?.valid === false) { + console.error(` ${beforePath}`); + for (const issue of beforeInput.validation.issues ?? []) { + console.error(` - ${issue}`); + } + } + + if (afterInput.validation?.valid === false) { + console.error(` ${afterPath}`); + for (const issue of afterInput.validation.issues ?? []) { + console.error(` - ${issue}`); + } + } + + process.exit(1); + } + + const includeServerMetrics = hasServerMetrics(beforeInput) && hasServerMetrics(afterInput); + const usesLegacyServerMetrics = reportUsesLegacyServerMetrics(beforeInput) || reportUsesLegacyServerMetrics(afterInput); + const includeClientMetrics = hasClientMetrics(beforeInput) && hasClientMetrics(afterInput); + const includeClientRenderMetrics = includeClientMetrics && hasClientRenderMetrics(beforeInput) && hasClientRenderMetrics(afterInput); + const includeServerTailMetrics = includeServerMetrics && !usesLegacyServerMetrics; + const includeServerBudgetMetrics = includeServerMetrics && !usesLegacyServerMetrics; + const includeServerNetworkMetrics = includeServerMetrics && !usesLegacyServerMetrics + && before.network !== undefined + && after.network !== undefined; + + if (!includeServerMetrics && !includeClientMetrics) { + console.error('Cannot compare these reports because they do not share any comparable metric categories.'); + process.exit(1); + } + + if (!includeServerMetrics) { + console.log('Skipping server metrics: one or both reports lack server snapshots.'); + } else if (usesLegacyServerMetrics) { + console.log('Skipping server p99, budget, and network metrics: one or both reports used the legacy server perf API.'); + } + + if (!includeClientMetrics) { + console.log('Skipping client metrics: one or both reports lack client snapshots.'); + } else if (!includeClientRenderMetrics) { + console.log('Skipping client draw-call and triangle metrics: one or both reports lack usable render counters.'); + } + + const comparison = comparer.compare( + before, + after, + `${path.basename(beforePath)} vs ${path.basename(afterPath)}`, + { + includeServerMetrics, + includeServerTailMetrics, + includeServerBudgetMetrics, + includeServerNetworkMetrics, + includeClientMetrics, + includeClientRenderMetrics, + }, + ); + + const reporter = new ConsoleReporter(); + + reporter.reportComparison(comparison); + + if (options.failOnRegression && comparison.overallStatus === 'fail') { + process.exitCode = 1; + } + }); + +program + .command('compare-series') + .description('Compare two repeated benchmark sets using median aggregation') + .requiredOption('--before ', 'Comma-separated list of baseline report JSON files') + .requiredOption('--after ', 'Comma-separated list of candidate report JSON files') + .option('--warn ', 'Warning threshold percentage', '5') + .option('--fail ', 'Failure threshold percentage', '15') + .option('--fail-on-regression', 'Exit with code 1 if the aggregate verdict regresses') + .action((options) => { + const beforePaths = splitPathList(options.before); + const afterPaths = splitPathList(options.after); + const aggregator = new BenchmarkSeriesAggregator(); + const beforeAggregate = aggregator.aggregateFiles(beforePaths); + const afterAggregate = aggregator.aggregateFiles(afterPaths); + const comparer = new BaselineComparer({ + warningThresholdPct: parseFloat(options.warn), + failThresholdPct: parseFloat(options.fail), + }); + const includeServerMetrics = hasServerMetrics(beforeAggregate) && hasServerMetrics(afterAggregate); + const usesLegacyServerMetrics = reportUsesLegacyServerMetrics(beforeAggregate) || reportUsesLegacyServerMetrics(afterAggregate); + const includeClientMetrics = hasClientMetrics(beforeAggregate) && hasClientMetrics(afterAggregate); + const includeClientRenderMetrics = includeClientMetrics && hasClientRenderMetrics(beforeAggregate) && hasClientRenderMetrics(afterAggregate); + const includeServerTailMetrics = includeServerMetrics && !usesLegacyServerMetrics; + const includeServerBudgetMetrics = includeServerMetrics && !usesLegacyServerMetrics; + const includeServerNetworkMetrics = includeServerMetrics && !usesLegacyServerMetrics + && beforeAggregate.baseline.network !== undefined + && afterAggregate.baseline.network !== undefined; + + if (!includeServerMetrics && !includeClientMetrics) { + console.error('Cannot compare these report series because they do not share any comparable metric categories.'); + process.exit(1); + } + + if (!includeServerMetrics) { + console.log('Skipping server metrics: one or both report series lack server snapshots.'); + } else if (usesLegacyServerMetrics) { + console.log('Skipping server p99, budget, and network metrics: one or both report series used the legacy server perf API.'); + } + + if (!includeClientMetrics) { + console.log('Skipping client metrics: one or both report series lack client snapshots.'); + } else if (!includeClientRenderMetrics) { + console.log('Skipping client draw-call and triangle metrics: one or both report series lack usable render counters.'); + } + + const comparison = comparer.compare( + beforeAggregate.baseline, + afterAggregate.baseline, + `${beforePaths.length}x before vs ${afterPaths.length}x after`, + { + includeServerMetrics, + includeServerTailMetrics, + includeServerBudgetMetrics, + includeServerNetworkMetrics, + includeClientMetrics, + includeClientRenderMetrics, + }, + ); + const summary = aggregator.classifyComparison(comparison); + const reporter = new ConsoleReporter(); + + reporter.reportComparison(comparison); + console.log(`Series verdict: ${summary.verdict.toUpperCase()}`); + + if (summary.keyEntries.length > 0) { + console.log('Key metrics:'); + for (const entry of summary.keyEntries) { + const change = entry.changePct > 0 ? `+${entry.changePct.toFixed(1)}%` : `${entry.changePct.toFixed(1)}%`; + console.log(` ${entry.metric}: ${entry.baseline.toFixed(2)} -> ${entry.current.toFixed(2)} (${change})`); + } + console.log(''); + } + + if (options.failOnRegression && summary.verdict === 'regresses') { + process.exitCode = 1; + } + }); + +program + .command('presets') + .description('List available built-in presets') + .action(() => { + const presetsDir = path.join(__dirname, 'presets'); + + if (!fs.existsSync(presetsDir)) { + console.log('No presets directory found'); + return; + } + + const files = fs.readdirSync(presetsDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml')); + + console.log('Available presets:'); + + for (const file of files) { + const scenario = loadScenario(path.join(presetsDir, file)); + + console.log(` ${path.basename(file, path.extname(file))}: ${scenario.description ?? scenario.name}`); + } + }); + +program.parse(); + +function hasServerMetrics(input: ReturnType): boolean { + if ((input.metrics?.serverSnapshotCount ?? 0) > 0) { + return true; + } + + return input.baseline.avgTickMs > 0 || Object.keys(input.baseline.operations ?? {}).length > 0 || input.baseline.network !== undefined; +} + +function reportUsesLegacyServerMetrics(input: ReturnType): boolean { + return input.capabilities?.serverMetricSources?.includes('legacy_perf_api') ?? false; +} + +function hasClientMetrics(input: ReturnType): boolean { + if ((input.metrics?.clientSnapshotCount ?? 0) > 0) { + return true; + } + + return input.baseline.client !== undefined || input.baseline.avgFps !== undefined; +} + +function hasClientRenderMetrics(input: ReturnType): boolean { + const client = input.baseline.client; + + if (!client) { + return false; + } + + return client.avgDrawCalls > 0 || client.maxDrawCalls > 0 || client.avgTriangles > 0 || client.maxTriangles > 0; +} + +function splitPathList(value: string): string[] { + return value + .split(',') + .map(item => item.trim()) + .filter(item => item.length > 0) + .map(item => path.isAbsolute(item) ? item : path.resolve(process.cwd(), item)); +} diff --git a/packages/perf-tools/src/index.ts b/packages/perf-tools/src/index.ts new file mode 100644 index 00000000..c9ec38b5 --- /dev/null +++ b/packages/perf-tools/src/index.ts @@ -0,0 +1,33 @@ +// Runners +export { default as BenchmarkRunner } from './runners/BenchmarkRunner.js'; +export type { BenchmarkRunnerOptions, BenchmarkResult, PhaseResult } from './runners/BenchmarkRunner.js'; + +export { loadScenario, parseDuration } from './runners/ScenarioLoader.js'; +export type { Scenario, ScenarioPhase, ScenarioAction, ScenarioThresholds } from './runners/ScenarioLoader.js'; + +export { default as MetricCollector } from './runners/MetricCollector.js'; +export type { CollectedMetrics, ServerSnapshot, ClientSnapshot, TickReportEntry, SpikeEntry, OperationSnapshot } from './runners/MetricCollector.js'; + +export { default as HeadlessClient } from './runners/HeadlessClient.js'; +export type { HeadlessClientOptions } from './runners/HeadlessClient.js'; + +export { default as BaselineComparer } from './runners/BaselineComparer.js'; +export type { BaselineResult, ComparisonEntry, ComparisonResult, BaselineComparerOptions } from './runners/BaselineComparer.js'; + +// Reporters +export { default as ConsoleReporter } from './reporters/ConsoleReporter.js'; +export { default as JsonReporter } from './reporters/JsonReporter.js'; +export type { JsonReport } from './reporters/JsonReporter.js'; + +// Analysis (re-exported when available) +export { default as TraceParser } from './analysis/TraceParser.js'; +export type { TraceEvent, FrameTiming, LongTask, GcEvent, TraceAnalysis } from './analysis/TraceParser.js'; + +export { default as CpuProfileAnalyzer } from './analysis/CpuProfileAnalyzer.js'; +export type { ProfileNode, HotFunction, CallTreeNode, CpuProfileAnalysis } from './analysis/CpuProfileAnalyzer.js'; + +export { default as SpikeCorrelator } from './analysis/SpikeCorrelator.js'; +export type { SpikeCorrelation, SpikeCause } from './analysis/SpikeCorrelator.js'; + +export { default as NoiseFilter } from './analysis/NoiseFilter.js'; +export type { ChangePoint, VarianceClassification } from './analysis/NoiseFilter.js'; diff --git a/packages/perf-tools/src/presets/block-churn.yaml b/packages/perf-tools/src/presets/block-churn.yaml new file mode 100644 index 00000000..dd94f5f7 --- /dev/null +++ b/packages/perf-tools/src/presets/block-churn.yaml @@ -0,0 +1,30 @@ +name: "block-churn" +description: "Continuous block edits to stress colliders + block sync packets" +warmupMs: 5000 +phases: + - name: setup + actions: + - type: clear_world + - type: load_map + mapPath: "assets/maps/boilerplate-small.json" + - type: connect_clients + count: 10 + - name: churn-and-measure + collect: true + actions: + - type: start_block_churn + blocksPerTick: 200 + blockTypeId: 1 + mode: toggle + min: { x: -16, y: 1, z: -16 } + max: { x: 16, y: 5, z: 16 } + duration: 60s +thresholds: + tick_duration_ms: + avg: 14 + p99: 30 + network: + maxBytesPerSecond: 15000000 + memory_mb: + max: 1500 + diff --git a/packages/perf-tools/src/presets/blocks-10k-dense.yaml b/packages/perf-tools/src/presets/blocks-10k-dense.yaml new file mode 100644 index 00000000..76991998 --- /dev/null +++ b/packages/perf-tools/src/presets/blocks-10k-dense.yaml @@ -0,0 +1,26 @@ +name: "blocks-10k-dense" +description: "Generate ~10k blocks (dense fill) and connect clients to measure join chunk sync overhead" +warmupMs: 5000 +phases: + - name: setup-world + actions: + - type: clear_world + - type: generate_blocks + blockCount: 10000 + blockTypeId: 1 + layout: dense + - name: stabilize + duration: 5s + - name: join-and-measure + collect: true + actions: + - type: connect_clients + count: 50 + duration: 30s +thresholds: + tick_duration_ms: + avg: 14 + p99: 30 + memory_mb: + max: 2000 + diff --git a/packages/perf-tools/src/presets/blocks-10m-dense.yaml b/packages/perf-tools/src/presets/blocks-10m-dense.yaml new file mode 100644 index 00000000..edc918f1 --- /dev/null +++ b/packages/perf-tools/src/presets/blocks-10m-dense.yaml @@ -0,0 +1,28 @@ +name: "blocks-10m-dense" +description: "Generate ~10M blocks (dense fill) and connect a client to measure join chunk sync overhead" +warmupMs: 5000 +phases: + - name: setup-world + actions: + - type: clear_world + - type: generate_blocks + blockCount: 10000000 + blockTypeId: 1 + layout: dense + - name: stabilize + duration: 15s + - name: join-and-measure + collect: true + actions: + - type: connect_clients + count: 1 + duration: 60s +thresholds: + tick_duration_ms: + avg: 14 + p99: 30 + network: + maxBytesPerSecond: 80000000 + memory_mb: + max: 4000 + diff --git a/packages/perf-tools/src/presets/blocks-1m-dense.yaml b/packages/perf-tools/src/presets/blocks-1m-dense.yaml new file mode 100644 index 00000000..5d3234c8 --- /dev/null +++ b/packages/perf-tools/src/presets/blocks-1m-dense.yaml @@ -0,0 +1,28 @@ +name: "blocks-1m-dense" +description: "Generate ~1M blocks (dense fill) and connect clients to measure join chunk sync overhead" +warmupMs: 5000 +phases: + - name: setup-world + actions: + - type: clear_world + - type: generate_blocks + blockCount: 1000000 + blockTypeId: 1 + layout: dense + - name: stabilize + duration: 10s + - name: join-and-measure + collect: true + actions: + - type: connect_clients + count: 10 + duration: 60s +thresholds: + tick_duration_ms: + avg: 14 + p99: 30 + network: + maxBytesPerSecond: 80000000 + memory_mb: + max: 3000 + diff --git a/packages/perf-tools/src/presets/blocks-1m-multi-world.yaml b/packages/perf-tools/src/presets/blocks-1m-multi-world.yaml new file mode 100644 index 00000000..03bf6c77 --- /dev/null +++ b/packages/perf-tools/src/presets/blocks-1m-multi-world.yaml @@ -0,0 +1,66 @@ +name: "blocks-1m-multi-world" +description: "Create 4 worlds with ~1M blocks each, then connect clients in bursts per world" +warmupMs: 5000 +phases: + - name: setup-worlds + actions: + - type: clear_world + - type: create_worlds + count: 3 + - type: generate_blocks + worldId: 1 + blockCount: 1000000 + blockTypeId: 1 + layout: dense + - type: generate_blocks + worldId: 2 + blockCount: 1000000 + blockTypeId: 1 + layout: dense + - type: generate_blocks + worldId: 3 + blockCount: 1000000 + blockTypeId: 1 + layout: dense + - type: generate_blocks + worldId: 4 + blockCount: 1000000 + blockTypeId: 1 + layout: dense + - name: stabilize + duration: 10s + - name: joins-and-measure + collect: true + actions: + - type: set_default_world + worldId: 1 + - type: connect_clients + count: 10 + - type: wait + durationMs: 5000 + - type: set_default_world + worldId: 2 + - type: connect_clients + count: 10 + - type: wait + durationMs: 5000 + - type: set_default_world + worldId: 3 + - type: connect_clients + count: 10 + - type: wait + durationMs: 5000 + - type: set_default_world + worldId: 4 + - type: connect_clients + count: 10 + duration: 90s +thresholds: + tick_duration_ms: + avg: 14 + p99: 30 + network: + maxBytesPerSecond: 80000000 + memory_mb: + max: 4000 + diff --git a/packages/perf-tools/src/presets/blocks-500k-dense.yaml b/packages/perf-tools/src/presets/blocks-500k-dense.yaml new file mode 100644 index 00000000..c16b4168 --- /dev/null +++ b/packages/perf-tools/src/presets/blocks-500k-dense.yaml @@ -0,0 +1,28 @@ +name: "blocks-500k-dense" +description: "Generate ~500k blocks (dense fill) and connect clients to measure join chunk sync overhead" +warmupMs: 5000 +phases: + - name: setup-world + actions: + - type: clear_world + - type: generate_blocks + blockCount: 500000 + blockTypeId: 1 + layout: dense + - name: stabilize + duration: 10s + - name: join-and-measure + collect: true + actions: + - type: connect_clients + count: 20 + duration: 60s +thresholds: + tick_duration_ms: + avg: 14 + p99: 30 + network: + maxBytesPerSecond: 80000000 + memory_mb: + max: 2500 + diff --git a/packages/perf-tools/src/presets/combined.yaml b/packages/perf-tools/src/presets/combined.yaml new file mode 100644 index 00000000..ef3a3d84 --- /dev/null +++ b/packages/perf-tools/src/presets/combined.yaml @@ -0,0 +1,31 @@ +name: "combined-stress" +description: "Combined stress test - large world, many bots, block interactions, all at once" +clients: 10 +warmupMs: 10000 +phases: + - name: load-world + actions: + - type: load_map + mapPath: "assets/maps/boilerplate.json" + - name: spawn-all + actions: + - type: spawn_bots + count: 50 + behavior: random_walk + - type: spawn_bots + count: 30 + behavior: chase + - type: spawn_bots + count: 20 + behavior: interact + - name: stabilize + duration: 10s + - name: measure + duration: 120s + collect: true +thresholds: + tick_duration_ms: + avg: 14 + p99: 30 + memory_mb: + max: 1500 diff --git a/packages/perf-tools/src/presets/entity-density.yaml b/packages/perf-tools/src/presets/entity-density.yaml new file mode 100644 index 00000000..17047cb6 --- /dev/null +++ b/packages/perf-tools/src/presets/entity-density.yaml @@ -0,0 +1,30 @@ +name: "entity-density" +description: "500 dynamic block entities (colliders) to stress physics broadphase + entity update scanning" +warmupMs: 5000 +phases: + - name: setup + actions: + - type: clear_world + - type: load_map + mapPath: "assets/maps/boilerplate-small.json" + - type: spawn_entities + count: 500 + kind: block + tag: "perf-tools.entities" + options: + blockTextureUri: "blocks/bricks.png" + blockHalfExtents: { x: 0.5, y: 0.5, z: 0.5 } + rigidBodyOptions: + type: dynamic + - name: stabilize + duration: 10s + - name: measure + collect: true + duration: 60s +thresholds: + tick_duration_ms: + avg: 14 + p99: 30 + memory_mb: + max: 2000 + diff --git a/packages/perf-tools/src/presets/hyfire2-bots.yaml b/packages/perf-tools/src/presets/hyfire2-bots.yaml new file mode 100644 index 00000000..4a2814e6 --- /dev/null +++ b/packages/perf-tools/src/presets/hyfire2-bots.yaml @@ -0,0 +1,13 @@ +name: "hyfire2-5v5-bots" +description: "HyFire2 5v5 bot match — full combat AI, pathfinding, weapons, economy" +warmupMs: 15000 +phases: + - name: gameplay + collect: true + duration: 120s +thresholds: + tick_duration_ms: + avg: 10 + p99: 25 + memory_mb: + max: 500 diff --git a/packages/perf-tools/src/presets/idle.yaml b/packages/perf-tools/src/presets/idle.yaml new file mode 100644 index 00000000..3df52f29 --- /dev/null +++ b/packages/perf-tools/src/presets/idle.yaml @@ -0,0 +1,14 @@ +name: "idle-baseline" +description: "Empty server baseline - measures overhead of the engine with no game logic" +phases: + - name: warmup + duration: 5s + - name: measure + duration: 30s + collect: true +thresholds: + tick_duration_ms: + avg: 2 + p99: 5 + memory_mb: + max: 200 diff --git a/packages/perf-tools/src/presets/join-storm.yaml b/packages/perf-tools/src/presets/join-storm.yaml new file mode 100644 index 00000000..4b2f9965 --- /dev/null +++ b/packages/perf-tools/src/presets/join-storm.yaml @@ -0,0 +1,26 @@ +name: "join-storm" +description: "Preload a large map, then connect many clients at once to stress join chunk sync + serialization" +warmupMs: 5000 +phases: + - name: preload-world + actions: + - type: clear_world + - type: load_map + mapPath: "assets/maps/boilerplate.json" + - name: stabilize + duration: 10s + - name: join-and-measure + collect: true + actions: + - type: connect_clients + count: 100 + duration: 60s +thresholds: + tick_duration_ms: + avg: 14 + p99: 30 + network: + maxBytesPerSecond: 80000000 + memory_mb: + max: 2000 + diff --git a/packages/perf-tools/src/presets/large-world.yaml b/packages/perf-tools/src/presets/large-world.yaml new file mode 100644 index 00000000..fa235bad --- /dev/null +++ b/packages/perf-tools/src/presets/large-world.yaml @@ -0,0 +1,24 @@ +name: "large-world" +description: "Large world with many chunks loaded - tests chunk lattice and physics broadphase" +warmupMs: 10000 +phases: + - name: load-world + actions: + - type: load_map + mapPath: "assets/maps/boilerplate.json" + - name: spawn-bots + actions: + - type: spawn_bots + count: 20 + behavior: random_walk + - name: stabilize + duration: 10s + - name: measure + duration: 60s + collect: true +thresholds: + tick_duration_ms: + avg: 14 + p99: 25 + memory_mb: + max: 1000 diff --git a/packages/perf-tools/src/presets/many-players.yaml b/packages/perf-tools/src/presets/many-players.yaml new file mode 100644 index 00000000..cff13a7b --- /dev/null +++ b/packages/perf-tools/src/presets/many-players.yaml @@ -0,0 +1,21 @@ +name: "many-players" +description: "Simulates 50 headless clients connecting to stress network and serialization" +clients: 50 +warmupMs: 10000 +phases: + - name: connect-clients + duration: 10s + - name: spawn-bots + actions: + - type: spawn_bots + count: 50 + behavior: random_walk + - name: measure + duration: 60s + collect: true +thresholds: + tick_duration_ms: + avg: 14 + p99: 25 + network: + maxBytesPerSecond: 10000000 diff --git a/packages/perf-tools/src/presets/mobile-stress.yaml b/packages/perf-tools/src/presets/mobile-stress.yaml new file mode 100644 index 00000000..d9197fee --- /dev/null +++ b/packages/perf-tools/src/presets/mobile-stress.yaml @@ -0,0 +1,34 @@ +name: "mobile-stress" +description: "Simulate mobile device: 4x CPU throttle + 200 idle entities. Measures client rendering under constrained CPU." +warmupMs: 5000 +phases: + - name: setup + actions: + - type: load_map + mapPath: "assets/maps/boilerplate-small.json" + - type: spawn_bots + count: 200 + behavior: idle + - type: throttle_cpu + rate: 4 + - name: wait-for-world + actions: + - type: wait_for_entities + count: 10 + durationMs: 30000 + - type: wait + durationMs: 8000 + - name: measure + collect: true + duration: 30s +thresholds: + tick_duration_ms: + avg: 12 + p99: 20 + memory_mb: + max: 512 + client: + fps_avg: 5 + draw_calls_max: 200 + triangles_max: 1000000 + frame_time_ms_max: 500 diff --git a/packages/perf-tools/src/presets/multi-world.yaml b/packages/perf-tools/src/presets/multi-world.yaml new file mode 100644 index 00000000..4d04b29f --- /dev/null +++ b/packages/perf-tools/src/presets/multi-world.yaml @@ -0,0 +1,20 @@ +name: "multi-world" +description: "4 concurrent worlds (default + 3) with boilerplate-small to gauge scaling" +warmupMs: 5000 +phases: + - name: setup + actions: + - type: clear_world + - type: create_worlds + count: 3 + mapPath: "assets/maps/boilerplate-small.json" + - name: measure + collect: true + duration: 60s +thresholds: + tick_duration_ms: + avg: 14 + p99: 30 + memory_mb: + max: 4000 + diff --git a/packages/perf-tools/src/presets/stress-walkthrough.yaml b/packages/perf-tools/src/presets/stress-walkthrough.yaml new file mode 100644 index 00000000..60ba1af9 --- /dev/null +++ b/packages/perf-tools/src/presets/stress-walkthrough.yaml @@ -0,0 +1,78 @@ +name: "stress-walkthrough" +description: "Deterministic stress test: 200 idle entities at fixed positions, player walks through them on a repeatable path" +warmupMs: 5000 +phases: + - name: spawn-entities + actions: + - type: load_map + mapPath: "assets/maps/boilerplate-small.json" + # All bots use idle behavior = they stay at their deterministic spawn positions + # (circle around 0,10,0 with radius based on sqrt(count)) + - type: spawn_bots + count: 200 + behavior: idle + - name: wait-for-world + actions: + - type: wait_for_entities + count: 10 + durationMs: 30000 + # Let entities fully load and settle + - type: wait + durationMs: 5000 + - name: walkthrough + collect: true + duration: 45s + actions: + # Face toward entity cluster center (yaw=0 = looking along +Z toward origin) + - type: set_camera + yaw: 0 + pitch: 0.3 + durationMs: 500 + # Walk forward into the entity cluster + - type: walk_player + durationMs: 5000 + options: { key: w } + # Turn right 90 degrees and strafe through entities + - type: set_camera + yaw: -1.57 + pitch: 0.3 + durationMs: 300 + - type: walk_player + durationMs: 4000 + options: { key: w } + # Turn to face back through entities (180 from start) + - type: set_camera + yaw: 3.14 + pitch: 0.3 + durationMs: 300 + - type: walk_player + durationMs: 5000 + options: { key: w } + # Turn left 90 and walk through again + - type: set_camera + yaw: 1.57 + pitch: 0.3 + durationMs: 300 + - type: walk_player + durationMs: 4000 + options: { key: w } + # Face original direction, walk forward again + - type: set_camera + yaw: 0 + pitch: 0.3 + durationMs: 300 + - type: walk_player + durationMs: 5000 + options: { key: w } +thresholds: + tick_duration_ms: + avg: 12 + p95: 15 + p99: 20 + memory_mb: + max: 512 + client: + fps_avg: 15 + draw_calls_max: 200 + triangles_max: 1000000 + frame_time_ms_max: 200 diff --git a/packages/perf-tools/src/presets/stress.yaml b/packages/perf-tools/src/presets/stress.yaml new file mode 100644 index 00000000..4034e9d0 --- /dev/null +++ b/packages/perf-tools/src/presets/stress.yaml @@ -0,0 +1,28 @@ +name: "stress-test" +description: "Stress test with 100 bots performing various actions" +warmupMs: 5000 +phases: + - name: spawn-entities + actions: + - type: load_map + mapPath: "assets/maps/boilerplate-small.json" + - type: spawn_bots + count: 50 + behavior: random_walk + - type: spawn_bots + count: 30 + behavior: chase + - type: spawn_bots + count: 20 + behavior: interact + - name: stabilize + duration: 5s + - name: measure + duration: 60s + collect: true +thresholds: + tick_duration_ms: + avg: 12 + p99: 20 + memory_mb: + max: 500 diff --git a/packages/perf-tools/src/presets/zoo-game-bots.yaml b/packages/perf-tools/src/presets/zoo-game-bots.yaml new file mode 100644 index 00000000..59d30413 --- /dev/null +++ b/packages/perf-tools/src/presets/zoo-game-bots.yaml @@ -0,0 +1,13 @@ +name: "zoo-game-18-bots" +description: "Zoo Game with 18 bots across 3 worlds, plots unlocked, animals placed" +warmupMs: 20000 +phases: + - name: gameplay + collect: true + duration: 120s +thresholds: + tick_duration_ms: + avg: 12 + p99: 30 + memory_mb: + max: 1500 diff --git a/packages/perf-tools/src/presets/zoo-game-full.yaml b/packages/perf-tools/src/presets/zoo-game-full.yaml new file mode 100644 index 00000000..90185e92 --- /dev/null +++ b/packages/perf-tools/src/presets/zoo-game-full.yaml @@ -0,0 +1,66 @@ +name: "zoo-game-full" +description: "Zoo Game walkthrough using /fillzoo on an external game server, with client rendering metrics" +warmupMs: 5000 +phases: + - name: setup-zoo + actions: + - type: send_chat + message: "/fillzoo" + durationMs: 3000 + - name: wait-for-animals + actions: + - type: wait_for_entities + count: 20 + durationMs: 45000 + - type: wait + durationMs: 5000 + - name: walkthrough + collect: true + duration: 45s + actions: + - type: set_camera + yaw: 0 + pitch: 0.3 + durationMs: 500 + - type: walk_player + durationMs: 5000 + options: { key: w } + - type: set_camera + yaw: -1.57 + pitch: 0.3 + durationMs: 300 + - type: walk_player + durationMs: 4000 + options: { key: w } + - type: set_camera + yaw: 3.14 + pitch: 0.3 + durationMs: 300 + - type: walk_player + durationMs: 5000 + options: { key: w } + - type: set_camera + yaw: 1.57 + pitch: 0.3 + durationMs: 300 + - type: walk_player + durationMs: 4000 + options: { key: w } + - type: set_camera + yaw: 0 + pitch: 0.3 + durationMs: 300 + - type: walk_player + durationMs: 5000 + options: { key: w } +thresholds: + tick_duration_ms: + avg: 16 + p99: 30 + memory_mb: + max: 768 + client: + fps_avg: 10 + draw_calls_max: 500 + triangles_max: 3000000 + frame_time_ms_max: 300 diff --git a/packages/perf-tools/src/presets/zoo-game-observe.yaml b/packages/perf-tools/src/presets/zoo-game-observe.yaml new file mode 100644 index 00000000..2b6c795c --- /dev/null +++ b/packages/perf-tools/src/presets/zoo-game-observe.yaml @@ -0,0 +1,90 @@ +name: "zoo-game-observe" +description: "Zoo Game observation run: 5 real benchmark clients join, each /fillzoo their own zoo, then move during a long measured phase" +browserClients: 5 +warmupMs: 5000 +phases: + - name: setup-zoo + actions: + - type: despawn_bots + - type: send_chat + message: "/fillzoo" + durationMs: 3000 + target: all + staggerMs: 750 + - name: wait-for-world + actions: + - type: wait_for_entities + count: 25 + durationMs: 45000 + - type: wait + durationMs: 5000 + - name: walkthrough + collect: true + duration: 5m + actions: + - type: set_camera + yaw: 0 + pitch: 0.3 + durationMs: 500 + target: all + staggerMs: 500 + - type: walk_player + durationMs: 5000 + options: { key: w } + target: all + staggerMs: 500 + - type: set_camera + yaw: -1.57 + pitch: 0.3 + durationMs: 300 + target: all + staggerMs: 500 + - type: walk_player + durationMs: 4000 + options: { key: w } + target: all + staggerMs: 500 + - type: set_camera + yaw: 3.14 + pitch: 0.3 + durationMs: 300 + target: all + staggerMs: 500 + - type: walk_player + durationMs: 5000 + options: { key: w } + target: all + staggerMs: 500 + - type: set_camera + yaw: 1.57 + pitch: 0.3 + durationMs: 300 + target: all + staggerMs: 500 + - type: walk_player + durationMs: 4000 + options: { key: w } + target: all + staggerMs: 500 + - type: set_camera + yaw: 0 + pitch: 0.3 + durationMs: 300 + target: all + staggerMs: 500 + - type: walk_player + durationMs: 5000 + options: { key: w } + target: all + staggerMs: 500 +thresholds: + tick_duration_ms: + avg: 16 + p99: 30 + memory_mb: + max: 768 + client: + fps_avg: 10 + draw_calls_max: 500 + triangles_max: 3000000 + frame_time_ms_max: 300 diff --git a/packages/perf-tools/src/reporters/ConsoleReporter.ts b/packages/perf-tools/src/reporters/ConsoleReporter.ts new file mode 100644 index 00000000..433f0048 --- /dev/null +++ b/packages/perf-tools/src/reporters/ConsoleReporter.ts @@ -0,0 +1,214 @@ +import type { BenchmarkResult } from '../runners/BenchmarkRunner.js'; +import type { ComparisonResult } from '../runners/BaselineComparer.js'; + +export default class ConsoleReporter { + public reportBenchmark(result: BenchmarkResult): void { + console.log(''); + console.log(`=== Benchmark: ${result.scenario.name} ===`); + console.log(`Duration: ${(result.durationMs / 1000).toFixed(1)}s`); + console.log(''); + + const b = result.baseline; + + console.log('Server Tick Performance:'); + console.log(` avg: ${b.avgTickMs.toFixed(2)}ms`); + console.log(` p95: ${b.p95TickMs.toFixed(2)}ms`); + console.log(` p99: ${b.p99TickMs.toFixed(2)}ms`); + console.log(` max: ${b.maxTickMs.toFixed(2)}ms`); + console.log(` over budget: ${b.ticksOverBudgetPct.toFixed(1)}%`); + console.log(''); + + console.log(`Memory: ${b.avgMemoryMb.toFixed(1)}MB avg heap`); + + if (result.processMetrics && result.processMetrics.snapshots.length > 0) { + const pm = result.processMetrics; + console.log(''); + console.log('Process Metrics (OS-level):'); + console.log(` CPU: avg=${pm.avgCpuPct.toFixed(1)}% max=${pm.maxCpuPct.toFixed(1)}%${pm.maxCpuPct > 90 ? ' FAIL' : pm.maxCpuPct > 70 ? ' WARN' : ''}`); + console.log(` RSS: avg=${pm.avgRssMb.toFixed(1)}MB max=${pm.maxRssMb.toFixed(1)}MB`); + console.log(` Threads: max=${pm.maxThreads}`); + console.log(` FDs: max=${pm.maxFds}`); + } + + if (b.client) { + console.log(''); + console.log('Client Performance:'); + console.log(` FPS: avg=${b.client.avgFps.toFixed(1)} min=${b.client.minFps.toFixed(1)}`); + console.log(` Frame time: avg=${b.client.avgFrameTimeMs.toFixed(2)}ms`); + console.log(` Draw calls: avg=${b.client.avgDrawCalls.toFixed(0)} max=${b.client.maxDrawCalls}`); + console.log(` Triangles: avg=${this._formatK(b.client.avgTriangles)} max=${this._formatK(b.client.maxTriangles)}`); + console.log(` Geometries: avg=${b.client.avgGeometries.toFixed(0)}`); + console.log(` Entities: avg=${b.client.avgEntities.toFixed(0)}`); + console.log(` Visible chunks: avg=${b.client.avgVisibleChunks.toFixed(0)}`); + + if (b.client.avgUsedMemoryMb > 0) { + console.log(` JS Heap: avg=${b.client.avgUsedMemoryMb.toFixed(1)}MB`); + } + } else if (b.avgFps !== undefined) { + console.log(`Client FPS: ${b.avgFps.toFixed(1)} avg`); + } + + if (b.network) { + console.log(''); + console.log('Network (server):'); + console.log(` bytes sent: avg=${(b.network.avgBytesSentPerSecond / 1_000_000).toFixed(2)}MB/s max=${(b.network.maxBytesSentPerSecond / 1_000_000).toFixed(2)}MB/s`); + console.log(` bytes recv: avg=${(b.network.avgBytesReceivedPerSecond / 1_000_000).toFixed(2)}MB/s max=${(b.network.maxBytesReceivedPerSecond / 1_000_000).toFixed(2)}MB/s`); + console.log(` totals: sent=${(b.network.totalBytesSent / 1_000_000).toFixed(1)}MB recv=${(b.network.totalBytesReceived / 1_000_000).toFixed(1)}MB players=${b.network.maxConnectedPlayers}`); + console.log(` serialize: avg=${b.network.avgSerializationMs.toFixed(2)}ms compressTotal=${b.network.compressionCountTotal}`); + } + + const opNames = Object.keys(b.operations); + + if (opNames.length > 0) { + console.log(''); + console.log('Operations:'); + + for (const name of opNames.sort()) { + const op = b.operations[name]; + + console.log(` ${name}: avg=${op.avgMs.toFixed(2)}ms p95=${op.p95Ms.toFixed(2)}ms`); + } + } + + console.log(''); + + if (result.scenario.thresholds) { + this._reportThresholds(result); + } + + console.log('Phases:'); + + for (const phase of result.phaseResults) { + console.log(` ${phase.name}: ${(phase.durationMs / 1000).toFixed(1)}s${phase.collected ? ' (collected)' : ''}`); + } + + console.log(''); + + if (result.validation.warnings.length > 0 || result.validation.issues.length > 0) { + console.log('Validation:'); + + for (const warning of result.validation.warnings) { + console.log(` WARN ${warning}`); + } + + for (const issue of result.validation.issues) { + console.log(` FAIL ${issue}`); + } + + console.log(` Overall: ${result.validation.valid ? 'VALID' : 'INVALID'}`); + console.log(''); + } + } + + public reportComparison(comparison: ComparisonResult): void { + console.log(''); + console.log(`=== Comparison: ${comparison.scenarioName} ===`); + console.log(`Thresholds: warning >${comparison.warningThresholdPct}%, fail >${comparison.failThresholdPct}%`); + console.log(`Overall: ${this._statusIcon(comparison.overallStatus)} ${comparison.overallStatus.toUpperCase()}`); + console.log(''); + + const maxNameLen = Math.max(...comparison.entries.map(e => e.metric.length)); + + for (const entry of comparison.entries) { + const name = entry.metric.padEnd(maxNameLen); + const icon = this._statusIcon(entry.status); + const change = entry.changePct > 0 ? `+${entry.changePct.toFixed(1)}%` : `${entry.changePct.toFixed(1)}%`; + + console.log(` ${icon} ${name} ${entry.baseline.toFixed(2)} -> ${entry.current.toFixed(2)} (${change})`); + } + + console.log(''); + } + + private _reportThresholds(result: BenchmarkResult): void { + const t = result.scenario.thresholds!; + const b = result.baseline; + let allPass = true; + + console.log('Threshold Checks:'); + + if (t.tick_duration_ms) { + if (t.tick_duration_ms.avg !== undefined) { + const pass = b.avgTickMs <= t.tick_duration_ms.avg; + + allPass = allPass && pass; + console.log(` ${pass ? 'PASS' : 'FAIL'} tick avg ${b.avgTickMs.toFixed(2)}ms <= ${t.tick_duration_ms.avg}ms`); + } + + if (t.tick_duration_ms.p99 !== undefined) { + const pass = b.p99TickMs <= t.tick_duration_ms.p99; + + allPass = allPass && pass; + console.log(` ${pass ? 'PASS' : 'FAIL'} tick p99 ${b.p99TickMs.toFixed(2)}ms <= ${t.tick_duration_ms.p99}ms`); + } + } + + if (t.memory_mb?.max !== undefined) { + const pass = b.avgMemoryMb <= t.memory_mb.max; + + allPass = allPass && pass; + console.log(` ${pass ? 'PASS' : 'FAIL'} memory ${b.avgMemoryMb.toFixed(1)}MB <= ${t.memory_mb.max}MB`); + } + + if (t.client && b.client) { + const ct = t.client; + + if (ct.fps_min !== undefined) { + const pass = b.client.minFps >= ct.fps_min; + + allPass = allPass && pass; + console.log(` ${pass ? 'PASS' : 'FAIL'} client minFps ${b.client.minFps.toFixed(1)} >= ${ct.fps_min}`); + } + + if (ct.fps_avg !== undefined) { + const pass = b.client.avgFps >= ct.fps_avg; + + allPass = allPass && pass; + console.log(` ${pass ? 'PASS' : 'FAIL'} client avgFps ${b.client.avgFps.toFixed(1)} >= ${ct.fps_avg}`); + } + + if (ct.draw_calls_max !== undefined) { + const pass = b.client.maxDrawCalls <= ct.draw_calls_max; + + allPass = allPass && pass; + console.log(` ${pass ? 'PASS' : 'FAIL'} client maxDrawCalls ${b.client.maxDrawCalls} <= ${ct.draw_calls_max}`); + } + + if (ct.triangles_max !== undefined) { + const pass = b.client.maxTriangles <= ct.triangles_max; + + allPass = allPass && pass; + console.log(` ${pass ? 'PASS' : 'FAIL'} client maxTriangles ${b.client.maxTriangles} <= ${ct.triangles_max}`); + } + + if (ct.frame_time_ms_max !== undefined) { + const pass = b.client.avgFrameTimeMs <= ct.frame_time_ms_max; + + allPass = allPass && pass; + console.log(` ${pass ? 'PASS' : 'FAIL'} client avgFrameTime ${b.client.avgFrameTimeMs.toFixed(2)}ms <= ${ct.frame_time_ms_max}ms`); + } + } + + if (t.network?.maxBytesPerSecond !== undefined && b.network) { + const pass = b.network.maxBytesSentPerSecond <= t.network.maxBytesPerSecond; + + allPass = allPass && pass; + console.log(` ${pass ? 'PASS' : 'FAIL'} net maxBytesSent ${(b.network.maxBytesSentPerSecond / 1_000_000).toFixed(2)}MB/s <= ${(t.network.maxBytesPerSecond / 1_000_000).toFixed(2)}MB/s`); + } + + console.log(` Overall: ${allPass ? 'ALL PASS' : 'SOME FAILED'}`); + console.log(''); + } + + private _formatK(n: number): string { + return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : n.toFixed(0); + } + + private _statusIcon(status: 'pass' | 'warning' | 'fail'): string { + switch (status) { + case 'pass': return 'OK'; + case 'warning': return 'WARN'; + case 'fail': return 'FAIL'; + } + } +} diff --git a/packages/perf-tools/src/reporters/JsonReporter.ts b/packages/perf-tools/src/reporters/JsonReporter.ts new file mode 100644 index 00000000..93e5015b --- /dev/null +++ b/packages/perf-tools/src/reporters/JsonReporter.ts @@ -0,0 +1,76 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import type { BenchmarkResult } from '../runners/BenchmarkRunner.js'; +import type { ComparisonResult } from '../runners/BaselineComparer.js'; + +export interface JsonReport { + timestamp: string; + scenario: string; + durationMs: number; + baseline: object; + phases: object[]; + comparison?: object; + metrics?: { + tickReportCount: number; + spikeCount: number; + serverSnapshotCount: number; + clientSnapshotCount: number; + }; + capabilities?: { + serverMetrics: boolean; + serverMetricSources: string[]; + clientMetrics: boolean; + clientMetricSources: string[]; + }; + validation?: { + valid: boolean; + warnings: string[]; + issues: string[]; + }; +} + +export default class JsonReporter { + public generateReport(result: BenchmarkResult, comparison?: ComparisonResult): JsonReport { + return { + timestamp: new Date().toISOString(), + scenario: result.scenario.name, + durationMs: result.durationMs, + baseline: result.baseline, + phases: result.phaseResults, + comparison: comparison ?? undefined, + metrics: { + tickReportCount: result.metrics.tickReports.length, + spikeCount: result.metrics.spikes.length, + serverSnapshotCount: result.metrics.serverSnapshots.length, + clientSnapshotCount: result.metrics.clientSnapshots.length, + }, + capabilities: { + serverMetrics: result.capabilities.serverMetrics, + serverMetricSources: result.capabilities.serverMetricSources, + clientMetrics: result.capabilities.clientMetrics, + clientMetricSources: result.capabilities.clientMetricSources, + }, + validation: result.validation, + }; + } + + public writeReport(report: JsonReport, outputPath: string): void { + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + fs.writeFileSync(outputPath, JSON.stringify(report, null, 2), 'utf-8'); + } + + public writeFullData(result: BenchmarkResult, outputPath: string): void { + const data = { + ...this.generateReport(result), + rawMetrics: { + serverSnapshots: result.metrics.serverSnapshots, + clientSnapshots: result.metrics.clientSnapshots, + tickReports: result.metrics.tickReports, + spikes: result.metrics.spikes, + }, + }; + + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + fs.writeFileSync(outputPath, JSON.stringify(data, null, 2), 'utf-8'); + } +} diff --git a/packages/perf-tools/src/runners/BaselineComparer.ts b/packages/perf-tools/src/runners/BaselineComparer.ts new file mode 100644 index 00000000..34b4e4f2 --- /dev/null +++ b/packages/perf-tools/src/runners/BaselineComparer.ts @@ -0,0 +1,223 @@ +import * as fs from 'node:fs'; + +export interface BaselineResult { + avgTickMs: number; + maxTickMs: number; + p95TickMs: number; + p99TickMs: number; + ticksOverBudgetPct: number; + avgMemoryMb: number; + avgFps?: number; + client?: { + avgFps: number; + minFps: number; + avgFrameTimeMs: number; + avgDrawCalls: number; + maxDrawCalls: number; + avgTriangles: number; + maxTriangles: number; + avgGeometries: number; + avgEntities: number; + avgVisibleChunks: number; + avgUsedMemoryMb: number; + }; + operations: Record; + network?: { + totalBytesSent: number; + totalBytesReceived: number; + maxConnectedPlayers: number; + avgBytesSentPerSecond: number; + maxBytesSentPerSecond: number; + avgBytesReceivedPerSecond: number; + maxBytesReceivedPerSecond: number; + avgPacketsSentPerSecond: number; + maxPacketsSentPerSecond: number; + avgPacketsReceivedPerSecond: number; + maxPacketsReceivedPerSecond: number; + avgSerializationMs: number; + compressionCountTotal: number; + }; +} + +export interface ComparisonEntry { + metric: string; + baseline: number; + current: number; + changePct: number; + status: 'pass' | 'warning' | 'fail'; +} + +export interface ComparisonResult { + scenarioName: string; + entries: ComparisonEntry[]; + overallStatus: 'pass' | 'warning' | 'fail'; + warningThresholdPct: number; + failThresholdPct: number; +} + +export interface BaselineComparerOptions { + warningThresholdPct?: number; + failThresholdPct?: number; +} + +export interface ComparisonScope { + includeServerMetrics?: boolean; + includeServerTailMetrics?: boolean; + includeServerBudgetMetrics?: boolean; + includeServerNetworkMetrics?: boolean; + includeClientMetrics?: boolean; + includeClientRenderMetrics?: boolean; +} + +export interface LoadedBenchmarkInput { + baseline: BaselineResult; + metrics?: { + tickReportCount?: number; + spikeCount?: number; + serverSnapshotCount?: number; + clientSnapshotCount?: number; + }; + validation?: { + valid?: boolean; + warnings?: string[]; + issues?: string[]; + }; + capabilities?: { + serverMetrics?: boolean; + serverMetricSources?: string[]; + clientMetrics?: boolean; + clientMetricSources?: string[]; + }; +} + +export default class BaselineComparer { + private _warningPct: number; + private _failPct: number; + + constructor(options?: BaselineComparerOptions) { + this._warningPct = options?.warningThresholdPct ?? 5; + this._failPct = options?.failThresholdPct ?? 15; + } + + public compare( + baseline: BaselineResult, + current: BaselineResult, + scenarioName: string = 'benchmark', + scope?: ComparisonScope, + ): ComparisonResult { + const entries: ComparisonEntry[] = []; + const includeServerMetrics = scope?.includeServerMetrics ?? true; + const includeServerTailMetrics = scope?.includeServerTailMetrics ?? includeServerMetrics; + const includeServerBudgetMetrics = scope?.includeServerBudgetMetrics ?? includeServerMetrics; + const includeServerNetworkMetrics = scope?.includeServerNetworkMetrics ?? includeServerMetrics; + const includeClientMetrics = scope?.includeClientMetrics ?? true; + const includeClientRenderMetrics = scope?.includeClientRenderMetrics ?? includeClientMetrics; + + if (includeServerMetrics) { + entries.push(this._compareMetric('avgTickMs', baseline.avgTickMs, current.avgTickMs)); + entries.push(this._compareMetric('maxTickMs', baseline.maxTickMs, current.maxTickMs)); + entries.push(this._compareMetric('p95TickMs', baseline.p95TickMs, current.p95TickMs)); + entries.push(this._compareMetric('avgMemoryMb', baseline.avgMemoryMb, current.avgMemoryMb)); + + if (includeServerTailMetrics) { + entries.push(this._compareMetric('p99TickMs', baseline.p99TickMs, current.p99TickMs)); + } + + if (includeServerBudgetMetrics) { + entries.push(this._compareMetric('ticksOverBudgetPct', baseline.ticksOverBudgetPct, current.ticksOverBudgetPct)); + } + } + + if (includeClientMetrics && baseline.avgFps !== undefined && current.avgFps !== undefined) { + entries.push(this._compareMetric('avgFps', baseline.avgFps, current.avgFps, true)); + } + + if (includeClientMetrics && baseline.client && current.client) { + entries.push(this._compareMetric('client.avgFps', baseline.client.avgFps, current.client.avgFps, true)); + entries.push(this._compareMetric('client.minFps', baseline.client.minFps, current.client.minFps, true)); + entries.push(this._compareMetric('client.avgFrameTimeMs', baseline.client.avgFrameTimeMs, current.client.avgFrameTimeMs)); + + if (includeClientRenderMetrics) { + entries.push(this._compareMetric('client.avgDrawCalls', baseline.client.avgDrawCalls, current.client.avgDrawCalls)); + entries.push(this._compareMetric('client.avgTriangles', baseline.client.avgTriangles, current.client.avgTriangles)); + } + } + + if (includeServerMetrics && includeServerNetworkMetrics && baseline.network && current.network) { + entries.push(this._compareMetric('net.maxBytesSentPerSecond', baseline.network.maxBytesSentPerSecond, current.network.maxBytesSentPerSecond)); + entries.push(this._compareMetric('net.avgBytesSentPerSecond', baseline.network.avgBytesSentPerSecond, current.network.avgBytesSentPerSecond)); + entries.push(this._compareMetric('net.avgSerializationMs', baseline.network.avgSerializationMs, current.network.avgSerializationMs)); + } + + const allBaselineOps = includeServerMetrics + ? new Set([...Object.keys(baseline.operations ?? {}), ...Object.keys(current.operations ?? {})]) + : new Set(); + + for (const op of allBaselineOps) { + if (baseline.operations?.[op] && current.operations?.[op]) { + entries.push(this._compareMetric(`ops.${op}.avgMs`, baseline.operations[op].avgMs, current.operations[op].avgMs)); + entries.push(this._compareMetric(`ops.${op}.p95Ms`, baseline.operations[op].p95Ms, current.operations[op].p95Ms)); + } + } + + const overallStatus = entries.some(e => e.status === 'fail') + ? 'fail' + : entries.some(e => e.status === 'warning') + ? 'warning' + : 'pass'; + + return { + scenarioName, + entries, + overallStatus, + warningThresholdPct: this._warningPct, + failThresholdPct: this._failPct, + }; + } + + public static loadInput(filePath: string): LoadedBenchmarkInput { + const content = fs.readFileSync(filePath, 'utf-8'); + const data = JSON.parse(content); + + if (data.baseline && typeof data.baseline === 'object' && 'avgTickMs' in data.baseline) { + return { + baseline: data.baseline as BaselineResult, + metrics: data.metrics, + validation: data.validation, + capabilities: data.capabilities, + }; + } + + return { + baseline: data as BaselineResult, + }; + } + + public static loadBaseline(filePath: string): BaselineResult { + return BaselineComparer.loadInput(filePath).baseline; + } + + public static saveBaseline(filePath: string, baseline: BaselineResult): void { + fs.writeFileSync(filePath, JSON.stringify(baseline, null, 2), 'utf-8'); + } + + private _compareMetric(name: string, baseline: number, current: number, lowerIsBetter: boolean = false): ComparisonEntry { + if (baseline === 0) { + return { metric: name, baseline, current, changePct: 0, status: 'pass' }; + } + + const changePct = lowerIsBetter + ? ((baseline - current) / baseline) * 100 + : ((current - baseline) / baseline) * 100; + + let status: 'pass' | 'warning' | 'fail' = 'pass'; + + if (changePct > this._failPct) { + status = 'fail'; + } else if (changePct > this._warningPct) { + status = 'warning'; + } + + return { metric: name, baseline, current, changePct, status }; + } +} diff --git a/packages/perf-tools/src/runners/BenchmarkRunner.ts b/packages/perf-tools/src/runners/BenchmarkRunner.ts new file mode 100644 index 00000000..36575dec --- /dev/null +++ b/packages/perf-tools/src/runners/BenchmarkRunner.ts @@ -0,0 +1,895 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import * as net from 'node:net'; +import { spawn, type ChildProcess } from 'node:child_process'; +import HeadlessClient from './HeadlessClient.js'; +import MetricCollector, { type CollectedMetrics } from './MetricCollector.js'; +import ProcessMonitor, { type ProcessMetrics } from './ProcessMonitor.js'; +import ServerApiClient from './ServerApiClient.js'; +import WsClient from './WsClient.js'; +import { type Scenario, type ScenarioAction, type ScenarioClientTarget, type ScenarioPhase, parseDuration } from './ScenarioLoader.js'; +import type { BaselineResult } from './BaselineComparer.js'; + +export interface BenchmarkRunnerOptions { + serverCommand?: string; + serverCwd?: string; + clientUrl?: string; + clientDevUrl?: string; + cpuThrottle?: number; + withClient?: boolean; + headless?: boolean; + verbose?: boolean; + noPerfApi?: boolean; + logFile?: string; + /** Skip server startup and connect to an already-running server at this URL */ + externalServerUrl?: string; +} + +export interface BenchmarkResult { + scenario: Scenario; + metrics: CollectedMetrics; + baseline: BaselineResult; + processMetrics?: ProcessMetrics; + durationMs: number; + phaseResults: PhaseResult[]; + capabilities: BenchmarkCapabilities; + validation: BenchmarkValidation; +} + +export interface PhaseResult { + name: string; + durationMs: number; + collected: boolean; +} + +export interface BenchmarkCapabilities { + serverMetrics: boolean; + serverMetricSources: Array<'perf_harness' | 'legacy_perf_api'>; + clientMetrics: boolean; + clientMetricSources: Array<'perf_bridge' | 'webgl_fallback'>; +} + +export interface BenchmarkValidation { + valid: boolean; + warnings: string[]; + issues: string[]; +} + +export default class BenchmarkRunner { + private _options: Required; + private _collector: MetricCollector; + private _processMonitor: ProcessMonitor; + private _serverProcess: ChildProcess | null = null; + private _headlessClients: HeadlessClient[] = []; + private _serverApi: ServerApiClient; + private _wsClients: WsClient[] = []; + private _perfApiAvailable: boolean = true; + private _logStream: fs.WriteStream | null = null; + private _log: (msg: string) => void; + + constructor(options?: BenchmarkRunnerOptions) { + this._options = { + serverCommand: options?.serverCommand ?? 'npm run build:perf-harness && node src/perf-harness.mjs', + serverCwd: options?.serverCwd ?? resolveDefaultServerCwd(process.cwd()), + clientUrl: options?.clientUrl ?? 'https://local.hytopiahosting.com:8080', + clientDevUrl: options?.clientDevUrl ?? '', + cpuThrottle: options?.cpuThrottle ?? 1, + withClient: options?.withClient ?? false, + headless: options?.headless ?? true, + verbose: options?.verbose ?? false, + noPerfApi: options?.noPerfApi ?? false, + logFile: options?.logFile ?? '', + externalServerUrl: options?.externalServerUrl ?? '', + }; + this._collector = new MetricCollector(); + this._processMonitor = new ProcessMonitor(); + this._serverApi = new ServerApiClient(this._options.clientUrl); + this._perfApiAvailable = !this._options.noPerfApi; + this._log = this._options.verbose ? console.log : () => {}; + } + + public async run(scenario: Scenario): Promise { + const startTime = Date.now(); + const phaseResults: PhaseResult[] = []; + + this._log(`[bench] Starting scenario: ${scenario.name}`); + + let processMetrics: ProcessMetrics | undefined; + + try { + if (this._options.externalServerUrl) { + // Use external server — skip startup, just set the URL + this._options.clientUrl = this._options.externalServerUrl; + this._serverApi = new ServerApiClient(this._options.externalServerUrl); + this._log(`[bench] Using external server: ${this._options.externalServerUrl}`); + } else { + await this._startServer(); + + // start process monitor as soon as server PID is available + if (this._serverProcess?.pid) { + this._log(`[bench] Starting process monitor (pid=${this._serverProcess.pid})`); + this._processMonitor.start(this._serverProcess.pid); + } + } + + await this._serverApi.waitForHealthy(); + + // Launch headless browser clients if configured + if (this._options.withClient && this._options.clientDevUrl) { + try { + const serverUrl = new URL(this._options.clientUrl); + const clientCount = Math.max(1, Math.floor(scenario.browserClients ?? 1)); + + this._log(`[bench] Launching ${clientCount} headless client(s): ${this._options.clientDevUrl}`); + + for (let i = 0; i < clientCount; i++) { + const client = new HeadlessClient({ + url: this._options.clientDevUrl, + headless: this._options.headless, + }); + + this._headlessClients.push(client); + await client.launch(); + + this._log(`[bench] Warming up server HTTPS cert in headless browser ${i + 1}/${clientCount}...`); + await client.warmCert(this._options.clientUrl); + + const clientNavUrl = new URL(this._options.clientDevUrl); + + clientNavUrl.searchParams.set('join', serverUrl.host); + + await client.navigate(clientNavUrl.toString()); + + const perfReady = await client.waitForPerfReady(30000); + + if (!perfReady && i === 0) { + this._log('[bench] WARNING: Client perf bridge not ready after 30s — client metrics may be unavailable'); + } else if (perfReady && i === 0) { + this._log('[bench] Client perf bridge ready'); + } + + if (this._options.cpuThrottle > 1) { + this._log(`[bench] Applying startup CPU throttle: ${this._options.cpuThrottle}x to client ${i + 1}/${clientCount}`); + await client.setCpuThrottle(this._options.cpuThrottle); + } + } + } catch (err: any) { + this._log(`[bench] WARNING: Headless client failed to launch: ${err?.message ?? err}`); + await this._closeHeadlessClients(); + } + } + + // probe PerfHarness availability unless explicitly disabled + if (this._perfApiAvailable) { + this._perfApiAvailable = await this._probePerfApi(); + if (!this._perfApiAvailable) { + this._log('[bench] PerfHarness API unavailable — using OS-level monitoring only'); + } + } + + if (scenario.clients && scenario.clients > 0) { + await this._launchWsClients(scenario.clients); + } + + if (scenario.warmupMs) { + this._log(`[bench] Warming up for ${scenario.warmupMs}ms`); + await this._wait(scenario.warmupMs); + } + + for (const phase of scenario.phases) { + const phaseResult = await this._runPhase(phase); + + phaseResults.push(phaseResult); + } + } finally { + processMetrics = this._processMonitor.stop(); + await this._cleanup(); + } + + const metrics = this._collector.stopCollecting(); + const baseline = this._buildBaseline(metrics); + const capabilities = this._buildCapabilities(metrics); + const validation = this._buildValidation(metrics, capabilities, scenario); + + return { + scenario, + metrics, + baseline, + processMetrics, + durationMs: Date.now() - startTime, + phaseResults, + capabilities, + validation, + }; + } + + private async _runPhase(phase: ScenarioPhase): Promise { + const startTime = Date.now(); + + this._log(`[bench] Phase: ${phase.name}`); + + if (phase.collect) { + if (this._perfApiAvailable) { + try { + await this._serverApi.reset(); + } catch { + this._log('[bench] PerfHarness reset failed — continuing without it'); + this._perfApiAvailable = false; + } + } + this._collector.startCollecting(); + } + + if (phase.actions) { + for (const action of phase.actions) { + this._log(`[bench] Action: ${action.type}`); + + switch (action.type) { + case 'wait': + if (action.durationMs) { + await this._wait(action.durationMs); + } + break; + case 'spawn_bots': + await this._serverApi.action({ + type: 'spawn_bots', + count: action.count ?? 0, + behavior: action.behavior, + origin: action.origin, + }); + break; + case 'despawn_bots': + await this._serverApi.action({ + type: 'despawn_bots', + count: action.count, + }); + break; + case 'load_map': + await this._serverApi.action({ + type: 'load_map', + mapPath: action.mapPath ?? '', + worldId: typeof action.worldId === 'number' ? action.worldId : undefined, + }); + break; + case 'generate_blocks': + await this._serverApi.action({ + type: 'generate_blocks', + blockCount: action.blockCount ?? 0, + blockTypeId: action.blockTypeId ?? 1, + worldId: typeof action.worldId === 'number' ? action.worldId : undefined, + layout: action.layout, + slabHeight: action.slabHeight, + origin: action.origin, + clear: action.clear, + }); + break; + case 'spawn_entities': + await this._serverApi.action({ + type: 'spawn_entities', + count: action.count ?? 0, + kind: action.kind, + options: action.options, + tag: action.tag, + }); + break; + case 'despawn_entities': + await this._serverApi.action({ + type: 'despawn_entities', + tag: action.tag, + }); + break; + case 'start_block_churn': + await this._serverApi.action({ + type: 'start_block_churn', + blocksPerTick: action.blocksPerTick ?? 0, + blockTypeId: action.blockTypeId ?? 0, + mode: action.mode, + min: action.min, + max: action.max, + }); + break; + case 'stop_block_churn': + await this._serverApi.action({ + type: 'stop_block_churn', + }); + break; + case 'create_worlds': + await this._serverApi.action({ + type: 'create_worlds', + count: action.count ?? 0, + mapPath: action.mapPath, + setDefault: action.setDefault, + }); + break; + case 'set_default_world': + await this._serverApi.action({ + type: 'set_default_world', + worldId: action.worldId ?? 0, + }); + break; + case 'clear_world': + await this._serverApi.action({ + type: 'clear_world', + }); + break; + case 'connect_clients': + await this._launchWsClients(action.count ?? 0, action.staggerMs); + break; + case 'disconnect_clients': + await this._disconnectWsClients(action.count); + break; + case 'wait_for_entities': + if (this._primaryHeadlessClient) { + const minEntities = action.count ?? 1; + const timeout = action.durationMs ?? 30000; + + this._log(`[bench] Waiting for ${minEntities}+ entities (timeout ${timeout}ms)`); + + const start = Date.now(); + + while (Date.now() - start < timeout) { + const snap = await this._primaryHeadlessClient.collectClientMetrics(); + + if (snap?.entities && snap.entities.count >= minEntities) { + this._log(`[bench] Got ${snap.entities.count} entities`); + break; + } + + await new Promise(r => setTimeout(r, 1000)); + } + } + break; + case 'walk_player': + if (this._headlessClients.length > 0) { + const key = (action.options?.key as string) ?? 'w'; + const dur = action.durationMs ?? 3000; + + this._log(`[bench] Walking player(s): target=${action.target ?? 'primary'} key=${key} for ${dur}ms`); + + try { + await this._runOnHeadlessClients(action, client => client.sendMovement(key, dur)); + } catch { + this._log('[bench] walk_player: input failed (non-fatal)'); + } + } + break; + case 'set_camera': + if (this._headlessClients.length > 0) { + const yaw = action.yaw ?? 0; + const pitch = action.pitch ?? -0.3; + + this._log(`[bench] Setting camera(s): target=${action.target ?? 'primary'} yaw=${yaw} pitch=${pitch}`); + + try { + await this._runOnHeadlessClients(action, async client => { + if (action.position) { + await client.setCameraPosition(action.position.x, action.position.y, action.position.z); + } + + await client.lookAt(yaw, pitch); + await new Promise(r => setTimeout(r, action.durationMs ?? 500)); + }); + } catch { + this._log('[bench] set_camera: failed (non-fatal)'); + } + } + break; + case 'throttle_cpu': + if (this._headlessClients.length > 0) { + const rate = action.rate ?? 1; + + this._log(`[bench] CPU throttle: target=${action.target ?? 'primary'} ${rate}x`); + + try { + await this._runOnHeadlessClients(action, client => client.setCpuThrottle(rate)); + } catch { + this._log('[bench] throttle_cpu: failed (non-fatal)'); + } + } + break; + case 'send_chat': + if (this._headlessClients.length > 0 && action.message) { + this._log(`[bench] Sending chat: target=${action.target ?? 'primary'} ${action.message}`); + + try { + await this._runOnHeadlessClients(action, async client => { + await client.sendChatMessage(action.message!); + await new Promise(r => setTimeout(r, action.durationMs ?? 500)); + }); + } catch { + this._log('[bench] send_chat: failed (non-fatal)'); + } + } + break; + case 'custom': + throw new Error(`Action not supported yet: ${action.type}`); + } + } + } + + if (phase.duration) { + const durationMs = parseDuration(phase.duration); + + this._log(`[bench] Waiting ${durationMs}ms`); + await this._collectDuring(durationMs); + } + + return { + name: phase.name, + durationMs: Date.now() - startTime, + collected: phase.collect ?? false, + }; + } + + private async _collectDuring(durationMs: number): Promise { + if (!this._collector.isCollecting) { + await this._wait(durationMs); + return; + } + + const intervalMs = 1000; + const intervals = Math.ceil(durationMs / intervalMs); + + for (let i = 0; i < intervals; i++) { + const remaining = Math.min(intervalMs, durationMs - i * intervalMs); + + await this._wait(remaining); + + if (this._perfApiAvailable) { + try { + const snapshot = await this._serverApi.snapshot(); + this._collector.addServerSnapshot(snapshot); + } catch { + this._log('[bench] PerfHarness snapshot failed — falling back to OS-only'); + this._perfApiAvailable = false; + } + } + + if (this._primaryHeadlessClient?.isConnected) { + try { + const clientSnapshot = await this._primaryHeadlessClient.collectClientMetrics(); + + if (clientSnapshot) { + this._collector.addClientSnapshot(clientSnapshot); + } + } catch { + this._log('[bench] Client metric collection failed'); + } + } + } + } + + private async _startServer(): Promise { + const baseUrl = new URL(this._options.clientUrl); + const startPort = baseUrl.port ? Number(baseUrl.port) : (baseUrl.protocol === 'https:' ? 443 : 80); + const port = await pickAvailablePort(startPort); + const finalUrl = new URL(this._options.clientUrl); + + finalUrl.port = String(port); + this._options.clientUrl = finalUrl.toString(); + this._serverApi = new ServerApiClient(this._options.clientUrl); + + this._log(`[bench] Starting server (cwd=${this._options.serverCwd}): ${this._options.serverCommand}`); + this._log(`[bench] Using server URL: ${this._options.clientUrl}`); + + const useLogFile = this._options.logFile && !this._options.verbose; + + if (useLogFile) { + const logDir = path.dirname(this._options.logFile); + if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true }); + this._logStream = fs.createWriteStream(this._options.logFile, { flags: 'w' }); + } + + this._serverProcess = spawn(this._options.serverCommand, { + cwd: this._options.serverCwd, + shell: true, + detached: true, + stdio: this._options.verbose ? 'inherit' : 'pipe', + env: { + ...process.env, + HYTOPIA_PERF_TOOLS: '1', + NODE_ENV: 'production', + PORT: String(port), + }, + }); + + if (this._logStream && this._serverProcess.stdout) { + this._serverProcess.stdout.pipe(this._logStream); + } + + if (this._logStream && this._serverProcess.stderr) { + this._serverProcess.stderr.pipe(this._logStream); + } + } + + private async _launchWsClients(count: number, staggerMs?: number): Promise { + const wsUrl = toWebSocketUrl(this._options.clientUrl); + + this._log(`[bench] Launching ${count} WebSocket client(s): ${wsUrl}`); + + const delayMs = typeof staggerMs === 'number' ? Math.max(0, Math.floor(staggerMs)) : 0; + + if (delayMs > 0) { + for (let i = 0; i < count; i++) { + const client = new WsClient({ url: wsUrl }); + await client.connect(); + this._wsClients.push(client); + + if (i < count - 1) { + // eslint-disable-next-line no-await-in-loop + await this._wait(delayMs); + } + } + + return; + } + + const batchSize = 25; + + for (let offset = 0; offset < count; offset += batchSize) { + const batchCount = Math.min(batchSize, count - offset); + const batchClients: WsClient[] = []; + + for (let i = 0; i < batchCount; i++) { + batchClients.push(new WsClient({ url: wsUrl })); + } + + await Promise.all(batchClients.map(async client => { + await client.connect(); + this._wsClients.push(client); + })); + } + } + + private async _disconnectWsClients(count?: number): Promise { + if (count === undefined) { + for (const client of this._wsClients) { + await client.close(); + } + + this._wsClients = []; + return; + } + + const target = Math.max(0, Math.floor(count)); + + for (let i = 0; i < target && this._wsClients.length > 0; i++) { + const client = this._wsClients.pop()!; + await client.close(); + } + } + + private async _probePerfApi(): Promise { + try { + await this._serverApi.snapshot(); + return true; + } catch { + return false; + } + } + + private get _primaryHeadlessClient(): HeadlessClient | null { + return this._headlessClients[0] ?? null; + } + + private _getTargetHeadlessClients(target: ScenarioClientTarget | undefined): HeadlessClient[] { + switch (target) { + case 'all': + return [...this._headlessClients]; + case 'extras': + return this._headlessClients.slice(1); + case 'primary': + default: + return this._headlessClients.slice(0, 1); + } + } + + private async _runOnHeadlessClients( + action: Pick, + run: (client: HeadlessClient, index: number) => Promise, + ): Promise { + const clients = this._getTargetHeadlessClients(action.target); + + if (clients.length === 0) { + return; + } + + const delayMs = typeof action.staggerMs === 'number' ? Math.max(0, Math.floor(action.staggerMs)) : 0; + + await Promise.all(clients.map(async (client, index) => { + if (delayMs > 0 && index > 0) { + await this._wait(delayMs * index); + } + + await run(client, index); + })); + } + + private async _closeHeadlessClients(): Promise { + for (const client of this._headlessClients) { + await client.close(); + } + + this._headlessClients = []; + } + + private async _cleanup(): Promise { + await this._closeHeadlessClients(); + + for (const client of this._wsClients) { + await client.close(); + } + + this._wsClients = []; + + if (this._serverProcess) { + const pid = this._serverProcess.pid; + + try { + if (pid) { + process.kill(-pid, 'SIGTERM'); + } else { + this._serverProcess.kill('SIGTERM'); + } + } catch { + this._serverProcess.kill('SIGTERM'); + } + + this._serverProcess = null; + } + + if (this._logStream) { + this._logStream.end(); + this._logStream = null; + } + } + + private _buildCapabilities(metrics: CollectedMetrics): BenchmarkCapabilities { + const serverMetricSources = Array.from(new Set( + metrics.serverSnapshots + .map(snapshot => snapshot.source) + .filter((source): source is 'perf_harness' | 'legacy_perf_api' => source === 'perf_harness' || source === 'legacy_perf_api'), + )); + const clientMetricSources = Array.from(new Set( + metrics.clientSnapshots + .map(snapshot => snapshot.source) + .filter((source): source is 'perf_bridge' | 'webgl_fallback' => source === 'perf_bridge' || source === 'webgl_fallback'), + )); + + return { + serverMetrics: metrics.serverSnapshots.length > 0, + serverMetricSources, + clientMetrics: metrics.clientSnapshots.length > 0, + clientMetricSources, + }; + } + + private _buildValidation( + metrics: CollectedMetrics, + capabilities: BenchmarkCapabilities, + scenario: Scenario, + ): BenchmarkValidation { + const warnings: string[] = []; + const issues: string[] = []; + const collectedPhaseCount = scenario.phases.filter(phase => phase.collect).length; + + if (collectedPhaseCount > 0 && !capabilities.serverMetrics && !capabilities.clientMetrics) { + issues.push('No benchmark snapshots were collected. The target client/server likely failed to expose metrics or failed to load.'); + } + + if (this._options.withClient && collectedPhaseCount > 0 && !capabilities.clientMetrics) { + issues.push('Client metrics were requested but no client snapshots were collected.'); + } + + if (!this._options.noPerfApi && collectedPhaseCount > 0 && !capabilities.serverMetrics) { + warnings.push('No server snapshots were collected. This run only supports client-side comparison.'); + } + + if (capabilities.serverMetricSources.includes('legacy_perf_api')) { + warnings.push('Server metrics were normalized from a legacy /__perf endpoint. Network, budget, and p99 comparisons may be limited.'); + } + + if (capabilities.clientMetricSources.includes('webgl_fallback') && !capabilities.clientMetricSources.includes('perf_bridge')) { + warnings.push('Client metrics were collected via WebGL fallback instrumentation because PerfBridge was unavailable.'); + } + + return { + valid: issues.length === 0, + warnings, + issues, + }; + } + + private _buildBaseline(metrics: CollectedMetrics): BaselineResult { + const snapshots = metrics.serverSnapshots; + const clientSnapshots = metrics.clientSnapshots; + + if (snapshots.length === 0 && clientSnapshots.length === 0) { + return { + avgTickMs: 0, + maxTickMs: 0, + p95TickMs: 0, + p99TickMs: 0, + ticksOverBudgetPct: 0, + avgMemoryMb: 0, + operations: {}, + }; + } + + if (snapshots.length === 0) { + // No server snapshots (e.g. external server without PerfHarness) — build client-only baseline + const client = clientSnapshots.length > 0 + ? { + avgFps: average(clientSnapshots, s => s.fps), + minFps: Math.min(...clientSnapshots.map(s => s.fps)), + avgFrameTimeMs: average(clientSnapshots, s => s.frameTimeMs), + avgDrawCalls: average(clientSnapshots, s => s.drawCalls), + maxDrawCalls: max(clientSnapshots, s => s.drawCalls), + avgTriangles: average(clientSnapshots, s => s.triangles), + maxTriangles: max(clientSnapshots, s => s.triangles), + avgGeometries: average(clientSnapshots, s => s.geometries ?? 0), + avgEntities: average(clientSnapshots, s => s.entities?.count ?? 0), + avgVisibleChunks: average(clientSnapshots, s => s.chunks?.visible ?? 0), + avgUsedMemoryMb: average(clientSnapshots, s => s.usedMemoryMb ?? 0), + } + : undefined; + + return { + avgTickMs: 0, + maxTickMs: 0, + p95TickMs: 0, + p99TickMs: 0, + ticksOverBudgetPct: 0, + avgMemoryMb: 0, + avgFps: client?.avgFps, + client, + operations: {}, + }; + } + + const avgTickMs = snapshots.reduce((s, v) => s + v.avgTickMs, 0) / snapshots.length; + const maxTickMs = Math.max(...snapshots.map(s => s.maxTickMs)); + const p95TickMs = snapshots.reduce((s, v) => s + v.p95TickMs, 0) / snapshots.length; + const p99TickMs = snapshots.reduce((s, v) => s + v.p99TickMs, 0) / snapshots.length; + const totalTicks = snapshots.reduce((s, v) => s + v.totalTicks, 0); + const overBudget = snapshots.reduce((s, v) => s + v.ticksOverBudget, 0); + const avgMemoryMb = snapshots.reduce((s, v) => s + v.memory.heapUsedMb, 0) / snapshots.length; + + const operations: Record = {}; + const opNames = new Set(snapshots.flatMap(s => Object.keys(s.operations))); + + for (const name of opNames) { + const opSnapshots = snapshots.filter(s => s.operations[name]); + + if (opSnapshots.length > 0) { + operations[name] = { + avgMs: opSnapshots.reduce((s, v) => s + v.operations[name].avgMs, 0) / opSnapshots.length, + p95Ms: opSnapshots.reduce((s, v) => s + v.operations[name].p95Ms, 0) / opSnapshots.length, + }; + } + } + + const client = clientSnapshots.length > 0 + ? { + avgFps: average(clientSnapshots, s => s.fps), + minFps: Math.min(...clientSnapshots.map(s => s.fps)), + avgFrameTimeMs: average(clientSnapshots, s => s.frameTimeMs), + avgDrawCalls: average(clientSnapshots, s => s.drawCalls), + maxDrawCalls: max(clientSnapshots, s => s.drawCalls), + avgTriangles: average(clientSnapshots, s => s.triangles), + maxTriangles: max(clientSnapshots, s => s.triangles), + avgGeometries: average(clientSnapshots, s => s.geometries ?? 0), + avgEntities: average(clientSnapshots, s => s.entities?.count ?? 0), + avgVisibleChunks: average(clientSnapshots, s => s.chunks?.visible ?? 0), + avgUsedMemoryMb: average(clientSnapshots, s => s.usedMemoryMb ?? 0), + } + : undefined; + + const avgFps = clientSnapshots.length > 0 + ? clientSnapshots.reduce((s, v) => s + v.fps, 0) / clientSnapshots.length + : undefined; + + const networkSnapshots = snapshots.flatMap(s => (s.network ? [ s.network ] : [])); + const network = networkSnapshots.length > 0 + ? { + totalBytesSent: Math.max(...networkSnapshots.map(s => s.bytesSentTotal)), + totalBytesReceived: Math.max(...networkSnapshots.map(s => s.bytesReceivedTotal)), + maxConnectedPlayers: Math.max(...networkSnapshots.map(s => s.connectedPlayers)), + avgBytesSentPerSecond: average(networkSnapshots, s => s.bytesSentPerSecond), + maxBytesSentPerSecond: max(networkSnapshots, s => s.bytesSentPerSecond), + avgBytesReceivedPerSecond: average(networkSnapshots, s => s.bytesReceivedPerSecond), + maxBytesReceivedPerSecond: max(networkSnapshots, s => s.bytesReceivedPerSecond), + avgPacketsSentPerSecond: average(networkSnapshots, s => s.packetsSentPerSecond), + maxPacketsSentPerSecond: max(networkSnapshots, s => s.packetsSentPerSecond), + avgPacketsReceivedPerSecond: average(networkSnapshots, s => s.packetsReceivedPerSecond), + maxPacketsReceivedPerSecond: max(networkSnapshots, s => s.packetsReceivedPerSecond), + avgSerializationMs: average(networkSnapshots, s => s.avgSerializationMs), + compressionCountTotal: Math.max(...networkSnapshots.map(s => s.compressionCount)), + } + : undefined; + + return { + avgTickMs, + maxTickMs, + p95TickMs, + p99TickMs, + ticksOverBudgetPct: totalTicks > 0 ? (overBudget / totalTicks) * 100 : 0, + avgMemoryMb, + avgFps, + client, + operations, + network, + }; + } + + private _wait(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } +} + +function average(items: T[], get: (item: T) => number): number { + if (items.length === 0) return 0; + + return items.reduce((s, v) => s + get(v), 0) / items.length; +} + +function max(items: T[], get: (item: T) => number): number { + if (items.length === 0) return 0; + + return Math.max(...items.map(get)); +} + +function resolveDefaultServerCwd(startDir: string): string { + let dir = startDir; + + for (let i = 0; i < 8; i++) { + const serverPkg = path.join(dir, 'server', 'package.json'); + + if (fs.existsSync(serverPkg)) { + return path.join(dir, 'server'); + } + + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + + return path.join(startDir, 'server'); +} + +function toWebSocketUrl(baseUrl: string): string { + const url = new URL(baseUrl); + url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'; + url.pathname = '/'; + url.search = ''; + url.hash = ''; + return url.toString(); +} + +async function pickAvailablePort(startPort: number): Promise { + const minPort = Math.max(1, Math.floor(startPort)); + + for (let port = minPort; port < minPort + 50; port++) { + // eslint-disable-next-line no-await-in-loop + const available = await canListen(port); + if (available) return port; + } + + throw new Error(`No available port found (starting from ${startPort})`); +} + +async function canListen(port: number): Promise { + return await new Promise(resolve => { + const server = net.createServer(); + + server.unref(); + + server.once('error', () => { + resolve(false); + }); + + server.listen(port, '127.0.0.1', () => { + server.close(() => resolve(true)); + }); + }); +} diff --git a/packages/perf-tools/src/runners/BenchmarkSeriesAggregator.ts b/packages/perf-tools/src/runners/BenchmarkSeriesAggregator.ts new file mode 100644 index 00000000..c445cdb7 --- /dev/null +++ b/packages/perf-tools/src/runners/BenchmarkSeriesAggregator.ts @@ -0,0 +1,242 @@ +import * as fs from 'node:fs'; +import type { BaselineResult, ComparisonEntry, ComparisonResult, LoadedBenchmarkInput } from './BaselineComparer.js'; + +export interface AggregatedBenchmarkInput extends LoadedBenchmarkInput { + scenario?: string; + durationMs?: number; + aggregation: { + method: 'median'; + runCount: number; + sourceFiles: string[]; + }; +} + +export type SeriesVerdict = 'improves' | 'neutral' | 'regresses' | 'inconclusive'; + +export interface SeriesComparisonSummary { + comparison: ComparisonResult; + verdict: SeriesVerdict; + keyEntries: ComparisonEntry[]; +} + +const CORE_METRICS = new Set([ + 'avgTickMs', + 'p95TickMs', + 'avgFps', + 'client.avgFrameTimeMs', +]); + +type ReportWithMetadata = LoadedBenchmarkInput & { + scenario?: string; + durationMs?: number; +}; + +export default class BenchmarkSeriesAggregator { + public aggregateFiles(filePaths: string[]): AggregatedBenchmarkInput { + if (filePaths.length === 0) { + throw new Error('aggregateFiles(): expected at least one benchmark report.'); + } + + const reports = filePaths.map(filePath => this._loadReport(filePath)); + + for (const [index, report] of reports.entries()) { + if (report.validation?.valid === false) { + throw new Error(`aggregateFiles(): ${filePaths[index]} is invalid and cannot be aggregated.`); + } + } + + const scenario = this._firstDefined(reports.map(report => report.scenario)); + const durationMsValues = reports + .map(report => report.durationMs) + .filter((value): value is number => value !== undefined); + + return { + scenario, + durationMs: durationMsValues.length > 0 ? this._median(durationMsValues) : undefined, + baseline: this._aggregateBaseline(reports.map(report => report.baseline)), + metrics: this._aggregateMetrics(reports), + validation: { + valid: true, + warnings: [ + `Aggregated ${filePaths.length} benchmark reports using the median for each numeric metric.`, + ], + issues: [], + }, + capabilities: this._aggregateCapabilities(reports), + aggregation: { + method: 'median', + runCount: filePaths.length, + sourceFiles: filePaths, + }, + }; + } + + public classifyComparison(comparison: ComparisonResult): SeriesComparisonSummary { + const keyEntries = comparison.entries.filter(entry => CORE_METRICS.has(entry.metric)); + const improvements = keyEntries.filter(entry => entry.changePct < -comparison.warningThresholdPct); + const regressions = keyEntries.filter(entry => entry.changePct > comparison.warningThresholdPct); + + let verdict: SeriesVerdict = 'inconclusive'; + + if (keyEntries.length === 0) { + verdict = 'inconclusive'; + } else if (regressions.length === 0 && improvements.length === 0) { + verdict = 'neutral'; + } else if (regressions.length > 0 && improvements.length === 0) { + verdict = 'regresses'; + } else if (improvements.length > 0 && regressions.length === 0) { + verdict = 'improves'; + } else { + verdict = 'inconclusive'; + } + + return { + comparison, + verdict, + keyEntries, + }; + } + + private _loadReport(filePath: string): ReportWithMetadata { + const content = fs.readFileSync(filePath, 'utf-8'); + const data = JSON.parse(content); + const loaded = ('baseline' in data ? data : { baseline: data }) as ReportWithMetadata; + + return { + scenario: loaded.scenario, + durationMs: loaded.durationMs, + baseline: loaded.baseline, + metrics: loaded.metrics, + validation: loaded.validation, + capabilities: loaded.capabilities, + }; + } + + private _aggregateBaseline(results: BaselineResult[]): BaselineResult { + const baseline: BaselineResult = { + avgTickMs: this._median(results.map(result => result.avgTickMs)), + maxTickMs: this._median(results.map(result => result.maxTickMs)), + p95TickMs: this._median(results.map(result => result.p95TickMs)), + p99TickMs: this._median(results.map(result => result.p99TickMs)), + ticksOverBudgetPct: this._median(results.map(result => result.ticksOverBudgetPct)), + avgMemoryMb: this._median(results.map(result => result.avgMemoryMb)), + operations: this._aggregateOperations(results), + }; + + if (results.every(result => result.avgFps !== undefined)) { + baseline.avgFps = this._median(results.map(result => result.avgFps as number)); + } + + if (results.every(result => result.client !== undefined)) { + baseline.client = { + avgFps: this._median(results.map(result => result.client!.avgFps)), + minFps: this._median(results.map(result => result.client!.minFps)), + avgFrameTimeMs: this._median(results.map(result => result.client!.avgFrameTimeMs)), + avgDrawCalls: this._median(results.map(result => result.client!.avgDrawCalls)), + maxDrawCalls: this._median(results.map(result => result.client!.maxDrawCalls)), + avgTriangles: this._median(results.map(result => result.client!.avgTriangles)), + maxTriangles: this._median(results.map(result => result.client!.maxTriangles)), + avgGeometries: this._median(results.map(result => result.client!.avgGeometries)), + avgEntities: this._median(results.map(result => result.client!.avgEntities)), + avgVisibleChunks: this._median(results.map(result => result.client!.avgVisibleChunks)), + avgUsedMemoryMb: this._median(results.map(result => result.client!.avgUsedMemoryMb)), + }; + } + + if (results.every(result => result.network !== undefined)) { + baseline.network = { + totalBytesSent: this._median(results.map(result => result.network!.totalBytesSent)), + totalBytesReceived: this._median(results.map(result => result.network!.totalBytesReceived)), + maxConnectedPlayers: this._median(results.map(result => result.network!.maxConnectedPlayers)), + avgBytesSentPerSecond: this._median(results.map(result => result.network!.avgBytesSentPerSecond)), + maxBytesSentPerSecond: this._median(results.map(result => result.network!.maxBytesSentPerSecond)), + avgBytesReceivedPerSecond: this._median(results.map(result => result.network!.avgBytesReceivedPerSecond)), + maxBytesReceivedPerSecond: this._median(results.map(result => result.network!.maxBytesReceivedPerSecond)), + avgPacketsSentPerSecond: this._median(results.map(result => result.network!.avgPacketsSentPerSecond)), + maxPacketsSentPerSecond: this._median(results.map(result => result.network!.maxPacketsSentPerSecond)), + avgPacketsReceivedPerSecond: this._median(results.map(result => result.network!.avgPacketsReceivedPerSecond)), + maxPacketsReceivedPerSecond: this._median(results.map(result => result.network!.maxPacketsReceivedPerSecond)), + avgSerializationMs: this._median(results.map(result => result.network!.avgSerializationMs)), + compressionCountTotal: this._median(results.map(result => result.network!.compressionCountTotal)), + }; + } + + return baseline; + } + + private _aggregateOperations(results: BaselineResult[]): BaselineResult['operations'] { + if (results.length === 0) { + return {}; + } + + const sharedOperationNames = results + .map(result => new Set(Object.keys(result.operations ?? {}))) + .reduce((shared, current) => { + return new Set([...shared].filter(name => current.has(name))); + }); + + const operations: BaselineResult['operations'] = {}; + + for (const operationName of sharedOperationNames) { + operations[operationName] = { + avgMs: this._median(results.map(result => result.operations[operationName].avgMs)), + p95Ms: this._median(results.map(result => result.operations[operationName].p95Ms)), + }; + } + + return operations; + } + + private _aggregateMetrics(reports: ReportWithMetadata[]): AggregatedBenchmarkInput['metrics'] { + const tickReportCounts = reports + .map(report => report.metrics?.tickReportCount) + .filter((value): value is number => value !== undefined); + const spikeCounts = reports + .map(report => report.metrics?.spikeCount) + .filter((value): value is number => value !== undefined); + const serverSnapshotCounts = reports + .map(report => report.metrics?.serverSnapshotCount) + .filter((value): value is number => value !== undefined); + const clientSnapshotCounts = reports + .map(report => report.metrics?.clientSnapshotCount) + .filter((value): value is number => value !== undefined); + + return { + tickReportCount: tickReportCounts.length > 0 ? this._median(tickReportCounts) : undefined, + spikeCount: spikeCounts.length > 0 ? this._median(spikeCounts) : undefined, + serverSnapshotCount: serverSnapshotCounts.length > 0 ? this._median(serverSnapshotCounts) : undefined, + clientSnapshotCount: clientSnapshotCounts.length > 0 ? this._median(clientSnapshotCounts) : undefined, + }; + } + + private _aggregateCapabilities(reports: ReportWithMetadata[]): AggregatedBenchmarkInput['capabilities'] { + const serverMetricSources = reports + .map(report => report.capabilities?.serverMetricSources ?? []) + .reduce((shared, sources) => shared.filter(source => sources.includes(source))); + const clientMetricSources = reports + .map(report => report.capabilities?.clientMetricSources ?? []) + .reduce((shared, sources) => shared.filter(source => sources.includes(source))); + + return { + serverMetrics: reports.every(report => report.capabilities?.serverMetrics !== false), + serverMetricSources, + clientMetrics: reports.every(report => report.capabilities?.clientMetrics !== false), + clientMetricSources, + }; + } + + private _median(values: number[]): number { + const sorted = [...values].sort((a, b) => a - b); + const middleIndex = Math.floor(sorted.length / 2); + + if (sorted.length % 2 === 1) { + return sorted[middleIndex]; + } + + return (sorted[middleIndex - 1] + sorted[middleIndex]) / 2; + } + + private _firstDefined(values: Array): T | undefined { + return values.find((value): value is T => value !== undefined); + } +} diff --git a/packages/perf-tools/src/runners/HeadlessClient.ts b/packages/perf-tools/src/runners/HeadlessClient.ts new file mode 100644 index 00000000..449cc15c --- /dev/null +++ b/packages/perf-tools/src/runners/HeadlessClient.ts @@ -0,0 +1,683 @@ +import type { ClientSnapshot } from './MetricCollector.js'; + +declare global { + interface Performance { + memory?: { + jsHeapSizeLimit: number; + totalJSHeapSize: number; + usedJSHeapSize: number; + }; + } +} + +export interface HeadlessClientOptions { + url: string; + headless?: boolean; + width?: number; + height?: number; + deviceScaleFactor?: number; + collectPerformance?: boolean; +} + +export default class HeadlessClient { + private _browser: unknown = null; + private _page: unknown = null; + private _cdp: any = null; + private _options: HeadlessClientOptions; + private _performanceEntries: ClientSnapshot[] = []; + private _connected: boolean = false; + + constructor(options: HeadlessClientOptions) { + this._options = { + headless: true, + width: 1280, + height: 720, + deviceScaleFactor: 1, + collectPerformance: true, + ...options, + }; + } + + public async launch(): Promise { + let puppeteer: any; + + try { + puppeteer = await import('puppeteer'); + } catch { + throw new Error( + 'puppeteer is required for headless client. Install it: npm install puppeteer', + ); + } + + this._browser = await puppeteer.default.launch({ + headless: this._options.headless ? 'new' : false, + ignoreHTTPSErrors: true, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--ignore-certificate-errors', + '--allow-insecure-localhost', + '--disable-web-security', + '--disable-features=PrivateNetworkAccessSendPreflights', + '--enable-precise-memory-info', + '--disable-notifications', + '--autoplay-policy=no-user-gesture-required', + '--enable-unsafe-swiftshader', + `--window-size=${this._options.width},${this._options.height}`, + ], + }); + + const browser = this._browser as any; + + this._page = await browser.newPage(); + + const page = this._page as any; + + await page.setViewport({ + width: this._options.width!, + height: this._options.height!, + deviceScaleFactor: this._options.deviceScaleFactor, + }); + + // Forward browser console to Node stdout for debugging + page.on('console', (msg: any) => { + const type = msg.type(); + const text = msg.text(); + + if (type === 'error' || type === 'warning') { + console.log(`[client:${type}] ${text}`); + } + }); + + page.on('pageerror', (err: any) => { + console.log(`[client:error] ${err.message ?? err}`); + }); + + this._cdp = await page.createCDPSession(); + + // Bypass certificate errors via CDP (--ignore-certificate-errors doesn't work in headless: 'new') + await this._cdp.send('Security.setIgnoreCertificateErrors', { ignore: true }); + + if (this._options.collectPerformance) { + await this._cdp.send('Performance.enable'); + } + + // Patch fetch() to strip unsupported targetAddressSpace option + // (Chrome's Private Network Access API is not available in all Chrome versions) + await page.evaluateOnNewDocument(() => { + const originalFetch = window.fetch.bind(window); + + (window as any).fetch = function(input: any, init?: any) { + if (init && 'targetAddressSpace' in init) { + const { targetAddressSpace: _, ...rest } = init; + + return originalFetch(input, rest); + } + + return originalFetch(input, init); + }; + + }); + + await page.evaluateOnNewDocument(() => { + if ((window as any).__HYTOPIA_FALLBACK_PERF__) { + return; + } + + const state = { + fps: 0, + frameTimeMs: 0, + lastFrameTimestamp: 0, + fpsWindowStart: 0, + fpsWindowFrames: 0, + drawCallsThisFrame: 0, + trianglesThisFrame: 0, + drawCallsLastFrame: 0, + trianglesLastFrame: 0, + usedMemoryMb: 0, + totalMemoryMb: 0, + hasSeenDrawCall: false, + }; + + const getTrianglesPerCall = (mode: number, count: number): number => { + switch (mode) { + case WebGLRenderingContext.TRIANGLES: + return Math.floor(count / 3); + case WebGLRenderingContext.TRIANGLE_STRIP: + case WebGLRenderingContext.TRIANGLE_FAN: + return Math.max(0, count - 2); + default: + return 0; + } + }; + + const wrapDraw = (proto: any, methodName: string, getCount: (...args: any[]) => number, getInstances?: (...args: any[]) => number) => { + if (!proto?.[methodName] || proto[methodName].__hytopiaPerfWrapped) { + return; + } + + const original = proto[methodName]; + + const wrapped = function(this: unknown, ...args: any[]) { + const count = getCount(...args); + const instances = getInstances ? Math.max(1, getInstances(...args)) : 1; + + state.drawCallsThisFrame += 1; + state.trianglesThisFrame += getTrianglesPerCall(args[0], count) * instances; + state.hasSeenDrawCall = true; + + return original.apply(this, args); + }; + + wrapped.__hytopiaPerfWrapped = true; + proto[methodName] = wrapped; + }; + + const webgl1Prototype = typeof WebGLRenderingContext !== 'undefined' ? WebGLRenderingContext.prototype : undefined; + const webgl2Prototype = typeof WebGL2RenderingContext !== 'undefined' ? WebGL2RenderingContext.prototype : undefined; + + wrapDraw(webgl1Prototype, 'drawArrays', (_mode: number, _first: number, count: number) => count); + wrapDraw(webgl1Prototype, 'drawElements', (_mode: number, count: number) => count); + wrapDraw(webgl2Prototype, 'drawArrays', (_mode: number, _first: number, count: number) => count); + wrapDraw(webgl2Prototype, 'drawElements', (_mode: number, count: number) => count); + wrapDraw(webgl2Prototype, 'drawArraysInstanced', (_mode: number, _first: number, count: number) => count, (_mode: number, _first: number, _count: number, instanceCount: number) => instanceCount); + wrapDraw(webgl2Prototype, 'drawElementsInstanced', (_mode: number, count: number) => count, (_mode: number, _count: number, _type: number, _offset: number, instanceCount: number) => instanceCount); + + const tick = (timestamp: number) => { + if (state.lastFrameTimestamp > 0) { + state.frameTimeMs = timestamp - state.lastFrameTimestamp; + } + + if (state.fpsWindowStart === 0) { + state.fpsWindowStart = timestamp; + } + + state.fpsWindowFrames += 1; + + if (timestamp - state.fpsWindowStart >= 1000) { + state.fps = (state.fpsWindowFrames * 1000) / (timestamp - state.fpsWindowStart); + state.fpsWindowFrames = 0; + state.fpsWindowStart = timestamp; + } + + state.lastFrameTimestamp = timestamp; + state.drawCallsLastFrame = state.drawCallsThisFrame; + state.trianglesLastFrame = state.trianglesThisFrame; + state.drawCallsThisFrame = 0; + state.trianglesThisFrame = 0; + + const memory = performance.memory; + + if (memory) { + state.usedMemoryMb = memory.usedJSHeapSize / (1024 * 1024); + state.totalMemoryMb = memory.totalJSHeapSize / (1024 * 1024); + } + + requestAnimationFrame(tick); + }; + + requestAnimationFrame(tick); + + (window as any).__HYTOPIA_FALLBACK_PERF__ = { + isReady() { + return state.hasSeenDrawCall; + }, + snapshot() { + return { + source: 'webgl_fallback', + fps: state.fps, + frameTimeMs: state.frameTimeMs, + drawCalls: state.drawCallsLastFrame, + triangles: state.trianglesLastFrame, + textureMemoryMb: 0, + usedMemoryMb: state.usedMemoryMb, + totalMemoryMb: state.totalMemoryMb, + }; + }, + }; + }); + } + + /** + * Visit the game server URL once to warm up the self-signed HTTPS cert + * in Chrome's cert cache. Without this, in-page fetch() to the server fails. + */ + public async warmCert(serverUrl: string): Promise { + const page = this._page as any; + + if (!page) return; + + try { + await page.goto(serverUrl, { waitUntil: 'load', timeout: 15000 }); + } catch { + // Expected — self-signed cert page may fail but Chrome records the exception + } + } + + public async navigate(url?: string): Promise { + const page = this._page as any; + + if (!page) throw new Error('Client not launched. Call launch() first.'); + + const target = new URL(url ?? this._options.url); + + target.searchParams.set('perf', '1'); + + await page.goto(target.toString(), { waitUntil: 'load', timeout: 60000 }); + this._connected = true; + } + + public async waitForPerfReady(timeoutMs: number = 30000): Promise { + const page = this._page as any; + + if (!page) return false; + + const start = Date.now(); + + while (Date.now() - start < timeoutMs) { + try { + const ready = await page.evaluate(() => { + const perf = (window as any).__HYTOPIA_PERF__; + const fallbackPerf = (window as any).__HYTOPIA_FALLBACK_PERF__; + + if (perf && typeof perf.snapshot === 'function') { + return true; + } + + if (fallbackPerf && typeof fallbackPerf.isReady === 'function') { + return fallbackPerf.isReady(); + } + + return false; + }); + + if (ready) return true; + } catch { + // page not ready yet + } + + await new Promise(resolve => setTimeout(resolve, 500)); + } + + return false; + } + + public async dismissModals(): Promise { + const page = this._page as any; + + if (!page) return; + + try { + await page.evaluate(() => { + const buttons = document.querySelectorAll('.hytopia-modal-button-ok'); + + buttons.forEach((btn: any) => btn.click()); + }); + } catch { + // no modals present + } + } + + public async collectClientMetrics(): Promise { + const page = this._page as any; + + if (!page || !this._connected) return null; + + try { + // Dismiss any modals that might have appeared + await this.dismissModals(); + + const metrics = await page.evaluate(() => { + const perf = (window as any).__HYTOPIA_PERF__; + const fallbackPerf = (window as any).__HYTOPIA_FALLBACK_PERF__; + + return { + perfSnapshot: perf && typeof perf.snapshot === 'function' ? perf.snapshot() : null, + fallbackSnapshot: fallbackPerf && typeof fallbackPerf.snapshot === 'function' && fallbackPerf.isReady() + ? fallbackPerf.snapshot() + : null, + }; + }); + + const normalized = normalizeClientMetrics(metrics?.perfSnapshot, metrics?.fallbackSnapshot); + + if (!normalized) return null; + + const snapshot: ClientSnapshot = { + timestamp: Date.now(), + ...normalized, + }; + + this._performanceEntries.push(snapshot); + + return snapshot; + } catch { + return null; + } + } + + /** + * Send movement input directly via the game's network manager. + * Bypasses pointer lock / keyboard events — works in headless mode. + * Sends packets continuously at 30Hz to match InputManager behavior. + * key: 'w' | 'a' | 's' | 'd' | 'sp' (space/jump) + */ + public async sendMovement(key: string, durationMs: number): Promise { + const page = this._page as any; + + if (!page) return; + + try { + // Send start packet and then continue sending at 30Hz with camera orientation + await page.evaluate((k: string, durMs: number) => { + const game = (window as any).__HYTOPIA_GAME__; + + if (!game?.networkManager) return; + + // Send initial key-down + game.networkManager.sendInputPacket({ [k]: true }); + + // Send continuous camera+movement at 30Hz so server knows direction + const interval = setInterval(() => { + const yaw = game.camera?._gameCameraYaw ?? 0; + const pitch = game.camera?._gameCameraPitch ?? 0; + + game.networkManager.sendInputPacket({ cp: pitch, cy: yaw }); + }, 33); + + // Store cleanup for later + (window as any).__HYTOPIA_MOVEMENT_INTERVAL__ = interval; + (window as any).__HYTOPIA_MOVEMENT_KEY__ = k; + + // Auto-stop after duration + setTimeout(() => { + clearInterval(interval); + game.networkManager.sendInputPacket({ [k]: false }); + (window as any).__HYTOPIA_MOVEMENT_INTERVAL__ = null; + }, durMs); + }, key, durationMs); + + // Wait for the movement to complete + await new Promise(r => setTimeout(r, durationMs + 100)); + } catch { + // best-effort + } + } + + /** + * Simulate mouse look by injecting camera yaw/pitch directly via the game's input system. + */ + public async lookAt(yawRadians: number, pitchRadians: number): Promise { + const page = this._page as any; + + if (!page) return; + + try { + await page.evaluate((yaw: number, pitch: number) => { + const game = (window as any).__HYTOPIA_GAME__; + + if (!game) return; + + // Set camera yaw/pitch directly on the Camera object (client-side rendering) + if (game.camera) { + game.camera._gameCameraYaw = yaw; + game.camera._gameCameraPitch = pitch; + } + + // Also send to server so movement direction matches camera direction + if (game.networkManager) { + game.networkManager.sendInputPacket({ cp: pitch, cy: yaw }); + } + }, yawRadians, pitchRadians); + } catch { + // best-effort + } + } + + /** + * Teleport the camera/player to a specific world position. + * Works by directly setting the camera target position. + */ + public async setCameraPosition(x: number, y: number, z: number): Promise { + const page = this._page as any; + + if (!page) return; + + try { + await page.evaluate((px: number, py: number, pz: number) => { + const game = (window as any).__HYTOPIA_GAME__; + + if (game?.camera) { + game.camera.setTarget(px, py, pz); + } + }, x, y, z); + } catch { + // best-effort + } + } + + /** + * Simulate a player walking forward for a duration, with optional camera rotation. + * This makes the headless client behave like a real player walking around. + */ + public async simulateWalkSequence(steps: Array<{ durationMs: number; key?: string; yaw?: number; pitch?: number }>): Promise { + const page = this._page as any; + + if (!page) return; + + // Click to get pointer lock + try { + await page.mouse.click(640, 360); + await new Promise(r => setTimeout(r, 300)); + } catch { + // continue + } + + for (const step of steps) { + try { + // Set camera direction if specified + if (step.yaw !== undefined || step.pitch !== undefined) { + await this.lookAt(step.yaw ?? 0, step.pitch ?? -0.3); + } + + // Press movement key + const key = step.key ?? 'w'; + + await page.keyboard.down(key); + await new Promise(r => setTimeout(r, step.durationMs)); + await page.keyboard.up(key); + } catch { + // continue sequence + } + } + } + + /** + * Throttle CPU execution speed via CDP. rate=1 means no throttle, rate=4 means 4x slower (simulates mobile). + */ + public async setCpuThrottle(rate: number): Promise { + if (!this._cdp) return; + + try { + await this._cdp.send('Emulation.setCPUThrottlingRate', { rate: Math.max(1, rate) }); + } catch { + // best-effort + } + } + + /** + * Send a chat message via the game's network manager. + * Used to trigger server-side debug commands like /fillzoo. + */ + public async sendChatMessage(message: string): Promise { + const page = this._page as any; + + if (!page || !this._connected) return; + + try { + await page.evaluate((msg: string) => { + const game = (window as any).__HYTOPIA_GAME__; + + if (game?.networkManager?.sendChatMessagePacket) { + game.networkManager.sendChatMessagePacket(msg); + } + }, message); + } catch { + // best-effort + } + } + + public async captureTrace(durationMs: number): Promise { + const page = this._page as any; + + if (!page) return null; + + try { + await page.tracing.start({ categories: ['devtools.timeline', 'v8.execute', 'blink.user_timing'] }); + + await new Promise(resolve => setTimeout(resolve, durationMs)); + + const buffer = await page.tracing.stop(); + + return JSON.parse(buffer.toString()); + } catch { + return null; + } + } + + public async close(): Promise { + try { + const browser = this._browser as any; + + if (browser) { + await browser.close(); + } + } catch { + // best-effort cleanup + } finally { + this._browser = null; + this._page = null; + this._connected = false; + } + } + + public get performanceEntries(): ClientSnapshot[] { + return this._performanceEntries; + } + + public get isConnected(): boolean { + return this._connected; + } +} + +type RawPerfSnapshot = Record | null | undefined; + +function normalizeClientMetrics(perfSnapshot: RawPerfSnapshot, fallbackSnapshot: RawPerfSnapshot): Omit | null { + if (!perfSnapshot && !fallbackSnapshot) { + return null; + } + + const perf = asObject(perfSnapshot); + const fallback = asObject(fallbackSnapshot); + const perfFrame = asObject(perf?.frame); + const perfMemory = asObject(perf?.memory); + const perfEntities = asObject(perf?.entities); + const perfWorld = asObject(perf?.world); + const perfChunks = asObject(perf?.chunks); + const perfGltf = asObject(perf?.gltf); + + const entityCount = asNumber(perfEntities?.count) ?? asNumber(perfWorld?.entityCount); + const chunkCount = asNumber(perfChunks?.count) ?? asNumber(perfWorld?.chunkCount); + const visibleChunkCount = asNumber(perfChunks?.visible); + + return { + source: perf ? 'perf_bridge' : 'webgl_fallback', + fps: coalesceNumber( + asNumber(perf?.fps), + asNumber(perfFrame?.currentFps), + asNumber(fallback?.fps), + 0, + ), + frameTimeMs: coalesceNumber( + asNumber(perf?.frameTimeMs), + asNumber(perfFrame?.currentFrameMs), + asNumber(fallback?.frameTimeMs), + 0, + ), + drawCalls: coalesceNumber( + asNumber(perf?.drawCalls), + asNumber(fallback?.drawCalls), + 0, + ), + triangles: coalesceNumber( + asNumber(perf?.triangles), + asNumber(fallback?.triangles), + 0, + ), + textureMemoryMb: coalesceNumber(asNumber(perf?.textureMemoryMb), 0), + geometries: asNumber(perf?.geometries) ?? undefined, + textures: asNumber(perf?.textures) ?? undefined, + programs: asNumber(perf?.programs) ?? undefined, + usedMemoryMb: coalesceNumber( + asNumber(perf?.usedMemoryMb), + asNumber(perfMemory?.usedHeapMb), + asNumber(fallback?.usedMemoryMb), + 0, + ), + totalMemoryMb: coalesceNumber( + asNumber(perf?.totalMemoryMb), + asNumber(perfMemory?.totalHeapMb), + asNumber(fallback?.totalMemoryMb), + 0, + ), + entities: entityCount === undefined + ? undefined + : { + count: entityCount, + inViewDistance: coalesceNumber(asNumber(perfEntities?.inViewDistance), entityCount), + frustumCulled: coalesceNumber(asNumber(perfEntities?.frustumCulled), 0), + staticEnvironment: coalesceNumber(asNumber(perfEntities?.staticEnvironment), 0), + }, + chunks: chunkCount === undefined && visibleChunkCount === undefined + ? undefined + : { + count: coalesceNumber(chunkCount, visibleChunkCount, 0), + visible: coalesceNumber(visibleChunkCount, chunkCount, 0), + blocks: coalesceNumber(asNumber(perfChunks?.blocks), 0), + opaqueFaces: coalesceNumber(asNumber(perfChunks?.opaqueFaces), 0), + transparentFaces: coalesceNumber(asNumber(perfChunks?.transparentFaces), 0), + liquidFaces: coalesceNumber(asNumber(perfChunks?.liquidFaces), 0), + }, + gltf: perfGltf + ? { + files: coalesceNumber(asNumber(perfGltf?.files), 0), + sourceMeshes: coalesceNumber(asNumber(perfGltf?.sourceMeshes), 0), + clonedMeshes: coalesceNumber(asNumber(perfGltf?.clonedMeshes), asNumber(perfGltf?.clonedMeshCount), 0), + instancedMeshes: coalesceNumber(asNumber(perfGltf?.instancedMeshes), asNumber(perfGltf?.instancedMeshCount), 0), + drawCallsSaved: coalesceNumber(asNumber(perfGltf?.drawCallsSaved), 0), + } + : undefined, + }; +} + +function asObject(value: unknown): Record | undefined { + if (!value || typeof value !== 'object') { + return undefined; + } + + return value as Record; +} + +function asNumber(value: unknown): number | undefined { + return typeof value === 'number' && Number.isFinite(value) ? value : undefined; +} + +function coalesceNumber(...values: Array): number { + for (const value of values) { + if (value !== undefined) { + return value; + } + } + + return 0; +} diff --git a/packages/perf-tools/src/runners/MetricCollector.ts b/packages/perf-tools/src/runners/MetricCollector.ts new file mode 100644 index 00000000..598de81d --- /dev/null +++ b/packages/perf-tools/src/runners/MetricCollector.ts @@ -0,0 +1,172 @@ +export interface ProcessSnapshotEntry { + timestamp: number; + cpuPct: number; + rssMb: number; + threads: number; + fds: number; +} + +export interface CollectedMetrics { + serverSnapshots: ServerSnapshot[]; + clientSnapshots: ClientSnapshot[]; + tickReports: TickReportEntry[]; + spikes: SpikeEntry[]; + processSnapshots: ProcessSnapshotEntry[]; + startTime: number; + endTime: number; +} + +export interface ServerSnapshot { + timestamp: number; + source?: 'perf_harness' | 'legacy_perf_api'; + avgTickMs: number; + maxTickMs: number; + p95TickMs: number; + p99TickMs: number; + ticksOverBudget: number; + totalTicks: number; + budgetMs: number; + operations: Record; + memory: { heapUsedMb: number; heapTotalMb: number; rssMb: number }; + network?: { + connectedPlayers: number; + bytesSentTotal: number; + bytesReceivedTotal: number; + bytesSentPerSecond: number; + bytesReceivedPerSecond: number; + packetsSentPerSecond: number; + packetsReceivedPerSecond: number; + avgSerializationMs: number; + compressionCount: number; + }; +} + +export interface OperationSnapshot { + count: number; + avgMs: number; + p95Ms: number; + p99Ms: number; + maxMs: number; +} + +export interface ClientSnapshot { + timestamp: number; + source?: 'perf_bridge' | 'webgl_fallback'; + fps: number; + frameTimeMs: number; + drawCalls: number; + triangles: number; + textureMemoryMb: number; + geometries?: number; + textures?: number; + programs?: number; + usedMemoryMb?: number; + totalMemoryMb?: number; + entities?: { + count: number; + inViewDistance: number; + frustumCulled: number; + staticEnvironment: number; + }; + chunks?: { + count: number; + visible: number; + blocks: number; + opaqueFaces: number; + transparentFaces: number; + liquidFaces: number; + }; + gltf?: { + files: number; + sourceMeshes: number; + clonedMeshes: number; + instancedMeshes: number; + drawCallsSaved: number; + }; +} + +export interface TickReportEntry { + timestamp: number; + tick: number; + durationMs: number; + budgetPercent: number; + phases: Record; + entityCount: number; + playerCount: number; +} + +export interface SpikeEntry { + timestamp: number; + tick: number; + durationMs: number; + phases: Record; + entityCount: number; +} + +export default class MetricCollector { + private _serverSnapshots: ServerSnapshot[] = []; + private _clientSnapshots: ClientSnapshot[] = []; + private _tickReports: TickReportEntry[] = []; + private _spikes: SpikeEntry[] = []; + private _processSnapshots: ProcessSnapshotEntry[] = []; + private _startTime: number = 0; + private _collecting: boolean = false; + + public startCollecting(): void { + this._collecting = true; + this._startTime = Date.now(); + this._serverSnapshots = []; + this._clientSnapshots = []; + this._tickReports = []; + this._spikes = []; + this._processSnapshots = []; + } + + public stopCollecting(): CollectedMetrics { + this._collecting = false; + + return { + serverSnapshots: this._serverSnapshots, + clientSnapshots: this._clientSnapshots, + tickReports: this._tickReports, + spikes: this._spikes, + processSnapshots: this._processSnapshots, + startTime: this._startTime, + endTime: Date.now(), + }; + } + + public get isCollecting(): boolean { + return this._collecting; + } + + public addServerSnapshot(snapshot: ServerSnapshot): void { + if (!this._collecting) return; + + this._serverSnapshots.push(snapshot); + } + + public addClientSnapshot(snapshot: ClientSnapshot): void { + if (!this._collecting) return; + + this._clientSnapshots.push(snapshot); + } + + public addTickReport(report: TickReportEntry): void { + if (!this._collecting) return; + + this._tickReports.push(report); + } + + public addSpike(spike: SpikeEntry): void { + if (!this._collecting) return; + + this._spikes.push(spike); + } + + public addProcessSnapshot(snapshot: ProcessSnapshotEntry): void { + if (!this._collecting) return; + + this._processSnapshots.push(snapshot); + } +} diff --git a/packages/perf-tools/src/runners/ProcessMonitor.ts b/packages/perf-tools/src/runners/ProcessMonitor.ts new file mode 100644 index 00000000..6b5c2e8d --- /dev/null +++ b/packages/perf-tools/src/runners/ProcessMonitor.ts @@ -0,0 +1,213 @@ +import * as fs from 'node:fs'; + +export interface ProcessSnapshot { + timestamp: number; + cpuPct: number; + rssMb: number; + threads: number; + fds: number; +} + +export interface ProcessMetrics { + snapshots: ProcessSnapshot[]; + avgCpuPct: number; + maxCpuPct: number; + avgRssMb: number; + maxRssMb: number; + maxThreads: number; + maxFds: number; +} + +interface ProcStatSample { + utime: number; + stime: number; + wallMs: number; +} + +export default class ProcessMonitor { + private _pid: number = 0; + private _interval: ReturnType | null = null; + private _snapshots: ProcessSnapshot[] = []; + private _lastSamples: Map = new Map(); + private _clockTick: number; + + constructor() { + this._clockTick = 100; // sysconf(_SC_CLK_TCK) default on Linux + } + + public start(pid: number, intervalMs: number = 1000): void { + this._pid = pid; + this._snapshots = []; + this._lastSamples.clear(); + + this._primeBaseline(); // prime the CPU delta baseline + + this._interval = setInterval(() => { + try { + const snapshot = this._collect(); + if (snapshot) this._snapshots.push(snapshot); + } catch { + // process may have exited + } + }, intervalMs); + } + + public stop(): ProcessMetrics { + if (this._interval) { + clearInterval(this._interval); + this._interval = null; + } + + // one final collection + try { + const snapshot = this._collect(); + if (snapshot) this._snapshots.push(snapshot); + } catch { + // ignore + } + + return this._summarize(); + } + + /** Discover all PIDs in the process group (leader + children). */ + private _getGroupPids(): number[] { + const pids: number[] = []; + try { + const entries = fs.readdirSync('/proc'); + for (const entry of entries) { + const pid = parseInt(entry, 10); + if (isNaN(pid)) continue; + try { + const raw = fs.readFileSync(`/proc/${pid}/stat`, 'utf-8'); + const closeParen = raw.lastIndexOf(')'); + const fields = raw.slice(closeParen + 2).split(' '); + const pgid = parseInt(fields[2], 10); // pgrp is field index 4 in stat (0-based after comm) + if (pgid === this._pid) pids.push(pid); + } catch { + // process vanished + } + } + } catch { + // fallback to just the leader + pids.push(this._pid); + } + if (pids.length === 0) pids.push(this._pid); + return pids; + } + + private _primeBaseline(): void { + const pids = this._getGroupPids(); + for (const pid of pids) { + const sample = this._readProcStat(pid); + if (sample) this._lastSamples.set(pid, sample); + } + } + + private _collect(): ProcessSnapshot | null { + const pids = this._getGroupPids(); + let totalCpuPct = 0; + let totalRssKb = 0; + let totalThreads = 0; + let totalFds = 0; + + for (const pid of pids) { + const curSample = this._readProcStat(pid); + if (!curSample) continue; + + const prevSample = this._lastSamples.get(pid); + if (prevSample) { + totalCpuPct += this._calcCpuPct(prevSample, curSample); + } + this._lastSamples.set(pid, curSample); + + totalRssKb += this._readRssKb(pid); + totalThreads += this._readThreadCount(pid); + totalFds += this._countFds(pid); + } + + // prune stale PIDs + for (const pid of this._lastSamples.keys()) { + if (!pids.includes(pid)) this._lastSamples.delete(pid); + } + + if (pids.length === 0) return null; + + return { + timestamp: Date.now(), + cpuPct: totalCpuPct, + rssMb: totalRssKb / 1024, + threads: totalThreads, + fds: totalFds, + }; + } + + private _readProcStat(pid: number): ProcStatSample | null { + try { + const raw = fs.readFileSync(`/proc/${pid}/stat`, 'utf-8'); + const closeParen = raw.lastIndexOf(')'); + const fields = raw.slice(closeParen + 2).split(' '); + // fields[11] = utime, fields[12] = stime (after state, which is fields[0]) + const utime = parseInt(fields[11], 10); + const stime = parseInt(fields[12], 10); + return { utime, stime, wallMs: Date.now() }; + } catch { + return null; + } + } + + private _calcCpuPct(prev: ProcStatSample, cur: ProcStatSample): number { + const wallDeltaS = (cur.wallMs - prev.wallMs) / 1000; + if (wallDeltaS <= 0) return 0; + + const cpuDeltaTicks = (cur.utime + cur.stime) - (prev.utime + prev.stime); + const cpuDeltaS = cpuDeltaTicks / this._clockTick; + + return (cpuDeltaS / wallDeltaS) * 100; + } + + private _readRssKb(pid: number): number { + try { + const raw = fs.readFileSync(`/proc/${pid}/status`, 'utf-8'); + const match = raw.match(/VmRSS:\s+(\d+)\s+kB/); + return match ? parseInt(match[1], 10) : 0; + } catch { + return 0; + } + } + + private _readThreadCount(pid: number): number { + try { + const raw = fs.readFileSync(`/proc/${pid}/status`, 'utf-8'); + const match = raw.match(/Threads:\s+(\d+)/); + return match ? parseInt(match[1], 10) : 0; + } catch { + return 0; + } + } + + private _countFds(pid: number): number { + try { + return fs.readdirSync(`/proc/${pid}/fd`).length; + } catch { + return 0; + } + } + + private _summarize(): ProcessMetrics { + const snapshots = this._snapshots; + + if (snapshots.length === 0) { + return { snapshots: [], avgCpuPct: 0, maxCpuPct: 0, avgRssMb: 0, maxRssMb: 0, maxThreads: 0, maxFds: 0 }; + } + + return { + snapshots, + avgCpuPct: snapshots.reduce((s, v) => s + v.cpuPct, 0) / snapshots.length, + maxCpuPct: Math.max(...snapshots.map(s => s.cpuPct)), + avgRssMb: snapshots.reduce((s, v) => s + v.rssMb, 0) / snapshots.length, + maxRssMb: Math.max(...snapshots.map(s => s.rssMb)), + maxThreads: Math.max(...snapshots.map(s => s.threads)), + maxFds: Math.max(...snapshots.map(s => s.fds)), + }; + } +} diff --git a/packages/perf-tools/src/runners/ScenarioLoader.ts b/packages/perf-tools/src/runners/ScenarioLoader.ts new file mode 100644 index 00000000..1b449ded --- /dev/null +++ b/packages/perf-tools/src/runners/ScenarioLoader.ts @@ -0,0 +1,172 @@ +import * as fs from 'node:fs'; +import * as yaml from 'js-yaml'; + +export interface ScenarioVector3 { + x: number; + y: number; + z: number; +} + +export type ScenarioClientTarget = 'primary' | 'all' | 'extras'; + +export interface ScenarioAction { + type: + | 'spawn_bots' + | 'despawn_bots' + | 'load_map' + | 'generate_blocks' + | 'spawn_entities' + | 'despawn_entities' + | 'start_block_churn' + | 'stop_block_churn' + | 'create_worlds' + | 'set_default_world' + | 'clear_world' + | 'connect_clients' + | 'disconnect_clients' + | 'wait' + | 'walk_player' + | 'wait_for_entities' + | 'set_camera' + | 'throttle_cpu' + | 'send_chat' + | 'custom'; + count?: number; + behavior?: string; + durationMs?: number; + mapPath?: string; + position?: ScenarioVector3; + yaw?: number; + pitch?: number; + rate?: number; + message?: string; + worldId?: number; + kind?: 'model' | 'block'; + tag?: string; + options?: Record; + blockCount?: number; + layout?: 'dense' | 'slab'; + slabHeight?: number; + origin?: ScenarioVector3; + clear?: boolean; + blocksPerTick?: number; + blockTypeId?: number; + mode?: 'toggle' | 'place' | 'remove'; + min?: ScenarioVector3; + max?: ScenarioVector3; + setDefault?: boolean; + staggerMs?: number; + target?: ScenarioClientTarget; + script?: string; +} + +export interface ScenarioPhase { + name: string; + duration?: string; + actions?: ScenarioAction[]; + collect?: boolean; +} + +export interface ScenarioThresholds { + tick_duration_ms?: { avg?: number; p95?: number; p99?: number; max?: number }; + memory_mb?: { max?: number }; + fps?: { min?: number; avg?: number }; + network?: { maxBytesPerSecond?: number }; + client?: { + fps_min?: number; + fps_avg?: number; + draw_calls_max?: number; + triangles_max?: number; + frame_time_ms_max?: number; + }; +} + +export interface Scenario { + name: string; + description?: string; + serverScript?: string; + phases: ScenarioPhase[]; + thresholds?: ScenarioThresholds; + clients?: number; + browserClients?: number; + warmupMs?: number; +} + +export function parseDuration(duration: string): number { + const match = duration.match(/^(\d+(?:\.\d+)?)\s*(ms|s|m)$/); + + if (!match) throw new Error(`Invalid duration: ${duration}`); + + const value = parseFloat(match[1]); + const unit = match[2]; + + switch (unit) { + case 'ms': return value; + case 's': return value * 1000; + case 'm': return value * 60000; + default: return value; + } +} + +export function loadScenario(filePath: string): Scenario { + const content = fs.readFileSync(filePath, 'utf-8'); + const ext = filePath.split('.').pop()?.toLowerCase(); + + let raw: unknown; + + if (ext === 'yaml' || ext === 'yml') { + raw = yaml.load(content); + } else if (ext === 'json') { + raw = JSON.parse(content); + } else { + throw new Error(`Unsupported scenario format: ${ext}. Use .yaml, .yml, or .json`); + } + + return validateScenario(raw); +} + +function validateScenario(raw: unknown): Scenario { + if (!raw || typeof raw !== 'object') { + throw new Error('Scenario must be an object'); + } + + const obj = raw as Record; + + if (!obj.name || typeof obj.name !== 'string') { + throw new Error('Scenario must have a "name" string field'); + } + + if (!Array.isArray(obj.phases) || obj.phases.length === 0) { + throw new Error('Scenario must have a non-empty "phases" array'); + } + + return { + name: obj.name, + description: typeof obj.description === 'string' ? obj.description : undefined, + serverScript: typeof obj.serverScript === 'string' ? obj.serverScript : undefined, + phases: obj.phases.map(validatePhase), + thresholds: obj.thresholds as ScenarioThresholds | undefined, + clients: typeof obj.clients === 'number' ? obj.clients : undefined, + browserClients: typeof obj.browserClients === 'number' ? obj.browserClients : undefined, + warmupMs: typeof obj.warmupMs === 'number' ? obj.warmupMs : undefined, + }; +} + +function validatePhase(raw: unknown, index: number): ScenarioPhase { + if (!raw || typeof raw !== 'object') { + throw new Error(`Phase ${index} must be an object`); + } + + const obj = raw as Record; + + if (!obj.name || typeof obj.name !== 'string') { + throw new Error(`Phase ${index} must have a "name" string`); + } + + return { + name: obj.name, + duration: typeof obj.duration === 'string' ? obj.duration : undefined, + actions: Array.isArray(obj.actions) ? obj.actions as ScenarioAction[] : undefined, + collect: typeof obj.collect === 'boolean' ? obj.collect : undefined, + }; +} diff --git a/packages/perf-tools/src/runners/ServerApiClient.ts b/packages/perf-tools/src/runners/ServerApiClient.ts new file mode 100644 index 00000000..3a7bbf60 --- /dev/null +++ b/packages/perf-tools/src/runners/ServerApiClient.ts @@ -0,0 +1,400 @@ +import * as http from 'node:http'; +import * as https from 'node:https'; +import type { ServerSnapshot } from './MetricCollector.js'; + +interface HealthResponse { + status?: string; + version?: string; + runtime?: string; + playerCount?: number; +} + +export interface NetworkSnapshot { + connectedPlayers: number; + bytesSentTotal: number; + bytesReceivedTotal: number; + bytesSentPerSecond: number; + bytesReceivedPerSecond: number; + packetsSentPerSecond: number; + packetsReceivedPerSecond: number; + avgSerializationMs: number; + compressionCount: number; +} + +interface PerfSnapshotResponse { + source?: 'perf_harness' | 'legacy_perf_api'; + timestamp: number; + avgTickMs: number; + maxTickMs: number; + p95TickMs: number; + p99TickMs: number; + ticksOverBudget: number; + totalTicks: number; + budgetMs: number; + operations: Record; + memory: { heapUsedMb: number; heapTotalMb: number; rssMb: number }; + network?: NetworkSnapshot; +} + +interface LegacyStatsWindowSnapshot { + average?: number; + count?: number; + max?: number; + min?: number; + p50?: number; + p95?: number; + sampleCount?: number; +} + +interface LegacyPerfSnapshotResponse { + generatedAt?: string; + packets?: { + batches?: { + compressedBatches?: number; + rawBytes?: number; + reliableBatches?: number; + totalBatches?: number; + unreliableBatches?: number; + wireBytes?: number; + }; + families?: Record; + }; + spans?: Record; + worlds?: Record>; +} + +interface LegacyPerfEnvelopeResponse { + playerCount?: number; + process?: { + jsHeapSizeMb?: number; + jsHeapCapacityMb?: number; + processHeapSizeMb?: number; + rssSizeMb?: number; + }; + snapshot?: LegacyPerfSnapshotResponse; + version?: string; +} + +export type ServerAction = + | { type: 'spawn_bots'; count: number; behavior?: string; origin?: { x: number; y: number; z: number } } + | { type: 'despawn_bots'; count?: number } + | { type: 'load_map'; mapPath: string; worldId?: number } + | { type: 'generate_blocks'; blockCount: number; blockTypeId: number; worldId?: number; layout?: 'dense' | 'slab'; slabHeight?: number; origin?: { x: number; y: number; z: number }; clear?: boolean } + | { type: 'spawn_entities'; count: number; kind?: 'model' | 'block'; options?: Record; tag?: string } + | { type: 'despawn_entities'; tag?: string } + | { type: 'start_block_churn'; blocksPerTick: number; blockTypeId: number; mode?: 'toggle' | 'place' | 'remove'; min?: { x: number; y: number; z: number }; max?: { x: number; y: number; z: number } } + | { type: 'stop_block_churn' } + | { type: 'create_worlds'; count: number; mapPath?: string; setDefault?: boolean } + | { type: 'set_default_world'; worldId: number } + | { type: 'clear_world' } + | { type: 'reset' }; + +export default class ServerApiClient { + private _baseUrl: URL; + private _token: string | undefined; + private _perfApiMode: 'modern' | 'legacy' | undefined; + private _previousLegacyNetworkSample: + | { timestamp: number; bytesSentTotal: number; packetsSentTotal: number } + | undefined; + + constructor(baseUrl: string, options?: { token?: string }) { + this._baseUrl = new URL(baseUrl); + this._token = options?.token; + } + + public async waitForHealthy(timeoutMs: number = 90_000): Promise { + const start = Date.now(); + let lastError: unknown; + + while (Date.now() - start < timeoutMs) { + try { + const health = await this.health(); + + if (health.status === 'OK') { + return health; + } + } catch (error) { + lastError = error; + } + + await new Promise(resolve => setTimeout(resolve, 500)); + } + + const message = lastError instanceof Error ? lastError.message : String(lastError ?? 'unknown'); + throw new Error(`Server not healthy after ${timeoutMs}ms: ${message}`); + } + + public async health(): Promise { + const url = new URL('/', this._baseUrl); + const res = await this._request(url, { method: 'GET' }); + + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`Health check failed: ${res.statusCode} ${res.statusMessage}`); + } + + return JSON.parse(res.body) as HealthResponse; + } + + public async reset(): Promise { + const url = new URL('/__perf/reset', this._baseUrl); + const res = await this._request(url, { + method: 'POST', + headers: this._headers(), + }); + + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`Reset failed: ${res.statusCode} ${res.statusMessage}`); + } + } + + public async action(action: ServerAction): Promise { + if (this._perfApiMode === 'legacy') { + throw new Error('Perf action failed: target only exposes the legacy snapshot/reset perf API'); + } + + const url = new URL('/__perf/action', this._baseUrl); + const res = await this._request(url, { + method: 'POST', + headers: { + ...this._headers(), + 'content-type': 'application/json', + }, + body: JSON.stringify(action), + }); + + if (res.statusCode < 200 || res.statusCode >= 300) { + if (res.statusCode === 404 || res.statusCode === 501) { + this._perfApiMode = 'legacy'; + } + + throw new Error(`Action failed: ${res.statusCode} ${res.statusMessage}${res.body ? ` - ${res.body}` : ''}`); + } + } + + public async snapshot(): Promise { + if (this._perfApiMode === 'legacy') { + return await this._snapshotLegacy(); + } + + try { + const snapshot = await this._snapshotModern(); + + this._perfApiMode = 'modern'; + return snapshot; + } catch (error) { + if (!this._looksLikeMissingModernPerfApi(error)) { + throw error; + } + } + + const legacySnapshot = await this._snapshotLegacy(); + + this._perfApiMode = 'legacy'; + return legacySnapshot; + } + + private async _snapshotModern(): Promise { + const url = new URL('/__perf/snapshot', this._baseUrl); + const res = await this._request(url, { method: 'GET', headers: this._headers() }); + + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`Snapshot failed: ${res.statusCode} ${res.statusMessage}`); + } + + const data = JSON.parse(res.body) as PerfSnapshotResponse; + + return { + source: data.source ?? 'perf_harness', + timestamp: data.timestamp, + avgTickMs: data.avgTickMs, + maxTickMs: data.maxTickMs, + p95TickMs: data.p95TickMs, + p99TickMs: data.p99TickMs, + ticksOverBudget: data.ticksOverBudget, + totalTicks: data.totalTicks, + budgetMs: data.budgetMs, + operations: data.operations, + memory: data.memory, + network: data.network, + }; + } + + private async _snapshotLegacy(): Promise { + const url = new URL('/__perf', this._baseUrl); + const res = await this._request(url, { method: 'GET', headers: this._headers() }); + + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`Legacy snapshot failed: ${res.statusCode} ${res.statusMessage}`); + } + + const data = JSON.parse(res.body) as LegacyPerfEnvelopeResponse; + const snapshot = data.snapshot; + const spans = snapshot?.spans ?? {}; + const tickSnapshot = this._resolveLegacyTickSnapshot(spans); + const timestamp = this._resolveLegacyTimestamp(snapshot?.generatedAt); + const bytesSentTotal = snapshot?.packets?.batches?.wireBytes ?? 0; + const packetsSentTotal = Object.values(snapshot?.packets?.families ?? {}).reduce((total, family) => { + return total + (family.packetCount ?? 0); + }, 0); + let bytesSentPerSecond = 0; + let packetsSentPerSecond = 0; + + if (this._previousLegacyNetworkSample && timestamp > this._previousLegacyNetworkSample.timestamp) { + const elapsedSeconds = (timestamp - this._previousLegacyNetworkSample.timestamp) / 1000; + + if (elapsedSeconds > 0) { + bytesSentPerSecond = Math.max(0, (bytesSentTotal - this._previousLegacyNetworkSample.bytesSentTotal) / elapsedSeconds); + packetsSentPerSecond = Math.max(0, (packetsSentTotal - this._previousLegacyNetworkSample.packetsSentTotal) / elapsedSeconds); + } + } + + this._previousLegacyNetworkSample = { + timestamp, + bytesSentTotal, + packetsSentTotal, + }; + + return { + source: 'legacy_perf_api', + timestamp, + avgTickMs: tickSnapshot?.average ?? 0, + maxTickMs: tickSnapshot?.max ?? 0, + p95TickMs: tickSnapshot?.p95 ?? 0, + p99TickMs: tickSnapshot?.max ?? tickSnapshot?.p95 ?? 0, + ticksOverBudget: 0, + totalTicks: tickSnapshot?.count ?? tickSnapshot?.sampleCount ?? 0, + budgetMs: 1000 / 60, + operations: Object.fromEntries( + Object.entries(spans).map(([name, legacySpan]) => [name, { + count: legacySpan.count ?? legacySpan.sampleCount ?? 0, + avgMs: legacySpan.average ?? 0, + p95Ms: legacySpan.p95 ?? 0, + p99Ms: legacySpan.max ?? legacySpan.p95 ?? 0, + maxMs: legacySpan.max ?? 0, + }]), + ), + memory: { + heapUsedMb: data.process?.jsHeapSizeMb ?? data.process?.processHeapSizeMb ?? 0, + heapTotalMb: data.process?.jsHeapCapacityMb ?? 0, + rssMb: data.process?.rssSizeMb ?? 0, + }, + network: { + connectedPlayers: data.playerCount ?? 0, + bytesSentTotal, + bytesReceivedTotal: 0, + bytesSentPerSecond, + bytesReceivedPerSecond: 0, + packetsSentPerSecond, + packetsReceivedPerSecond: 0, + avgSerializationMs: spans.serialize_packets?.average ?? spans.serialize_packets_encode?.average ?? 0, + compressionCount: snapshot?.packets?.batches?.compressedBatches ?? 0, + }, + }; + } + + private _looksLikeMissingModernPerfApi(error: unknown): boolean { + if (!(error instanceof Error)) { + return false; + } + + return /Snapshot failed: (404|500|501)\b/.test(error.message); + } + + private _resolveLegacyTimestamp(generatedAt: string | undefined): number { + if (!generatedAt) { + return Date.now(); + } + + const parsed = Date.parse(generatedAt); + + return Number.isFinite(parsed) ? parsed : Date.now(); + } + + private _resolveLegacyTickSnapshot( + spans: Record, + ): LegacyStatsWindowSnapshot | undefined { + const preferred = ['world_tick', 'ticker_tick']; + + for (const name of preferred) { + if (spans[name]) { + return spans[name]; + } + } + + for (const [name, snapshot] of Object.entries(spans)) { + if (name.includes('tick')) { + return snapshot; + } + } + + return undefined; + } + + private _headers(): Record { + if (!this._token) return {}; + + return { + 'x-hytopia-perf-token': this._token, + }; + } + + private async _request( + url: URL, + options: { method: string; headers?: Record; body?: string }, + ): Promise<{ statusCode: number; statusMessage: string; body: string }> { + const transport = url.protocol === 'https:' ? https : http; + + return await new Promise((resolve, reject) => { + const req = transport.request(url, { + method: options.method, + headers: options.headers, + rejectUnauthorized: !this._shouldAllowInsecureTls(url), + }, res => { + const chunks: Buffer[] = []; + + res.on('data', chunk => { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + }); + res.on('end', () => { + resolve({ + statusCode: res.statusCode ?? 0, + statusMessage: res.statusMessage ?? 'Unknown Error', + body: Buffer.concat(chunks).toString('utf8'), + }); + }); + }); + + req.on('error', reject); + + if (options.body) { + req.write(options.body); + } + + req.end(); + }); + } + + private _shouldAllowInsecureTls(url: URL): boolean { + if (url.protocol !== 'https:') { + return false; + } + + return url.hostname === 'localhost' + || url.hostname === '127.0.0.1' + || url.hostname === '::1' + || url.hostname === 'local.hytopiahosting.com'; + } +} diff --git a/packages/perf-tools/src/runners/WsClient.ts b/packages/perf-tools/src/runners/WsClient.ts new file mode 100644 index 00000000..30b302c9 --- /dev/null +++ b/packages/perf-tools/src/runners/WsClient.ts @@ -0,0 +1,60 @@ +import { WebSocket } from 'ws'; + +export interface WsClientOptions { + url: string; +} + +export default class WsClient { + private _ws: WebSocket | null = null; + private _options: WsClientOptions; + + constructor(options: WsClientOptions) { + this._options = options; + } + + public async connect(): Promise { + if (this._ws) return; + + await new Promise((resolve, reject) => { + const ws = new WebSocket(this._options.url); + + const cleanup = () => { + ws.off('open', onOpen); + ws.off('error', onError); + }; + + const onOpen = () => { + cleanup(); + this._ws = ws; + resolve(); + }; + + const onError = (err: unknown) => { + cleanup(); + try { ws.close(); } catch { /* NOOP */ } + reject(err); + }; + + ws.on('open', onOpen); + ws.on('error', onError); + ws.on('message', () => {}); + }); + } + + public async close(): Promise { + const ws = this._ws; + this._ws = null; + + if (!ws) return; + + await new Promise(resolve => { + ws.once('close', () => resolve()); + try { + ws.close(); + } catch { + resolve(); + } + }); + } +} + diff --git a/packages/perf-tools/tsconfig.json b/packages/perf-tools/tsconfig.json new file mode 100644 index 00000000..2cb00c31 --- /dev/null +++ b/packages/perf-tools/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/perf-results/block-churn.json b/perf-results/block-churn.json new file mode 100644 index 00000000..927cc0f2 --- /dev/null +++ b/perf-results/block-churn.json @@ -0,0 +1,96 @@ +{ + "timestamp": "2026-03-05T13:33:14.607Z", + "scenario": "block-churn", + "durationMs": 63725, + "baseline": { + "avgTickMs": 0.7478451288708664, + "maxTickMs": 2.4919690000006085, + "p95TickMs": 1.2697642666665616, + "p99TickMs": 1.6568233499997405, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 53.592525227864584, + "operations": { + "entities_tick": { + "avgMs": 0.0013408276736730962, + "p95Ms": 0.0021010333335349666 + }, + "physics_step": { + "avgMs": 0.5195412378673131, + "p95Ms": 0.687410099999958 + }, + "physics_cleanup": { + "avgMs": 0.004241948672423851, + "p95Ms": 0.006812216666670186 + }, + "simulation_step": { + "avgMs": 0.5280920586234846, + "p95Ms": 0.7004179999999602 + }, + "entities_emit_updates": { + "avgMs": 0.0007233812045910356, + "p95Ms": 0.0011036666669042461 + }, + "world_tick": { + "avgMs": 0.744934013290529, + "p95Ms": 1.2329469999999978 + }, + "serialize_packets": { + "avgMs": 0.05616502495925675, + "p95Ms": 0.08697479999981018 + }, + "send_packets": { + "avgMs": 0.036419154585375105, + "p95Ms": 0.14526188333349335 + }, + "send_all_packets": { + "avgMs": 0.38529490651853676, + "p95Ms": 0.6914354499996382 + }, + "network_synchronize_cleanup": { + "avgMs": 0.005455039610011175, + "p95Ms": 0.007534549999945739 + }, + "network_synchronize": { + "avgMs": 0.412380600656741, + "p95Ms": 0.7254833166664867 + }, + "ticker_tick": { + "avgMs": 1.1041424130552329, + "p95Ms": 1.7002290999999938 + } + }, + "network": { + "totalBytesSent": 32296320, + "totalBytesReceived": 0, + "maxConnectedPlayers": 10, + "avgBytesSentPerSecond": 536648.3619004155, + "maxBytesSentPerSecond": 553833.8316077758, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 300.09213658368157, + "maxPacketsSentPerSecond": 309.1758247479803, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0.053296203268412, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "setup", + "durationMs": 80, + "collected": false + }, + { + "name": "churn-and-measure", + "durationMs": 59107, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/perf-results/blocks-500k-dense.json b/perf-results/blocks-500k-dense.json new file mode 100644 index 00000000..8e0efbd1 --- /dev/null +++ b/perf-results/blocks-500k-dense.json @@ -0,0 +1,101 @@ +{ + "timestamp": "2026-03-05T13:33:26.233Z", + "scenario": "blocks-500k-dense", + "durationMs": 72841, + "baseline": { + "avgTickMs": 0.1631674734917104, + "maxTickMs": 72.8982930000002, + "p95TickMs": 0.10288778333360823, + "p99TickMs": 1.5982031666683119, + "ticksOverBudgetPct": 0.05428488708743486, + "avgMemoryMb": 51.6248166402181, + "operations": { + "entities_tick": { + "avgMs": 0.0011274534396748448, + "p95Ms": 0.0017713833337135536 + }, + "physics_step": { + "avgMs": 0.034213811505710776, + "p95Ms": 0.0527195333330989 + }, + "physics_cleanup": { + "avgMs": 0.0037263208330111795, + "p95Ms": 0.005419000000044131 + }, + "simulation_step": { + "avgMs": 0.04138255903810552, + "p95Ms": 0.06454301666659983 + }, + "entities_emit_updates": { + "avgMs": 0.0006063593384779239, + "p95Ms": 0.0009713166658912087 + }, + "send_all_packets": { + "avgMs": 0.16913427650803953, + "p95Ms": 0.11162658333226622 + }, + "network_synchronize_cleanup": { + "avgMs": 0.003721956903819125, + "p95Ms": 0.004957583333119449 + }, + "network_synchronize": { + "avgMs": 0.2159241547183823, + "p95Ms": 0.14127511666714174 + }, + "world_tick": { + "avgMs": 0.16040858436057095, + "p95Ms": 0.09963781666616948 + }, + "ticker_tick": { + "avgMs": 0.20196307873702138, + "p95Ms": 0.15624794999924538 + }, + "serialize_packets": { + "avgMs": 1.365306317073015, + "p95Ms": 2.832683000000543 + }, + "send_packets": { + "avgMs": 1.1494577413793416, + "p95Ms": 3.2994650000000547 + } + }, + "network": { + "totalBytesSent": 71027, + "totalBytesReceived": 0, + "maxConnectedPlayers": 20, + "avgBytesSentPerSecond": 864.3487947089699, + "maxBytesSentPerSecond": 51860.92768253819, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 1.9105799311432028, + "maxPacketsSentPerSecond": 114.63479586859216, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 1.3619142926828531, + "compressionCountTotal": 20 + } + }, + "phases": [ + { + "name": "setup-world", + "durationMs": 11, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 10004, + "collected": false + }, + { + "name": "join-and-measure", + "durationMs": 58286, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/perf-results/entity-density.json b/perf-results/entity-density.json new file mode 100644 index 00000000..d15822c3 --- /dev/null +++ b/perf-results/entity-density.json @@ -0,0 +1,93 @@ +{ + "timestamp": "2026-03-05T13:32:02.575Z", + "scenario": "entity-density", + "durationMs": 73977, + "baseline": { + "avgTickMs": 0.3924285878918189, + "maxTickMs": 1.970153999995091, + "p95TickMs": 0.5939613166672036, + "p99TickMs": 0.7920050833331819, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 43.67034441630046, + "operations": { + "entities_tick": { + "avgMs": 0.020244843751854383, + "p95Ms": 0.03560461666662983 + }, + "physics_step": { + "avgMs": 0.1488405374272718, + "p95Ms": 0.21693571666686087 + }, + "physics_cleanup": { + "avgMs": 0.002849293828988602, + "p95Ms": 0.003773400000560893 + }, + "simulation_step": { + "avgMs": 0.15477759287683973, + "p95Ms": 0.22579720000024583 + }, + "entities_emit_updates": { + "avgMs": 0.19779759032391983, + "p95Ms": 0.3156859999987925 + }, + "world_tick": { + "avgMs": 0.3903471358062624, + "p95Ms": 0.5898788333338719 + }, + "ticker_tick": { + "avgMs": 0.4233709562050146, + "p95Ms": 0.6320953999998286 + }, + "send_all_packets": { + "avgMs": 0.0029213276313554246, + "p95Ms": 0.0035241833331080366 + }, + "network_synchronize_cleanup": { + "avgMs": 0.004256742413836006, + "p95Ms": 0.005522016666509444 + }, + "network_synchronize": { + "avgMs": 0.020431206791100387, + "p95Ms": 0.03007198333358853 + } + }, + "network": { + "totalBytesSent": 0, + "totalBytesReceived": 0, + "maxConnectedPlayers": 0, + "avgBytesSentPerSecond": 0, + "maxBytesSentPerSecond": 0, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 0, + "maxPacketsSentPerSecond": 0, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "setup", + "durationMs": 53, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 8835, + "collected": false + }, + { + "name": "measure", + "durationMs": 59042, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/perf-results/idle.json b/perf-results/idle.json new file mode 100644 index 00000000..49dcdf1e --- /dev/null +++ b/perf-results/idle.json @@ -0,0 +1,88 @@ +{ + "timestamp": "2026-03-05T13:31:11.934Z", + "scenario": "idle-baseline", + "durationMs": 34982, + "baseline": { + "avgTickMs": 0.04719115960534945, + "maxTickMs": 0.6909230000019306, + "p95TickMs": 0.08715813333368108, + "p99TickMs": 0.14540096666669342, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 40.678313700358075, + "operations": { + "entities_tick": { + "avgMs": 0.001102290221576149, + "p95Ms": 0.0018749000001662352 + }, + "physics_step": { + "avgMs": 0.02373388734481303, + "p95Ms": 0.03907096666656192 + }, + "physics_cleanup": { + "avgMs": 0.003181443150410741, + "p95Ms": 0.005339933333289082 + }, + "simulation_step": { + "avgMs": 0.02967492297315783, + "p95Ms": 0.049755566666711576 + }, + "entities_emit_updates": { + "avgMs": 0.0004899452923573849, + "p95Ms": 0.0008204333329255557 + }, + "send_all_packets": { + "avgMs": 0.003244902913314326, + "p95Ms": 0.0050246999998307725 + }, + "network_synchronize_cleanup": { + "avgMs": 0.0025102745460807542, + "p95Ms": 0.0034839666665902767 + }, + "network_synchronize": { + "avgMs": 0.013592855373576471, + "p95Ms": 0.021785066666476876 + }, + "world_tick": { + "avgMs": 0.044920792441284045, + "p95Ms": 0.07788763333304208 + }, + "ticker_tick": { + "avgMs": 0.07728472066898213, + "p95Ms": 0.12599726666652108 + } + }, + "network": { + "totalBytesSent": 0, + "totalBytesReceived": 0, + "maxConnectedPlayers": 0, + "avgBytesSentPerSecond": 0, + "maxBytesSentPerSecond": 0, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 0, + "maxPacketsSentPerSecond": 0, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "warmup", + "durationMs": 5004, + "collected": false + }, + { + "name": "measure", + "durationMs": 28938, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 30, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/perf-results/large-world.json b/perf-results/large-world.json new file mode 100644 index 00000000..f9690ed9 --- /dev/null +++ b/perf-results/large-world.json @@ -0,0 +1,98 @@ +{ + "timestamp": "2026-03-05T13:33:35.440Z", + "scenario": "large-world", + "durationMs": 83198, + "baseline": { + "avgTickMs": 0.3502514052795173, + "maxTickMs": 0.9146570000011707, + "p95TickMs": 0.5091090999991744, + "p99TickMs": 0.598821266665982, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 43.48845672607422, + "operations": { + "entities_tick": { + "avgMs": 0.041368946448760026, + "p95Ms": 0.06814049999908699 + }, + "physics_step": { + "avgMs": 0.24694364356640122, + "p95Ms": 0.3470961333325249 + }, + "physics_cleanup": { + "avgMs": 0.0038016709050663227, + "p95Ms": 0.005615016666342854 + }, + "simulation_step": { + "avgMs": 0.2549660947558462, + "p95Ms": 0.35964101666668286 + }, + "entities_emit_updates": { + "avgMs": 0.02854657873016627, + "p95Ms": 0.04696543333381366 + }, + "send_all_packets": { + "avgMs": 0.0028167214646624707, + "p95Ms": 0.004112333333857047 + }, + "network_synchronize_cleanup": { + "avgMs": 0.005738166117265033, + "p95Ms": 0.007642583333169265 + }, + "network_synchronize": { + "avgMs": 0.026373561114945786, + "p95Ms": 0.040851699998953946 + }, + "world_tick": { + "avgMs": 0.34763314835848064, + "p95Ms": 0.49770978333429716 + }, + "ticker_tick": { + "avgMs": 0.38605935829633026, + "p95Ms": 0.549840633333406 + } + }, + "network": { + "totalBytesSent": 0, + "totalBytesReceived": 0, + "maxConnectedPlayers": 0, + "avgBytesSentPerSecond": 0, + "maxBytesSentPerSecond": 0, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 0, + "maxPacketsSentPerSecond": 0, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "load-world", + "durationMs": 5579, + "collected": false + }, + { + "name": "spawn-bots", + "durationMs": 152, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 10004, + "collected": false + }, + { + "name": "measure", + "durationMs": 57934, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/perf-results/stress.json b/perf-results/stress.json new file mode 100644 index 00000000..8ea32124 --- /dev/null +++ b/perf-results/stress.json @@ -0,0 +1,93 @@ +{ + "timestamp": "2026-03-05T13:31:52.170Z", + "scenario": "stress-test", + "durationMs": 68948, + "baseline": { + "avgTickMs": 0.2595655961719306, + "maxTickMs": 2.327634000001126, + "p95TickMs": 0.4197937500005234, + "p99TickMs": 1.3410269000005732, + "ticksOverBudgetPct": 0, + "avgMemoryMb": 47.012769317626955, + "operations": { + "entities_tick": { + "avgMs": 0.0797661296938092, + "p95Ms": 0.11473178333332422 + }, + "physics_step": { + "avgMs": 0.08759558901160559, + "p95Ms": 0.12878991666675574 + }, + "physics_cleanup": { + "avgMs": 0.0023959388187931724, + "p95Ms": 0.003491416666535467 + }, + "simulation_step": { + "avgMs": 0.09300718503222376, + "p95Ms": 0.13865256666616307 + }, + "entities_emit_updates": { + "avgMs": 0.06381610782518951, + "p95Ms": 0.10084328333332451 + }, + "send_all_packets": { + "avgMs": 0.0024664665928586663, + "p95Ms": 0.003504566666651954 + }, + "network_synchronize_cleanup": { + "avgMs": 0.004384639747884721, + "p95Ms": 0.0057560833345633 + }, + "network_synchronize": { + "avgMs": 0.028199720105262952, + "p95Ms": 0.0410169166661035 + }, + "world_tick": { + "avgMs": 0.25732274811424377, + "p95Ms": 0.42134193333279957 + }, + "ticker_tick": { + "avgMs": 0.29024768527190165, + "p95Ms": 0.4716848000004272 + } + }, + "network": { + "totalBytesSent": 0, + "totalBytesReceived": 0, + "maxConnectedPlayers": 0, + "avgBytesSentPerSecond": 0, + "maxBytesSentPerSecond": 0, + "avgBytesReceivedPerSecond": 0, + "maxBytesReceivedPerSecond": 0, + "avgPacketsSentPerSecond": 0, + "maxPacketsSentPerSecond": 0, + "avgPacketsReceivedPerSecond": 0, + "maxPacketsReceivedPerSecond": 0, + "avgSerializationMs": 0, + "compressionCountTotal": 0 + } + }, + "phases": [ + { + "name": "spawn-entities", + "durationMs": 44, + "collected": false + }, + { + "name": "stabilize", + "durationMs": 5002, + "collected": false + }, + { + "name": "measure", + "durationMs": 57850, + "collected": true + } + ], + "metrics": { + "tickReportCount": 0, + "spikeCount": 0, + "serverSnapshotCount": 60, + "clientSnapshotCount": 0 + } +} \ No newline at end of file diff --git a/sdk/docs/server.anyworldmap.md b/sdk/docs/server.anyworldmap.md new file mode 100644 index 00000000..467b8608 --- /dev/null +++ b/sdk/docs/server.anyworldmap.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [server](./server.md) > [AnyWorldMap](./server.anyworldmap.md) + +## AnyWorldMap type + +**Signature:** + +```typescript +export type AnyWorldMap = WorldMap | CompressedWorldMap | WorldMapChunkCache; +``` +**References:** [WorldMap](./server.worldmap.md), [CompressedWorldMap](./server.compressedworldmap.md), [WorldMapChunkCache](./server.worldmapchunkcache.md) + diff --git a/sdk/docs/server.botbehavior.md b/sdk/docs/server.botbehavior.md new file mode 100644 index 00000000..f2bcda52 --- /dev/null +++ b/sdk/docs/server.botbehavior.md @@ -0,0 +1,77 @@ + + +[Home](./index.md) > [server](./server.md) > [BotBehavior](./server.botbehavior.md) + +## BotBehavior interface + +**Signature:** + +```typescript +export interface BotBehavior +``` + +## Properties + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[name](./server.botbehavior.name.md) + + + + + + + +string + + + + + +
+ +## Methods + + + +
+ +Method + + + + +Description + + +
+ +[tick(bot, world, deltaTimeMs)](./server.botbehavior.tick.md) + + + + + +
diff --git a/sdk/docs/server.botbehavior.name.md b/sdk/docs/server.botbehavior.name.md new file mode 100644 index 00000000..03d4535c --- /dev/null +++ b/sdk/docs/server.botbehavior.name.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [BotBehavior](./server.botbehavior.md) > [name](./server.botbehavior.name.md) + +## BotBehavior.name property + +**Signature:** + +```typescript +name: string; +``` diff --git a/sdk/docs/server.botbehavior.tick.md b/sdk/docs/server.botbehavior.tick.md new file mode 100644 index 00000000..72a1e45a --- /dev/null +++ b/sdk/docs/server.botbehavior.tick.md @@ -0,0 +1,77 @@ + + +[Home](./index.md) > [server](./server.md) > [BotBehavior](./server.botbehavior.md) > [tick](./server.botbehavior.tick.md) + +## BotBehavior.tick() method + +**Signature:** + +```typescript +tick(bot: BotPlayer, world: World, deltaTimeMs: number): void; +``` + +## Parameters + + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +bot + + + + +[BotPlayer](./server.botplayer.md) + + + + + +
+ +world + + + + +[World](./server.world.md) + + + + + +
+ +deltaTimeMs + + + + +number + + + + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.botmanager.botcount.md b/sdk/docs/server.botmanager.botcount.md new file mode 100644 index 00000000..bbc902ed --- /dev/null +++ b/sdk/docs/server.botmanager.botcount.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [BotManager](./server.botmanager.md) > [botCount](./server.botmanager.botcount.md) + +## BotManager.botCount property + +**Signature:** + +```typescript +get botCount(): number; +``` diff --git a/sdk/docs/server.botmanager.despawnall.md b/sdk/docs/server.botmanager.despawnall.md new file mode 100644 index 00000000..42bcc3b3 --- /dev/null +++ b/sdk/docs/server.botmanager.despawnall.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [server](./server.md) > [BotManager](./server.botmanager.md) > [despawnAll](./server.botmanager.despawnall.md) + +## BotManager.despawnAll() method + +**Signature:** + +```typescript +despawnAll(): void; +``` +**Returns:** + +void + diff --git a/sdk/docs/server.botmanager.despawnbot.md b/sdk/docs/server.botmanager.despawnbot.md new file mode 100644 index 00000000..72259ed9 --- /dev/null +++ b/sdk/docs/server.botmanager.despawnbot.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [server](./server.md) > [BotManager](./server.botmanager.md) > [despawnBot](./server.botmanager.despawnbot.md) + +## BotManager.despawnBot() method + +**Signature:** + +```typescript +despawnBot(id: number): void; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +id + + + + +number + + + + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.botmanager.getallbots.md b/sdk/docs/server.botmanager.getallbots.md new file mode 100644 index 00000000..9e3f4c13 --- /dev/null +++ b/sdk/docs/server.botmanager.getallbots.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [server](./server.md) > [BotManager](./server.botmanager.md) > [getAllBots](./server.botmanager.getallbots.md) + +## BotManager.getAllBots() method + +**Signature:** + +```typescript +getAllBots(): BotPlayer[]; +``` +**Returns:** + +[BotPlayer](./server.botplayer.md)\[\] + diff --git a/sdk/docs/server.botmanager.getbot.md b/sdk/docs/server.botmanager.getbot.md new file mode 100644 index 00000000..f23c33aa --- /dev/null +++ b/sdk/docs/server.botmanager.getbot.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [server](./server.md) > [BotManager](./server.botmanager.md) > [getBot](./server.botmanager.getbot.md) + +## BotManager.getBot() method + +**Signature:** + +```typescript +getBot(id: number): BotPlayer | undefined; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +id + + + + +number + + + + + +
+**Returns:** + +[BotPlayer](./server.botplayer.md) \| undefined + diff --git a/sdk/docs/server.botmanager.instance.md b/sdk/docs/server.botmanager.instance.md new file mode 100644 index 00000000..6e9389a5 --- /dev/null +++ b/sdk/docs/server.botmanager.instance.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [BotManager](./server.botmanager.md) > [instance](./server.botmanager.instance.md) + +## BotManager.instance property + +**Signature:** + +```typescript +static get instance(): BotManager; +``` diff --git a/sdk/docs/server.botmanager.md b/sdk/docs/server.botmanager.md new file mode 100644 index 00000000..dca97d37 --- /dev/null +++ b/sdk/docs/server.botmanager.md @@ -0,0 +1,168 @@ + + +[Home](./index.md) > [server](./server.md) > [BotManager](./server.botmanager.md) + +## BotManager class + +**Signature:** + +```typescript +export default class BotManager +``` + +## Properties + + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[botCount](./server.botmanager.botcount.md) + + + + +`readonly` + + + + +number + + + + + +
+ +[instance](./server.botmanager.instance.md) + + + + +`static` + +`readonly` + + + + +[BotManager](./server.botmanager.md) + + + + + +
+ +## Methods + + + + + + + + +
+ +Method + + + + +Modifiers + + + + +Description + + +
+ +[despawnAll()](./server.botmanager.despawnall.md) + + + + + + + + +
+ +[despawnBot(id)](./server.botmanager.despawnbot.md) + + + + + + + + +
+ +[getAllBots()](./server.botmanager.getallbots.md) + + + + + + + + +
+ +[getBot(id)](./server.botmanager.getbot.md) + + + + + + + + +
+ +[spawnBot(world, options)](./server.botmanager.spawnbot.md) + + + + + + + + +
+ +[spawnBots(world, count, options)](./server.botmanager.spawnbots.md) + + + + + + + + +
diff --git a/sdk/docs/server.botmanager.spawnbot.md b/sdk/docs/server.botmanager.spawnbot.md new file mode 100644 index 00000000..4246dfc6 --- /dev/null +++ b/sdk/docs/server.botmanager.spawnbot.md @@ -0,0 +1,65 @@ + + +[Home](./index.md) > [server](./server.md) > [BotManager](./server.botmanager.md) > [spawnBot](./server.botmanager.spawnbot.md) + +## BotManager.spawnBot() method + +**Signature:** + +```typescript +spawnBot(world: World, options?: BotPlayerOptions): BotPlayer; +``` + +## Parameters + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +world + + + + +[World](./server.world.md) + + + + + +
+ +options + + + + +[BotPlayerOptions](./server.botplayeroptions.md) + + + + +_(Optional)_ + + +
+**Returns:** + +[BotPlayer](./server.botplayer.md) + diff --git a/sdk/docs/server.botmanager.spawnbots.md b/sdk/docs/server.botmanager.spawnbots.md new file mode 100644 index 00000000..6305723d --- /dev/null +++ b/sdk/docs/server.botmanager.spawnbots.md @@ -0,0 +1,79 @@ + + +[Home](./index.md) > [server](./server.md) > [BotManager](./server.botmanager.md) > [spawnBots](./server.botmanager.spawnbots.md) + +## BotManager.spawnBots() method + +**Signature:** + +```typescript +spawnBots(world: World, count: number, options?: BotPlayerOptions): BotPlayer[]; +``` + +## Parameters + + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +world + + + + +[World](./server.world.md) + + + + + +
+ +count + + + + +number + + + + + +
+ +options + + + + +[BotPlayerOptions](./server.botplayeroptions.md) + + + + +_(Optional)_ + + +
+**Returns:** + +[BotPlayer](./server.botplayer.md)\[\] + diff --git a/sdk/docs/server.botplayer._constructor_.md b/sdk/docs/server.botplayer._constructor_.md new file mode 100644 index 00000000..3c46e121 --- /dev/null +++ b/sdk/docs/server.botplayer._constructor_.md @@ -0,0 +1,63 @@ + + +[Home](./index.md) > [server](./server.md) > [BotPlayer](./server.botplayer.md) > [(constructor)](./server.botplayer._constructor_.md) + +## BotPlayer.(constructor) + +Constructs a new instance of the `BotPlayer` class + +**Signature:** + +```typescript +constructor(world: World, options?: BotPlayerOptions); +``` + +## Parameters + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +world + + + + +[World](./server.world.md) + + + + + +
+ +options + + + + +[BotPlayerOptions](./server.botplayeroptions.md) + + + + +_(Optional)_ + + +
diff --git a/sdk/docs/server.botplayer.controller.md b/sdk/docs/server.botplayer.controller.md new file mode 100644 index 00000000..960d79f9 --- /dev/null +++ b/sdk/docs/server.botplayer.controller.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [BotPlayer](./server.botplayer.md) > [controller](./server.botplayer.controller.md) + +## BotPlayer.controller property + +**Signature:** + +```typescript +get controller(): SimpleEntityController; +``` diff --git a/sdk/docs/server.botplayer.despawn.md b/sdk/docs/server.botplayer.despawn.md new file mode 100644 index 00000000..c8bb024e --- /dev/null +++ b/sdk/docs/server.botplayer.despawn.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [server](./server.md) > [BotPlayer](./server.botplayer.md) > [despawn](./server.botplayer.despawn.md) + +## BotPlayer.despawn() method + +**Signature:** + +```typescript +despawn(): void; +``` +**Returns:** + +void + diff --git a/sdk/docs/server.botplayer.entity.md b/sdk/docs/server.botplayer.entity.md new file mode 100644 index 00000000..2d9037f9 --- /dev/null +++ b/sdk/docs/server.botplayer.entity.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [BotPlayer](./server.botplayer.md) > [entity](./server.botplayer.entity.md) + +## BotPlayer.entity property + +**Signature:** + +```typescript +readonly entity: Entity; +``` diff --git a/sdk/docs/server.botplayer.id.md b/sdk/docs/server.botplayer.id.md new file mode 100644 index 00000000..4b7b32c3 --- /dev/null +++ b/sdk/docs/server.botplayer.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [BotPlayer](./server.botplayer.md) > [id](./server.botplayer.id.md) + +## BotPlayer.id property + +**Signature:** + +```typescript +readonly id: number; +``` diff --git a/sdk/docs/server.botplayer.isspawned.md b/sdk/docs/server.botplayer.isspawned.md new file mode 100644 index 00000000..4e679a4c --- /dev/null +++ b/sdk/docs/server.botplayer.isspawned.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [BotPlayer](./server.botplayer.md) > [isSpawned](./server.botplayer.isspawned.md) + +## BotPlayer.isSpawned property + +**Signature:** + +```typescript +get isSpawned(): boolean; +``` diff --git a/sdk/docs/server.botplayer.md b/sdk/docs/server.botplayer.md new file mode 100644 index 00000000..54b6c6b3 --- /dev/null +++ b/sdk/docs/server.botplayer.md @@ -0,0 +1,252 @@ + + +[Home](./index.md) > [server](./server.md) > [BotPlayer](./server.botplayer.md) + +## BotPlayer class + +**Signature:** + +```typescript +export default class BotPlayer +``` + +## Constructors + + + +
+ +Constructor + + + + +Modifiers + + + + +Description + + +
+ +[(constructor)(world, options)](./server.botplayer._constructor_.md) + + + + + + + +Constructs a new instance of the `BotPlayer` class + + +
+ +## Properties + + + + + + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[controller](./server.botplayer.controller.md) + + + + +`readonly` + + + + +[SimpleEntityController](./server.simpleentitycontroller.md) + + + + + +
+ +[entity](./server.botplayer.entity.md) + + + + +`readonly` + + + + +[Entity](./server.entity.md) + + + + + +
+ +[id](./server.botplayer.id.md) + + + + +`readonly` + + + + +number + + + + + +
+ +[isSpawned](./server.botplayer.isspawned.md) + + + + +`readonly` + + + + +boolean + + + + + +
+ +[name](./server.botplayer.name.md) + + + + +`readonly` + + + + +string + + + + + +
+ +[world](./server.botplayer.world.md) + + + + +`readonly` + + + + +[World](./server.world.md) + + + + + +
+ +## Methods + + + + + + +
+ +Method + + + + +Modifiers + + + + +Description + + +
+ +[despawn()](./server.botplayer.despawn.md) + + + + + + + + +
+ +[setBehavior(behavior)](./server.botplayer.setbehavior.md) + + + + + + + + +
+ +[spawn(position)](./server.botplayer.spawn.md) + + + + + + + + +
+ +[teleport(position)](./server.botplayer.teleport.md) + + + + + + + + +
diff --git a/sdk/docs/server.botplayer.name.md b/sdk/docs/server.botplayer.name.md new file mode 100644 index 00000000..808764ad --- /dev/null +++ b/sdk/docs/server.botplayer.name.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [BotPlayer](./server.botplayer.md) > [name](./server.botplayer.name.md) + +## BotPlayer.name property + +**Signature:** + +```typescript +readonly name: string; +``` diff --git a/sdk/docs/server.botplayer.setbehavior.md b/sdk/docs/server.botplayer.setbehavior.md new file mode 100644 index 00000000..1353d5a9 --- /dev/null +++ b/sdk/docs/server.botplayer.setbehavior.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [server](./server.md) > [BotPlayer](./server.botplayer.md) > [setBehavior](./server.botplayer.setbehavior.md) + +## BotPlayer.setBehavior() method + +**Signature:** + +```typescript +setBehavior(behavior: BotBehavior): void; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +behavior + + + + +[BotBehavior](./server.botbehavior.md) + + + + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.botplayer.spawn.md b/sdk/docs/server.botplayer.spawn.md new file mode 100644 index 00000000..b2a67d4e --- /dev/null +++ b/sdk/docs/server.botplayer.spawn.md @@ -0,0 +1,51 @@ + + +[Home](./index.md) > [server](./server.md) > [BotPlayer](./server.botplayer.md) > [spawn](./server.botplayer.spawn.md) + +## BotPlayer.spawn() method + +**Signature:** + +```typescript +spawn(position?: Vector3Like): void; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +position + + + + +[Vector3Like](./server.vector3like.md) + + + + +_(Optional)_ + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.botplayer.teleport.md b/sdk/docs/server.botplayer.teleport.md new file mode 100644 index 00000000..8c419296 --- /dev/null +++ b/sdk/docs/server.botplayer.teleport.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [server](./server.md) > [BotPlayer](./server.botplayer.md) > [teleport](./server.botplayer.teleport.md) + +## BotPlayer.teleport() method + +**Signature:** + +```typescript +teleport(position: Vector3Like): void; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +position + + + + +[Vector3Like](./server.vector3like.md) + + + + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.botplayer.world.md b/sdk/docs/server.botplayer.world.md new file mode 100644 index 00000000..0815debd --- /dev/null +++ b/sdk/docs/server.botplayer.world.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [BotPlayer](./server.botplayer.md) > [world](./server.botplayer.world.md) + +## BotPlayer.world property + +**Signature:** + +```typescript +get world(): World; +``` diff --git a/sdk/docs/server.botplayeroptions.behavior.md b/sdk/docs/server.botplayeroptions.behavior.md new file mode 100644 index 00000000..0ec00a7c --- /dev/null +++ b/sdk/docs/server.botplayeroptions.behavior.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [BotPlayerOptions](./server.botplayeroptions.md) > [behavior](./server.botplayeroptions.behavior.md) + +## BotPlayerOptions.behavior property + +**Signature:** + +```typescript +behavior?: BotBehavior; +``` diff --git a/sdk/docs/server.botplayeroptions.md b/sdk/docs/server.botplayeroptions.md new file mode 100644 index 00000000..5b3d9c35 --- /dev/null +++ b/sdk/docs/server.botplayeroptions.md @@ -0,0 +1,150 @@ + + +[Home](./index.md) > [server](./server.md) > [BotPlayerOptions](./server.botplayeroptions.md) + +## BotPlayerOptions interface + +**Signature:** + +```typescript +export interface BotPlayerOptions +``` + +## Properties + + + + + + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[behavior?](./server.botplayeroptions.behavior.md) + + + + + + + +[BotBehavior](./server.botbehavior.md) + + + + +_(Optional)_ + + +
+ +[modelScale?](./server.botplayeroptions.modelscale.md) + + + + + + + +number + + + + +_(Optional)_ + + +
+ +[modelUri?](./server.botplayeroptions.modeluri.md) + + + + + + + +string + + + + +_(Optional)_ + + +
+ +[name?](./server.botplayeroptions.name.md) + + + + + + + +string + + + + +_(Optional)_ + + +
+ +[rigidBodyType?](./server.botplayeroptions.rigidbodytype.md) + + + + + + + +[RigidBodyType](./server.rigidbodytype.md) + + + + +_(Optional)_ + + +
+ +[spawnPosition?](./server.botplayeroptions.spawnposition.md) + + + + + + + +[Vector3Like](./server.vector3like.md) + + + + +_(Optional)_ + + +
diff --git a/sdk/docs/server.botplayeroptions.modelscale.md b/sdk/docs/server.botplayeroptions.modelscale.md new file mode 100644 index 00000000..6bd69bf0 --- /dev/null +++ b/sdk/docs/server.botplayeroptions.modelscale.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [BotPlayerOptions](./server.botplayeroptions.md) > [modelScale](./server.botplayeroptions.modelscale.md) + +## BotPlayerOptions.modelScale property + +**Signature:** + +```typescript +modelScale?: number; +``` diff --git a/sdk/docs/server.botplayeroptions.modeluri.md b/sdk/docs/server.botplayeroptions.modeluri.md new file mode 100644 index 00000000..d306c56c --- /dev/null +++ b/sdk/docs/server.botplayeroptions.modeluri.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [BotPlayerOptions](./server.botplayeroptions.md) > [modelUri](./server.botplayeroptions.modeluri.md) + +## BotPlayerOptions.modelUri property + +**Signature:** + +```typescript +modelUri?: string; +``` diff --git a/sdk/docs/server.botplayeroptions.name.md b/sdk/docs/server.botplayeroptions.name.md new file mode 100644 index 00000000..0149b34a --- /dev/null +++ b/sdk/docs/server.botplayeroptions.name.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [BotPlayerOptions](./server.botplayeroptions.md) > [name](./server.botplayeroptions.name.md) + +## BotPlayerOptions.name property + +**Signature:** + +```typescript +name?: string; +``` diff --git a/sdk/docs/server.botplayeroptions.rigidbodytype.md b/sdk/docs/server.botplayeroptions.rigidbodytype.md new file mode 100644 index 00000000..ed8c6a92 --- /dev/null +++ b/sdk/docs/server.botplayeroptions.rigidbodytype.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [BotPlayerOptions](./server.botplayeroptions.md) > [rigidBodyType](./server.botplayeroptions.rigidbodytype.md) + +## BotPlayerOptions.rigidBodyType property + +**Signature:** + +```typescript +rigidBodyType?: RigidBodyType; +``` diff --git a/sdk/docs/server.botplayeroptions.spawnposition.md b/sdk/docs/server.botplayeroptions.spawnposition.md new file mode 100644 index 00000000..17035cb7 --- /dev/null +++ b/sdk/docs/server.botplayeroptions.spawnposition.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [BotPlayerOptions](./server.botplayeroptions.md) > [spawnPosition](./server.botplayeroptions.spawnposition.md) + +## BotPlayerOptions.spawnPosition property + +**Signature:** + +```typescript +spawnPosition?: Vector3Like; +``` diff --git a/sdk/docs/server.chasebehavior._constructor_.md b/sdk/docs/server.chasebehavior._constructor_.md new file mode 100644 index 00000000..b86a1087 --- /dev/null +++ b/sdk/docs/server.chasebehavior._constructor_.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [server](./server.md) > [ChaseBehavior](./server.chasebehavior.md) > [(constructor)](./server.chasebehavior._constructor_.md) + +## ChaseBehavior.(constructor) + +Constructs a new instance of the `ChaseBehavior` class + +**Signature:** + +```typescript +constructor(options?: ChaseBehaviorOptions); +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +options + + + + +[ChaseBehaviorOptions](./server.chasebehavioroptions.md) + + + + +_(Optional)_ + + +
diff --git a/sdk/docs/server.chasebehavior.md b/sdk/docs/server.chasebehavior.md new file mode 100644 index 00000000..1ffd6994 --- /dev/null +++ b/sdk/docs/server.chasebehavior.md @@ -0,0 +1,122 @@ + + +[Home](./index.md) > [server](./server.md) > [ChaseBehavior](./server.chasebehavior.md) + +## ChaseBehavior class + +**Signature:** + +```typescript +export default class ChaseBehavior implements BotBehavior +``` +**Implements:** [BotBehavior](./server.botbehavior.md) + +## Constructors + + + +
+ +Constructor + + + + +Modifiers + + + + +Description + + +
+ +[(constructor)(options)](./server.chasebehavior._constructor_.md) + + + + + + + +Constructs a new instance of the `ChaseBehavior` class + + +
+ +## Properties + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[name](./server.chasebehavior.name.md) + + + + +`readonly` + + + + +(not declared) + + + + + +
+ +## Methods + + + +
+ +Method + + + + +Modifiers + + + + +Description + + +
+ +[tick(bot, world, deltaTimeMs)](./server.chasebehavior.tick.md) + + + + + + + + +
diff --git a/sdk/docs/server.chasebehavior.name.md b/sdk/docs/server.chasebehavior.name.md new file mode 100644 index 00000000..6862de69 --- /dev/null +++ b/sdk/docs/server.chasebehavior.name.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [ChaseBehavior](./server.chasebehavior.md) > [name](./server.chasebehavior.name.md) + +## ChaseBehavior.name property + +**Signature:** + +```typescript +readonly name = "chase"; +``` diff --git a/sdk/docs/server.chasebehavior.tick.md b/sdk/docs/server.chasebehavior.tick.md new file mode 100644 index 00000000..82dd17f8 --- /dev/null +++ b/sdk/docs/server.chasebehavior.tick.md @@ -0,0 +1,77 @@ + + +[Home](./index.md) > [server](./server.md) > [ChaseBehavior](./server.chasebehavior.md) > [tick](./server.chasebehavior.tick.md) + +## ChaseBehavior.tick() method + +**Signature:** + +```typescript +tick(bot: BotPlayer, world: World, deltaTimeMs: number): void; +``` + +## Parameters + + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +bot + + + + +[BotPlayer](./server.botplayer.md) + + + + + +
+ +world + + + + +[World](./server.world.md) + + + + + +
+ +deltaTimeMs + + + + +number + + + + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.chasebehavioroptions.chasespeed.md b/sdk/docs/server.chasebehavioroptions.chasespeed.md new file mode 100644 index 00000000..3f02f47b --- /dev/null +++ b/sdk/docs/server.chasebehavioroptions.chasespeed.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [ChaseBehaviorOptions](./server.chasebehavioroptions.md) > [chaseSpeed](./server.chasebehavioroptions.chasespeed.md) + +## ChaseBehaviorOptions.chaseSpeed property + +**Signature:** + +```typescript +chaseSpeed?: number; +``` diff --git a/sdk/docs/server.chasebehavioroptions.detectionradius.md b/sdk/docs/server.chasebehavioroptions.detectionradius.md new file mode 100644 index 00000000..0cf60357 --- /dev/null +++ b/sdk/docs/server.chasebehavioroptions.detectionradius.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [ChaseBehaviorOptions](./server.chasebehavioroptions.md) > [detectionRadius](./server.chasebehavioroptions.detectionradius.md) + +## ChaseBehaviorOptions.detectionRadius property + +**Signature:** + +```typescript +detectionRadius?: number; +``` diff --git a/sdk/docs/server.chasebehavioroptions.md b/sdk/docs/server.chasebehavioroptions.md new file mode 100644 index 00000000..49ce9abc --- /dev/null +++ b/sdk/docs/server.chasebehavioroptions.md @@ -0,0 +1,93 @@ + + +[Home](./index.md) > [server](./server.md) > [ChaseBehaviorOptions](./server.chasebehavioroptions.md) + +## ChaseBehaviorOptions interface + +**Signature:** + +```typescript +export interface ChaseBehaviorOptions +``` + +## Properties + + + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[chaseSpeed?](./server.chasebehavioroptions.chasespeed.md) + + + + + + + +number + + + + +_(Optional)_ + + +
+ +[detectionRadius?](./server.chasebehavioroptions.detectionradius.md) + + + + + + + +number + + + + +_(Optional)_ + + +
+ +[updateIntervalMs?](./server.chasebehavioroptions.updateintervalms.md) + + + + + + + +number + + + + +_(Optional)_ + + +
diff --git a/sdk/docs/server.chasebehavioroptions.updateintervalms.md b/sdk/docs/server.chasebehavioroptions.updateintervalms.md new file mode 100644 index 00000000..4f810e56 --- /dev/null +++ b/sdk/docs/server.chasebehavioroptions.updateintervalms.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [ChaseBehaviorOptions](./server.chasebehavioroptions.md) > [updateIntervalMs](./server.chasebehavioroptions.updateintervalms.md) + +## ChaseBehaviorOptions.updateIntervalMs property + +**Signature:** + +```typescript +updateIntervalMs?: number; +``` diff --git a/sdk/docs/server.compressedworldmap.algorithm.md b/sdk/docs/server.compressedworldmap.algorithm.md new file mode 100644 index 00000000..f9053090 --- /dev/null +++ b/sdk/docs/server.compressedworldmap.algorithm.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [CompressedWorldMap](./server.compressedworldmap.md) > [algorithm](./server.compressedworldmap.algorithm.md) + +## CompressedWorldMap.algorithm property + +**Signature:** + +```typescript +algorithm?: CompressedWorldMapAlgorithm; +``` diff --git a/sdk/docs/server.compressedworldmap.blocktypes.md b/sdk/docs/server.compressedworldmap.blocktypes.md new file mode 100644 index 00000000..714e1614 --- /dev/null +++ b/sdk/docs/server.compressedworldmap.blocktypes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [CompressedWorldMap](./server.compressedworldmap.md) > [blockTypes](./server.compressedworldmap.blocktypes.md) + +## CompressedWorldMap.blockTypes property + +**Signature:** + +```typescript +blockTypes?: BlockTypeOptions[] | Record; +``` diff --git a/sdk/docs/server.compressedworldmap.bounds.md b/sdk/docs/server.compressedworldmap.bounds.md new file mode 100644 index 00000000..7742476c --- /dev/null +++ b/sdk/docs/server.compressedworldmap.bounds.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [CompressedWorldMap](./server.compressedworldmap.md) > [bounds](./server.compressedworldmap.bounds.md) + +## CompressedWorldMap.bounds property + +**Signature:** + +```typescript +bounds: CompressedWorldMapBounds; +``` diff --git a/sdk/docs/server.compressedworldmap.codecversion.md b/sdk/docs/server.compressedworldmap.codecversion.md new file mode 100644 index 00000000..79fb5233 --- /dev/null +++ b/sdk/docs/server.compressedworldmap.codecversion.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [CompressedWorldMap](./server.compressedworldmap.md) > [codecVersion](./server.compressedworldmap.codecversion.md) + +## CompressedWorldMap.codecVersion property + +**Signature:** + +```typescript +codecVersion?: number; +``` diff --git a/sdk/docs/server.compressedworldmap.data.md b/sdk/docs/server.compressedworldmap.data.md new file mode 100644 index 00000000..b61b393d --- /dev/null +++ b/sdk/docs/server.compressedworldmap.data.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [CompressedWorldMap](./server.compressedworldmap.md) > [data](./server.compressedworldmap.data.md) + +## CompressedWorldMap.data property + +**Signature:** + +```typescript +data: string; +``` diff --git a/sdk/docs/server.compressedworldmap.entities.md b/sdk/docs/server.compressedworldmap.entities.md new file mode 100644 index 00000000..30be0137 --- /dev/null +++ b/sdk/docs/server.compressedworldmap.entities.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [CompressedWorldMap](./server.compressedworldmap.md) > [entities](./server.compressedworldmap.entities.md) + +## CompressedWorldMap.entities property + +**Signature:** + +```typescript +entities?: WorldMap['entities']; +``` diff --git a/sdk/docs/server.compressedworldmap.format.md b/sdk/docs/server.compressedworldmap.format.md new file mode 100644 index 00000000..58f07e26 --- /dev/null +++ b/sdk/docs/server.compressedworldmap.format.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [CompressedWorldMap](./server.compressedworldmap.md) > [format](./server.compressedworldmap.format.md) + +## CompressedWorldMap.format property + +**Signature:** + +```typescript +format?: 'hytopia.worldmap.compressed'; +``` diff --git a/sdk/docs/server.compressedworldmap.mapversion.md b/sdk/docs/server.compressedworldmap.mapversion.md new file mode 100644 index 00000000..e9bffd89 --- /dev/null +++ b/sdk/docs/server.compressedworldmap.mapversion.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [CompressedWorldMap](./server.compressedworldmap.md) > [mapVersion](./server.compressedworldmap.mapversion.md) + +## CompressedWorldMap.mapVersion property + +**Signature:** + +```typescript +mapVersion?: unknown; +``` diff --git a/sdk/docs/server.compressedworldmap.md b/sdk/docs/server.compressedworldmap.md new file mode 100644 index 00000000..e2528f80 --- /dev/null +++ b/sdk/docs/server.compressedworldmap.md @@ -0,0 +1,241 @@ + + +[Home](./index.md) > [server](./server.md) > [CompressedWorldMap](./server.compressedworldmap.md) + +## CompressedWorldMap interface + +**Signature:** + +```typescript +export interface CompressedWorldMap +``` + +## Properties + + + + + + + + + + + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[algorithm?](./server.compressedworldmap.algorithm.md) + + + + + + + +[CompressedWorldMapAlgorithm](./server.compressedworldmapalgorithm.md) + + + + +_(Optional)_ + + +
+ +[blockTypes?](./server.compressedworldmap.blocktypes.md) + + + + + + + +[BlockTypeOptions](./server.blocktypeoptions.md)\[\] \| Record<string, [BlockTypeOptions](./server.blocktypeoptions.md)> + + + + +_(Optional)_ + + +
+ +[bounds](./server.compressedworldmap.bounds.md) + + + + + + + +CompressedWorldMapBounds + + + + + +
+ +[codecVersion?](./server.compressedworldmap.codecversion.md) + + + + + + + +number + + + + +_(Optional)_ + + +
+ +[data](./server.compressedworldmap.data.md) + + + + + + + +string + + + + + +
+ +[entities?](./server.compressedworldmap.entities.md) + + + + + + + +[WorldMap](./server.worldmap.md)\['entities'\] + + + + +_(Optional)_ + + +
+ +[format?](./server.compressedworldmap.format.md) + + + + + + + +'hytopia.worldmap.compressed' + + + + +_(Optional)_ + + +
+ +[mapVersion?](./server.compressedworldmap.mapversion.md) + + + + + + + +unknown + + + + +_(Optional)_ + + +
+ +[metadata?](./server.compressedworldmap.metadata.md) + + + + + + + +unknown + + + + +_(Optional)_ + + +
+ +[options?](./server.compressedworldmap.options.md) + + + + + + + +CompressedWorldMapOptions + + + + +_(Optional)_ + + +
+ +[version?](./server.compressedworldmap.version.md) + + + + + + + +string + + + + +_(Optional)_ + + +
diff --git a/sdk/docs/server.compressedworldmap.metadata.md b/sdk/docs/server.compressedworldmap.metadata.md new file mode 100644 index 00000000..0a7500a1 --- /dev/null +++ b/sdk/docs/server.compressedworldmap.metadata.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [CompressedWorldMap](./server.compressedworldmap.md) > [metadata](./server.compressedworldmap.metadata.md) + +## CompressedWorldMap.metadata property + +**Signature:** + +```typescript +metadata?: unknown; +``` diff --git a/sdk/docs/server.compressedworldmap.options.md b/sdk/docs/server.compressedworldmap.options.md new file mode 100644 index 00000000..c498ee92 --- /dev/null +++ b/sdk/docs/server.compressedworldmap.options.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [CompressedWorldMap](./server.compressedworldmap.md) > [options](./server.compressedworldmap.options.md) + +## CompressedWorldMap.options property + +**Signature:** + +```typescript +options?: CompressedWorldMapOptions; +``` diff --git a/sdk/docs/server.compressedworldmap.version.md b/sdk/docs/server.compressedworldmap.version.md new file mode 100644 index 00000000..7a156434 --- /dev/null +++ b/sdk/docs/server.compressedworldmap.version.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [CompressedWorldMap](./server.compressedworldmap.md) > [version](./server.compressedworldmap.version.md) + +## CompressedWorldMap.version property + +**Signature:** + +```typescript +version?: string; +``` diff --git a/sdk/docs/server.compressedworldmapalgorithm.md b/sdk/docs/server.compressedworldmapalgorithm.md new file mode 100644 index 00000000..45f64dce --- /dev/null +++ b/sdk/docs/server.compressedworldmapalgorithm.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [CompressedWorldMapAlgorithm](./server.compressedworldmapalgorithm.md) + +## CompressedWorldMapAlgorithm type + +**Signature:** + +```typescript +export type CompressedWorldMapAlgorithm = 'brotli' | 'gzip' | 'none'; +``` diff --git a/sdk/docs/server.compressworldmapoptions.algorithm.md b/sdk/docs/server.compressworldmapoptions.algorithm.md new file mode 100644 index 00000000..894dae9b --- /dev/null +++ b/sdk/docs/server.compressworldmapoptions.algorithm.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [CompressWorldMapOptions](./server.compressworldmapoptions.md) > [algorithm](./server.compressworldmapoptions.algorithm.md) + +## CompressWorldMapOptions.algorithm property + +**Signature:** + +```typescript +algorithm?: CompressedWorldMapAlgorithm; +``` diff --git a/sdk/docs/server.compressworldmapoptions.includerotations.md b/sdk/docs/server.compressworldmapoptions.includerotations.md new file mode 100644 index 00000000..c7a1236b --- /dev/null +++ b/sdk/docs/server.compressworldmapoptions.includerotations.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [CompressWorldMapOptions](./server.compressworldmapoptions.md) > [includeRotations](./server.compressworldmapoptions.includerotations.md) + +## CompressWorldMapOptions.includeRotations property + +**Signature:** + +```typescript +includeRotations?: boolean; +``` diff --git a/sdk/docs/server.compressworldmapoptions.level.md b/sdk/docs/server.compressworldmapoptions.level.md new file mode 100644 index 00000000..2aba2619 --- /dev/null +++ b/sdk/docs/server.compressworldmapoptions.level.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [CompressWorldMapOptions](./server.compressworldmapoptions.md) > [level](./server.compressworldmapoptions.level.md) + +## CompressWorldMapOptions.level property + +**Signature:** + +```typescript +level?: number; +``` diff --git a/sdk/docs/server.compressworldmapoptions.md b/sdk/docs/server.compressworldmapoptions.md new file mode 100644 index 00000000..489258ad --- /dev/null +++ b/sdk/docs/server.compressworldmapoptions.md @@ -0,0 +1,93 @@ + + +[Home](./index.md) > [server](./server.md) > [CompressWorldMapOptions](./server.compressworldmapoptions.md) + +## CompressWorldMapOptions interface + +**Signature:** + +```typescript +export interface CompressWorldMapOptions +``` + +## Properties + + + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[algorithm?](./server.compressworldmapoptions.algorithm.md) + + + + + + + +[CompressedWorldMapAlgorithm](./server.compressedworldmapalgorithm.md) + + + + +_(Optional)_ + + +
+ +[includeRotations?](./server.compressworldmapoptions.includerotations.md) + + + + + + + +boolean + + + + +_(Optional)_ + + +
+ +[level?](./server.compressworldmapoptions.level.md) + + + + + + + +number + + + + +_(Optional)_ + + +
diff --git a/sdk/docs/server.cpuprofiler.captureheapsnapshot.md b/sdk/docs/server.cpuprofiler.captureheapsnapshot.md new file mode 100644 index 00000000..d0480cbb --- /dev/null +++ b/sdk/docs/server.cpuprofiler.captureheapsnapshot.md @@ -0,0 +1,51 @@ + + +[Home](./index.md) > [server](./server.md) > [CpuProfiler](./server.cpuprofiler.md) > [captureHeapSnapshot](./server.cpuprofiler.captureheapsnapshot.md) + +## CpuProfiler.captureHeapSnapshot() method + +**Signature:** + +```typescript +static captureHeapSnapshot(outputPath?: string): Promise; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +outputPath + + + + +string + + + + +_(Optional)_ + + +
+**Returns:** + +Promise<string> + diff --git a/sdk/docs/server.cpuprofiler.captureprofile.md b/sdk/docs/server.cpuprofiler.captureprofile.md new file mode 100644 index 00000000..f4e09ab7 --- /dev/null +++ b/sdk/docs/server.cpuprofiler.captureprofile.md @@ -0,0 +1,65 @@ + + +[Home](./index.md) > [server](./server.md) > [CpuProfiler](./server.cpuprofiler.md) > [captureProfile](./server.cpuprofiler.captureprofile.md) + +## CpuProfiler.captureProfile() method + +**Signature:** + +```typescript +static captureProfile(durationMs: number, outputPath?: string): Promise; +``` + +## Parameters + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +durationMs + + + + +number + + + + + +
+ +outputPath + + + + +string + + + + +_(Optional)_ + + +
+**Returns:** + +Promise<object \| null> + diff --git a/sdk/docs/server.cpuprofiler.md b/sdk/docs/server.cpuprofiler.md new file mode 100644 index 00000000..235371a7 --- /dev/null +++ b/sdk/docs/server.cpuprofiler.md @@ -0,0 +1,59 @@ + + +[Home](./index.md) > [server](./server.md) > [CpuProfiler](./server.cpuprofiler.md) + +## CpuProfiler class + +**Signature:** + +```typescript +export default class CpuProfiler +``` + +## Methods + + + + +
+ +Method + + + + +Modifiers + + + + +Description + + +
+ +[captureHeapSnapshot(outputPath)](./server.cpuprofiler.captureheapsnapshot.md) + + + + +`static` + + + + + +
+ +[captureProfile(durationMs, outputPath)](./server.cpuprofiler.captureprofile.md) + + + + +`static` + + + + + +
diff --git a/sdk/docs/server.createworldmapchunkcacheoptions.algorithm.md b/sdk/docs/server.createworldmapchunkcacheoptions.algorithm.md new file mode 100644 index 00000000..7c66ab09 --- /dev/null +++ b/sdk/docs/server.createworldmapchunkcacheoptions.algorithm.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [CreateWorldMapChunkCacheOptions](./server.createworldmapchunkcacheoptions.md) > [algorithm](./server.createworldmapchunkcacheoptions.algorithm.md) + +## CreateWorldMapChunkCacheOptions.algorithm property + +**Signature:** + +```typescript +algorithm?: WorldMapChunkCacheAlgorithm; +``` diff --git a/sdk/docs/server.createworldmapchunkcacheoptions.includerotations.md b/sdk/docs/server.createworldmapchunkcacheoptions.includerotations.md new file mode 100644 index 00000000..8190e0b7 --- /dev/null +++ b/sdk/docs/server.createworldmapchunkcacheoptions.includerotations.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [CreateWorldMapChunkCacheOptions](./server.createworldmapchunkcacheoptions.md) > [includeRotations](./server.createworldmapchunkcacheoptions.includerotations.md) + +## CreateWorldMapChunkCacheOptions.includeRotations property + +**Signature:** + +```typescript +includeRotations?: boolean; +``` diff --git a/sdk/docs/server.createworldmapchunkcacheoptions.level.md b/sdk/docs/server.createworldmapchunkcacheoptions.level.md new file mode 100644 index 00000000..a0cf9b43 --- /dev/null +++ b/sdk/docs/server.createworldmapchunkcacheoptions.level.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [CreateWorldMapChunkCacheOptions](./server.createworldmapchunkcacheoptions.md) > [level](./server.createworldmapchunkcacheoptions.level.md) + +## CreateWorldMapChunkCacheOptions.level property + +**Signature:** + +```typescript +level?: number; +``` diff --git a/sdk/docs/server.createworldmapchunkcacheoptions.md b/sdk/docs/server.createworldmapchunkcacheoptions.md new file mode 100644 index 00000000..133898d2 --- /dev/null +++ b/sdk/docs/server.createworldmapchunkcacheoptions.md @@ -0,0 +1,112 @@ + + +[Home](./index.md) > [server](./server.md) > [CreateWorldMapChunkCacheOptions](./server.createworldmapchunkcacheoptions.md) + +## CreateWorldMapChunkCacheOptions interface + +**Signature:** + +```typescript +export interface CreateWorldMapChunkCacheOptions +``` + +## Properties + + + + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[algorithm?](./server.createworldmapchunkcacheoptions.algorithm.md) + + + + + + + +[WorldMapChunkCacheAlgorithm](./server.worldmapchunkcachealgorithm.md) + + + + +_(Optional)_ + + +
+ +[includeRotations?](./server.createworldmapchunkcacheoptions.includerotations.md) + + + + + + + +boolean + + + + +_(Optional)_ + + +
+ +[level?](./server.createworldmapchunkcacheoptions.level.md) + + + + + + + +number + + + + +_(Optional)_ + + +
+ +[sourceSha256?](./server.createworldmapchunkcacheoptions.sourcesha256.md) + + + + + + + +string + + + + +_(Optional)_ + + +
diff --git a/sdk/docs/server.createworldmapchunkcacheoptions.sourcesha256.md b/sdk/docs/server.createworldmapchunkcacheoptions.sourcesha256.md new file mode 100644 index 00000000..f54f9a0d --- /dev/null +++ b/sdk/docs/server.createworldmapchunkcacheoptions.sourcesha256.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [CreateWorldMapChunkCacheOptions](./server.createworldmapchunkcacheoptions.md) > [sourceSha256](./server.createworldmapchunkcacheoptions.sourcesha256.md) + +## CreateWorldMapChunkCacheOptions.sourceSha256 property + +**Signature:** + +```typescript +sourceSha256?: string; +``` diff --git a/sdk/docs/server.entity.md b/sdk/docs/server.entity.md index 34eec291..54876653 100644 --- a/sdk/docs/server.entity.md +++ b/sdk/docs/server.entity.md @@ -1005,6 +1005,48 @@ Use for: glow effects or highlighted states. Sets the emissive intensity of the entity. + + + +[setModelAnimationsPlaybackRate(playbackRate)](./server.entity.setmodelanimationsplaybackrate.md) + + + + + + + +Sets the playback rate for all of the entity's model animations. + + + + + +[setModelNodeEmissiveColor(nodeName, color)](./server.entity.setmodelnodeemissivecolor.md) + + + + + + + +Sets the emissive color for a model node by name. + + + + + +[setModelNodeEmissiveIntensity(nodeName, intensity)](./server.entity.setmodelnodeemissiveintensity.md) + + + + + + + +Sets the emissive intensity for a model node by name. + + @@ -1147,6 +1189,34 @@ Spawns the entity in the world. Use for: placing the entity into a world so it simulates and syncs to clients. Do NOT use for: reusing a single entity instance across multiple worlds. + + + +[startModelLoopedAnimations(names)](./server.entity.startmodelloopedanimations.md) + + + + + + + +Starts looped animations by name on this entity's model. + + + + + +[startModelOneshotAnimations(names)](./server.entity.startmodeloneshotanimations.md) + + + + + + + +Starts one-shot animations by name on this entity's model. + + diff --git a/sdk/docs/server.entity.setmodelanimationsplaybackrate.md b/sdk/docs/server.entity.setmodelanimationsplaybackrate.md new file mode 100644 index 00000000..fcd8997a --- /dev/null +++ b/sdk/docs/server.entity.setmodelanimationsplaybackrate.md @@ -0,0 +1,59 @@ + + +[Home](./index.md) > [server](./server.md) > [Entity](./server.entity.md) > [setModelAnimationsPlaybackRate](./server.entity.setmodelanimationsplaybackrate.md) + +## Entity.setModelAnimationsPlaybackRate() method + +Sets the playback rate for all of the entity's model animations. + +**Signature:** + +```typescript +setModelAnimationsPlaybackRate(playbackRate: number): void; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +playbackRate + + + + +number + + + + +The playback rate of the entity's model animations. + +\*\*Category:\*\* Entities + + +
+**Returns:** + +void + +## Remarks + +A value of 1 is normal speed, 0.5 is half speed, 2 is double speed. A negative value will play the animation in reverse. + diff --git a/sdk/docs/server.entity.setmodelnodeemissivecolor.md b/sdk/docs/server.entity.setmodelnodeemissivecolor.md new file mode 100644 index 00000000..1fa37d6c --- /dev/null +++ b/sdk/docs/server.entity.setmodelnodeemissivecolor.md @@ -0,0 +1,71 @@ + + +[Home](./index.md) > [server](./server.md) > [Entity](./server.entity.md) > [setModelNodeEmissiveColor](./server.entity.setmodelnodeemissivecolor.md) + +## Entity.setModelNodeEmissiveColor() method + +Sets the emissive color for a model node by name. + +**Signature:** + +```typescript +setModelNodeEmissiveColor(nodeName: string, color: RgbColor | undefined): void; +``` + +## Parameters + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +nodeName + + + + +string + + + + +The node name to target. + + +
+ +color + + + + +[RgbColor](./server.rgbcolor.md) \| undefined + + + + +The RGB color to set, or undefined to clear. + +\*\*Category:\*\* Entities + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.entity.setmodelnodeemissiveintensity.md b/sdk/docs/server.entity.setmodelnodeemissiveintensity.md new file mode 100644 index 00000000..0433a3b1 --- /dev/null +++ b/sdk/docs/server.entity.setmodelnodeemissiveintensity.md @@ -0,0 +1,71 @@ + + +[Home](./index.md) > [server](./server.md) > [Entity](./server.entity.md) > [setModelNodeEmissiveIntensity](./server.entity.setmodelnodeemissiveintensity.md) + +## Entity.setModelNodeEmissiveIntensity() method + +Sets the emissive intensity for a model node by name. + +**Signature:** + +```typescript +setModelNodeEmissiveIntensity(nodeName: string, intensity: number | undefined): void; +``` + +## Parameters + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +nodeName + + + + +string + + + + +The node name to target. + + +
+ +intensity + + + + +number \| undefined + + + + +The intensity value to set, or undefined to clear. + +\*\*Category:\*\* Entities + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.entity.startmodelloopedanimations.md b/sdk/docs/server.entity.startmodelloopedanimations.md new file mode 100644 index 00000000..2a901147 --- /dev/null +++ b/sdk/docs/server.entity.startmodelloopedanimations.md @@ -0,0 +1,55 @@ + + +[Home](./index.md) > [server](./server.md) > [Entity](./server.entity.md) > [startModelLoopedAnimations](./server.entity.startmodelloopedanimations.md) + +## Entity.startModelLoopedAnimations() method + +Starts looped animations by name on this entity's model. + +**Signature:** + +```typescript +startModelLoopedAnimations(names: readonly string[]): void; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +names + + + + +readonly string\[\] + + + + +Animation names to start looping. + +\*\*Category:\*\* Entities + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.entity.startmodeloneshotanimations.md b/sdk/docs/server.entity.startmodeloneshotanimations.md new file mode 100644 index 00000000..47cce0b6 --- /dev/null +++ b/sdk/docs/server.entity.startmodeloneshotanimations.md @@ -0,0 +1,55 @@ + + +[Home](./index.md) > [server](./server.md) > [Entity](./server.entity.md) > [startModelOneshotAnimations](./server.entity.startmodeloneshotanimations.md) + +## Entity.startModelOneshotAnimations() method + +Starts one-shot animations by name on this entity's model. + +**Signature:** + +```typescript +startModelOneshotAnimations(names: readonly string[]): void; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +names + + + + +readonly string\[\] + + + + +Animation names to play once. + +\*\*Category:\*\* Entities + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.idlebehavior.md b/sdk/docs/server.idlebehavior.md new file mode 100644 index 00000000..a5ce6597 --- /dev/null +++ b/sdk/docs/server.idlebehavior.md @@ -0,0 +1,88 @@ + + +[Home](./index.md) > [server](./server.md) > [IdleBehavior](./server.idlebehavior.md) + +## IdleBehavior class + +**Signature:** + +```typescript +export default class IdleBehavior implements BotBehavior +``` +**Implements:** [BotBehavior](./server.botbehavior.md) + +## Properties + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[name](./server.idlebehavior.name.md) + + + + +`readonly` + + + + +(not declared) + + + + + +
+ +## Methods + + + +
+ +Method + + + + +Modifiers + + + + +Description + + +
+ +[tick(\_bot, \_world, \_deltaTimeMs)](./server.idlebehavior.tick.md) + + + + + + + + +
diff --git a/sdk/docs/server.idlebehavior.name.md b/sdk/docs/server.idlebehavior.name.md new file mode 100644 index 00000000..9070c64e --- /dev/null +++ b/sdk/docs/server.idlebehavior.name.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [IdleBehavior](./server.idlebehavior.md) > [name](./server.idlebehavior.name.md) + +## IdleBehavior.name property + +**Signature:** + +```typescript +readonly name = "idle"; +``` diff --git a/sdk/docs/server.idlebehavior.tick.md b/sdk/docs/server.idlebehavior.tick.md new file mode 100644 index 00000000..5f324bf9 --- /dev/null +++ b/sdk/docs/server.idlebehavior.tick.md @@ -0,0 +1,77 @@ + + +[Home](./index.md) > [server](./server.md) > [IdleBehavior](./server.idlebehavior.md) > [tick](./server.idlebehavior.tick.md) + +## IdleBehavior.tick() method + +**Signature:** + +```typescript +tick(_bot: BotPlayer, _world: World, _deltaTimeMs: number): void; +``` + +## Parameters + + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +\_bot + + + + +[BotPlayer](./server.botplayer.md) + + + + + +
+ +\_world + + + + +[World](./server.world.md) + + + + + +
+ +\_deltaTimeMs + + + + +number + + + + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.interactbehavior._constructor_.md b/sdk/docs/server.interactbehavior._constructor_.md new file mode 100644 index 00000000..9ae66abc --- /dev/null +++ b/sdk/docs/server.interactbehavior._constructor_.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [server](./server.md) > [InteractBehavior](./server.interactbehavior.md) > [(constructor)](./server.interactbehavior._constructor_.md) + +## InteractBehavior.(constructor) + +Constructs a new instance of the `InteractBehavior` class + +**Signature:** + +```typescript +constructor(options?: InteractBehaviorOptions); +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +options + + + + +[InteractBehaviorOptions](./server.interactbehavioroptions.md) + + + + +_(Optional)_ + + +
diff --git a/sdk/docs/server.interactbehavior.md b/sdk/docs/server.interactbehavior.md new file mode 100644 index 00000000..2c0f7e93 --- /dev/null +++ b/sdk/docs/server.interactbehavior.md @@ -0,0 +1,122 @@ + + +[Home](./index.md) > [server](./server.md) > [InteractBehavior](./server.interactbehavior.md) + +## InteractBehavior class + +**Signature:** + +```typescript +export default class InteractBehavior implements BotBehavior +``` +**Implements:** [BotBehavior](./server.botbehavior.md) + +## Constructors + + + +
+ +Constructor + + + + +Modifiers + + + + +Description + + +
+ +[(constructor)(options)](./server.interactbehavior._constructor_.md) + + + + + + + +Constructs a new instance of the `InteractBehavior` class + + +
+ +## Properties + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[name](./server.interactbehavior.name.md) + + + + +`readonly` + + + + +(not declared) + + + + + +
+ +## Methods + + + +
+ +Method + + + + +Modifiers + + + + +Description + + +
+ +[tick(bot, world, deltaTimeMs)](./server.interactbehavior.tick.md) + + + + + + + + +
diff --git a/sdk/docs/server.interactbehavior.name.md b/sdk/docs/server.interactbehavior.name.md new file mode 100644 index 00000000..6d29dab9 --- /dev/null +++ b/sdk/docs/server.interactbehavior.name.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [InteractBehavior](./server.interactbehavior.md) > [name](./server.interactbehavior.name.md) + +## InteractBehavior.name property + +**Signature:** + +```typescript +readonly name = "interact"; +``` diff --git a/sdk/docs/server.interactbehavior.tick.md b/sdk/docs/server.interactbehavior.tick.md new file mode 100644 index 00000000..d4b3e0e8 --- /dev/null +++ b/sdk/docs/server.interactbehavior.tick.md @@ -0,0 +1,77 @@ + + +[Home](./index.md) > [server](./server.md) > [InteractBehavior](./server.interactbehavior.md) > [tick](./server.interactbehavior.tick.md) + +## InteractBehavior.tick() method + +**Signature:** + +```typescript +tick(bot: BotPlayer, world: World, deltaTimeMs: number): void; +``` + +## Parameters + + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +bot + + + + +[BotPlayer](./server.botplayer.md) + + + + + +
+ +world + + + + +[World](./server.world.md) + + + + + +
+ +deltaTimeMs + + + + +number + + + + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.interactbehavioroptions.actionintervalms.md b/sdk/docs/server.interactbehavioroptions.actionintervalms.md new file mode 100644 index 00000000..243a28a8 --- /dev/null +++ b/sdk/docs/server.interactbehavioroptions.actionintervalms.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [InteractBehaviorOptions](./server.interactbehavioroptions.md) > [actionIntervalMs](./server.interactbehavioroptions.actionintervalms.md) + +## InteractBehaviorOptions.actionIntervalMs property + +**Signature:** + +```typescript +actionIntervalMs?: number; +``` diff --git a/sdk/docs/server.interactbehavioroptions.interactradius.md b/sdk/docs/server.interactbehavioroptions.interactradius.md new file mode 100644 index 00000000..10dcb997 --- /dev/null +++ b/sdk/docs/server.interactbehavioroptions.interactradius.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [InteractBehaviorOptions](./server.interactbehavioroptions.md) > [interactRadius](./server.interactbehavioroptions.interactradius.md) + +## InteractBehaviorOptions.interactRadius property + +**Signature:** + +```typescript +interactRadius?: number; +``` diff --git a/sdk/docs/server.interactbehavioroptions.md b/sdk/docs/server.interactbehavioroptions.md new file mode 100644 index 00000000..6f52c3a8 --- /dev/null +++ b/sdk/docs/server.interactbehavioroptions.md @@ -0,0 +1,93 @@ + + +[Home](./index.md) > [server](./server.md) > [InteractBehaviorOptions](./server.interactbehavioroptions.md) + +## InteractBehaviorOptions interface + +**Signature:** + +```typescript +export interface InteractBehaviorOptions +``` + +## Properties + + + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[actionIntervalMs?](./server.interactbehavioroptions.actionintervalms.md) + + + + + + + +number + + + + +_(Optional)_ + + +
+ +[interactRadius?](./server.interactbehavioroptions.interactradius.md) + + + + + + + +number + + + + +_(Optional)_ + + +
+ +[moveSpeed?](./server.interactbehavioroptions.movespeed.md) + + + + + + + +number + + + + +_(Optional)_ + + +
diff --git a/sdk/docs/server.interactbehavioroptions.movespeed.md b/sdk/docs/server.interactbehavioroptions.movespeed.md new file mode 100644 index 00000000..214819d0 --- /dev/null +++ b/sdk/docs/server.interactbehavioroptions.movespeed.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [InteractBehaviorOptions](./server.interactbehavioroptions.md) > [moveSpeed](./server.interactbehavioroptions.movespeed.md) + +## InteractBehaviorOptions.moveSpeed property + +**Signature:** + +```typescript +moveSpeed?: number; +``` diff --git a/sdk/docs/server.md b/sdk/docs/server.md index cc345102..318d3e23 100644 --- a/sdk/docs/server.md +++ b/sdk/docs/server.md @@ -105,6 +105,33 @@ Manages known block types in a world. When to use: registering and retrieving block types for a specific world. Do NOT use for: placing blocks; use `ChunkLattice.setBlock`. + + + +[BotManager](./server.botmanager.md) + + + + + + + + +[BotPlayer](./server.botplayer.md) + + + + + + + + +[ChaseBehavior](./server.chasebehavior.md) + + + + + @@ -170,6 +197,15 @@ A helper class for building and decoding collision groups. When to use: creating custom collision filters for colliders and rigid bodies. Do NOT use for: per-frame changes; collision group changes are usually infrequent. + + + +[CpuProfiler](./server.cpuprofiler.md) + + + + + @@ -287,6 +323,24 @@ Global entry point for server systems (players, worlds, assets). When to use: accessing global managers and registries after startup. Do NOT use for: constructing your own server instance. + + + +[IdleBehavior](./server.idlebehavior.md) + + + + + + + + +[InteractBehavior](./server.interactbehavior.md) + + + + + @@ -352,6 +406,15 @@ Manages model data for all known models of the game. When to use: querying model metadata (bounds, node names, animations, trimesh). Do NOT use for: runtime mesh editing; use dedicated tooling or physics colliders. + + + +[NetworkMetrics](./server.networkmetrics.md) + + + + + @@ -389,6 +452,15 @@ A pathfinding entity controller built on top of `SimpleEntityController` When to use: obstacle-aware movement to a target coordinate. Do NOT use for: per-tick recalculation; pathfinding is synchronous and can be expensive. + + + +[PerformanceMonitor](./server.performancemonitor.md) + + + + + @@ -480,6 +552,15 @@ Represents a quaternion. When to use: rotation math for entities, cameras, or transforms. Do NOT use for: immutable math; most methods mutate the instance. + + + +[RandomWalkBehavior](./server.randomwalkbehavior.md) + + + + + @@ -621,6 +702,42 @@ Manages all worlds in a game server. When to use: creating additional worlds, routing players, or querying the active world set. Do NOT use for: instantiating `World` directly for gameplay; use `WorldManager.createWorld` to ensure IDs and lifecycle are managed consistently. + + + +[WorldMapArtifactsGenerator](./server.worldmapartifactsgenerator.md) + + + + + + + + +[WorldMapChunkCacheCodec](./server.worldmapchunkcachecodec.md) + + + + + + + + +[WorldMapCodec](./server.worldmapcodec.md) + + + + + + + + +[WorldMapFileLoader](./server.worldmapfileloader.md) + + + + + @@ -905,6 +1022,15 @@ See `ParticleEmitterEventPayloads` for the payloads. \*\*Category:\*\* Events + + + +[PerformanceMonitorEvent](./server.performancemonitorevent.md) + + + + + @@ -1097,6 +1223,42 @@ Description +[Monitor(operationName)](./server.monitor.md) + + + + + + + + +[monitorAsyncBlock(name, fn)](./server.monitorasyncblock.md) + + + + + + + + +[monitorBlock(name, fn)](./server.monitorblock.md) + + + + + + + + +[MonitorClass(prefix)](./server.monitorclass.md) + + + + + + + + [startServer(init)](./server.startserver.md) @@ -1309,6 +1471,24 @@ Event payloads for BlockTypeRegistry emitted events. \*\*Category:\*\* Events + + + +[BotBehavior](./server.botbehavior.md) + + + + + + + + +[BotPlayerOptions](./server.botplayeroptions.md) + + + + + @@ -1324,6 +1504,15 @@ Use for: capsule-shaped colliders. Do NOT use for: other shapes; use the matchin \*\*Category:\*\* Physics + + + +[ChaseBehaviorOptions](./server.chasebehavioroptions.md) + + + + + @@ -1350,6 +1539,24 @@ Event payloads for ChunkLattice emitted events. \*\*Category:\*\* Events + + + +[CompressedWorldMap](./server.compressedworldmap.md) + + + + + + + + +[CompressWorldMapOptions](./server.compressworldmapoptions.md) + + + + + @@ -1365,6 +1572,15 @@ Use for: cone-shaped colliders. Do NOT use for: other shapes; use the matching c \*\*Category:\*\* Physics + + + +[CreateWorldMapChunkCacheOptions](./server.createworldmapchunkcacheoptions.md) + + + + + @@ -1516,6 +1732,15 @@ Event payloads for GameServer emitted events. \*\*Category:\*\* Events + + + +[InteractBehaviorOptions](./server.interactbehavioroptions.md) + + + + + @@ -1561,6 +1786,15 @@ Use for: entities rendered from a glTF model. Do NOT use for: block entities; us \*\*Category:\*\* Entities + + + +[NetworkMetricsSnapshot](./server.networkmetricssnapshot.md) + + + + + @@ -1576,6 +1810,15 @@ Use for: explicitly disabling collider creation. Do NOT use for: physical intera \*\*Category:\*\* Physics + + + +[OperationStats](./server.operationstats.md) + + + + + @@ -1617,6 +1860,33 @@ Use for: configuring an emitter before calling `ParticleEmitter.spawn`. \*\*Category:\*\* Particles + + + +[PerformanceMonitorEventPayloads](./server.performancemonitoreventpayloads.md) + + + + + + + + +[PerformanceMonitorOptions](./server.performancemonitoroptions.md) + + + + + + + + +[PerformanceSnapshot](./server.performancesnapshot.md) + + + + + @@ -1682,6 +1952,15 @@ A quaternion. \*\*Category:\*\* Math + + + +[RandomWalkOptions](./server.randomwalkoptions.md) + + + + + @@ -1764,6 +2043,15 @@ A 3x3 symmetric positive-definite matrix for spatial dynamics. \*\*Category:\*\* Math + + + +[TickReport](./server.tickreport.md) + + + + + @@ -1913,6 +2201,33 @@ A map representation for initializing a world. Use for: importing static maps or tooling exports via `World.loadMap`. Do NOT use for: incremental edits while a world is live; use chunk/block APIs instead. + + + +[WorldMapChunkCache](./server.worldmapchunkcache.md) + + + + + + + + +[WorldMapChunkCacheMetadata](./server.worldmapchunkcachemetadata.md) + + + + + + + + +[WorldMapChunkCacheOptions](./server.worldmapchunkcacheoptions.md) + + + + + @@ -2000,6 +2315,15 @@ Description +[AnyWorldMap](./server.anyworldmap.md) + + + + + + + + [BlockRotation](./server.blockrotation.md) @@ -2073,6 +2397,15 @@ A set of collision groups. A callback function for a chat command. + + + +[CompressedWorldMapAlgorithm](./server.compressedworldmapalgorithm.md) + + + + + @@ -2536,5 +2869,23 @@ Callback invoked when the entity finishes moving to a waypoint. Callback invoked when a waypoint is skipped due to timeout. + + + +[WorldMapArtifacts](./server.worldmapartifacts.md) + + + + + + + + +[WorldMapChunkCacheAlgorithm](./server.worldmapchunkcachealgorithm.md) + + + + + diff --git a/sdk/docs/server.monitor.md b/sdk/docs/server.monitor.md new file mode 100644 index 00000000..4ed6314e --- /dev/null +++ b/sdk/docs/server.monitor.md @@ -0,0 +1,51 @@ + + +[Home](./index.md) > [server](./server.md) > [Monitor](./server.monitor.md) + +## Monitor() function + +**Signature:** + +```typescript +export declare function Monitor(operationName?: string): MethodDecorator; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +operationName + + + + +string + + + + +_(Optional)_ + + +
+**Returns:** + +MethodDecorator + diff --git a/sdk/docs/server.monitorasyncblock.md b/sdk/docs/server.monitorasyncblock.md new file mode 100644 index 00000000..96f02d12 --- /dev/null +++ b/sdk/docs/server.monitorasyncblock.md @@ -0,0 +1,63 @@ + + +[Home](./index.md) > [server](./server.md) > [monitorAsyncBlock](./server.monitorasyncblock.md) + +## monitorAsyncBlock() function + +**Signature:** + +```typescript +export declare function monitorAsyncBlock(name: string, fn: () => Promise): Promise; +``` + +## Parameters + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +name + + + + +string + + + + + +
+ +fn + + + + +() => Promise<T> + + + + + +
+**Returns:** + +Promise<T> + diff --git a/sdk/docs/server.monitorblock.md b/sdk/docs/server.monitorblock.md new file mode 100644 index 00000000..7f72b48b --- /dev/null +++ b/sdk/docs/server.monitorblock.md @@ -0,0 +1,63 @@ + + +[Home](./index.md) > [server](./server.md) > [monitorBlock](./server.monitorblock.md) + +## monitorBlock() function + +**Signature:** + +```typescript +export declare function monitorBlock(name: string, fn: () => T): T; +``` + +## Parameters + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +name + + + + +string + + + + + +
+ +fn + + + + +() => T + + + + + +
+**Returns:** + +T + diff --git a/sdk/docs/server.monitorclass.md b/sdk/docs/server.monitorclass.md new file mode 100644 index 00000000..b41cdc78 --- /dev/null +++ b/sdk/docs/server.monitorclass.md @@ -0,0 +1,51 @@ + + +[Home](./index.md) > [server](./server.md) > [MonitorClass](./server.monitorclass.md) + +## MonitorClass() function + +**Signature:** + +```typescript +export declare function MonitorClass(prefix?: string): (constructor: TConstructor) => TConstructor; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +prefix + + + + +string + + + + +_(Optional)_ + + +
+**Returns:** + +<TConstructor extends AnyConstructor>(constructor: TConstructor) => TConstructor + diff --git a/sdk/docs/server.networkmetrics.disable.md b/sdk/docs/server.networkmetrics.disable.md new file mode 100644 index 00000000..9e20603f --- /dev/null +++ b/sdk/docs/server.networkmetrics.disable.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetrics](./server.networkmetrics.md) > [disable](./server.networkmetrics.disable.md) + +## NetworkMetrics.disable() method + +**Signature:** + +```typescript +disable(): void; +``` +**Returns:** + +void + diff --git a/sdk/docs/server.networkmetrics.enable.md b/sdk/docs/server.networkmetrics.enable.md new file mode 100644 index 00000000..5247b888 --- /dev/null +++ b/sdk/docs/server.networkmetrics.enable.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetrics](./server.networkmetrics.md) > [enable](./server.networkmetrics.enable.md) + +## NetworkMetrics.enable() method + +**Signature:** + +```typescript +enable(): void; +``` +**Returns:** + +void + diff --git a/sdk/docs/server.networkmetrics.getsnapshot.md b/sdk/docs/server.networkmetrics.getsnapshot.md new file mode 100644 index 00000000..9edb9d55 --- /dev/null +++ b/sdk/docs/server.networkmetrics.getsnapshot.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetrics](./server.networkmetrics.md) > [getSnapshot](./server.networkmetrics.getsnapshot.md) + +## NetworkMetrics.getSnapshot() method + +**Signature:** + +```typescript +getSnapshot(): NetworkMetricsSnapshot; +``` +**Returns:** + +[NetworkMetricsSnapshot](./server.networkmetricssnapshot.md) + diff --git a/sdk/docs/server.networkmetrics.instance.md b/sdk/docs/server.networkmetrics.instance.md new file mode 100644 index 00000000..f9556a99 --- /dev/null +++ b/sdk/docs/server.networkmetrics.instance.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetrics](./server.networkmetrics.md) > [instance](./server.networkmetrics.instance.md) + +## NetworkMetrics.instance property + +**Signature:** + +```typescript +static get instance(): NetworkMetrics; +``` diff --git a/sdk/docs/server.networkmetrics.isenabled.md b/sdk/docs/server.networkmetrics.isenabled.md new file mode 100644 index 00000000..81c1906a --- /dev/null +++ b/sdk/docs/server.networkmetrics.isenabled.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetrics](./server.networkmetrics.md) > [isEnabled](./server.networkmetrics.isenabled.md) + +## NetworkMetrics.isEnabled property + +**Signature:** + +```typescript +get isEnabled(): boolean; +``` diff --git a/sdk/docs/server.networkmetrics.md b/sdk/docs/server.networkmetrics.md new file mode 100644 index 00000000..5e5eeba5 --- /dev/null +++ b/sdk/docs/server.networkmetrics.md @@ -0,0 +1,228 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetrics](./server.networkmetrics.md) + +## NetworkMetrics class + +**Signature:** + +```typescript +export default class NetworkMetrics +``` + +## Properties + + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[instance](./server.networkmetrics.instance.md) + + + + +`static` + +`readonly` + + + + +[NetworkMetrics](./server.networkmetrics.md) + + + + + +
+ +[isEnabled](./server.networkmetrics.isenabled.md) + + + + +`readonly` + + + + +boolean + + + + + +
+ +## Methods + + + + + + + + + + + + + +
+ +Method + + + + +Modifiers + + + + +Description + + +
+ +[disable()](./server.networkmetrics.disable.md) + + + + + + + + +
+ +[enable()](./server.networkmetrics.enable.md) + + + + + + + + +
+ +[getSnapshot()](./server.networkmetrics.getsnapshot.md) + + + + + + + + +
+ +[recordBytesReceived(bytes)](./server.networkmetrics.recordbytesreceived.md) + + + + + + + + +
+ +[recordBytesSent(bytes)](./server.networkmetrics.recordbytessent.md) + + + + + + + + +
+ +[recordCompression()](./server.networkmetrics.recordcompression.md) + + + + + + + + +
+ +[recordPacketReceived()](./server.networkmetrics.recordpacketreceived.md) + + + + + + + + +
+ +[recordPacketSent()](./server.networkmetrics.recordpacketsent.md) + + + + + + + + +
+ +[recordSerialization(durationMs)](./server.networkmetrics.recordserialization.md) + + + + + + + + +
+ +[reset()](./server.networkmetrics.reset.md) + + + + + + + + +
+ +[setConnectedPlayers(count)](./server.networkmetrics.setconnectedplayers.md) + + + + + + + + +
diff --git a/sdk/docs/server.networkmetrics.recordbytesreceived.md b/sdk/docs/server.networkmetrics.recordbytesreceived.md new file mode 100644 index 00000000..045e15c2 --- /dev/null +++ b/sdk/docs/server.networkmetrics.recordbytesreceived.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetrics](./server.networkmetrics.md) > [recordBytesReceived](./server.networkmetrics.recordbytesreceived.md) + +## NetworkMetrics.recordBytesReceived() method + +**Signature:** + +```typescript +recordBytesReceived(bytes: number): void; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +bytes + + + + +number + + + + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.networkmetrics.recordbytessent.md b/sdk/docs/server.networkmetrics.recordbytessent.md new file mode 100644 index 00000000..e6786644 --- /dev/null +++ b/sdk/docs/server.networkmetrics.recordbytessent.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetrics](./server.networkmetrics.md) > [recordBytesSent](./server.networkmetrics.recordbytessent.md) + +## NetworkMetrics.recordBytesSent() method + +**Signature:** + +```typescript +recordBytesSent(bytes: number): void; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +bytes + + + + +number + + + + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.networkmetrics.recordcompression.md b/sdk/docs/server.networkmetrics.recordcompression.md new file mode 100644 index 00000000..36231c4b --- /dev/null +++ b/sdk/docs/server.networkmetrics.recordcompression.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetrics](./server.networkmetrics.md) > [recordCompression](./server.networkmetrics.recordcompression.md) + +## NetworkMetrics.recordCompression() method + +**Signature:** + +```typescript +recordCompression(): void; +``` +**Returns:** + +void + diff --git a/sdk/docs/server.networkmetrics.recordpacketreceived.md b/sdk/docs/server.networkmetrics.recordpacketreceived.md new file mode 100644 index 00000000..6e12ae8c --- /dev/null +++ b/sdk/docs/server.networkmetrics.recordpacketreceived.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetrics](./server.networkmetrics.md) > [recordPacketReceived](./server.networkmetrics.recordpacketreceived.md) + +## NetworkMetrics.recordPacketReceived() method + +**Signature:** + +```typescript +recordPacketReceived(): void; +``` +**Returns:** + +void + diff --git a/sdk/docs/server.networkmetrics.recordpacketsent.md b/sdk/docs/server.networkmetrics.recordpacketsent.md new file mode 100644 index 00000000..f097f86e --- /dev/null +++ b/sdk/docs/server.networkmetrics.recordpacketsent.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetrics](./server.networkmetrics.md) > [recordPacketSent](./server.networkmetrics.recordpacketsent.md) + +## NetworkMetrics.recordPacketSent() method + +**Signature:** + +```typescript +recordPacketSent(): void; +``` +**Returns:** + +void + diff --git a/sdk/docs/server.networkmetrics.recordserialization.md b/sdk/docs/server.networkmetrics.recordserialization.md new file mode 100644 index 00000000..047b0297 --- /dev/null +++ b/sdk/docs/server.networkmetrics.recordserialization.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetrics](./server.networkmetrics.md) > [recordSerialization](./server.networkmetrics.recordserialization.md) + +## NetworkMetrics.recordSerialization() method + +**Signature:** + +```typescript +recordSerialization(durationMs: number): void; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +durationMs + + + + +number + + + + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.networkmetrics.reset.md b/sdk/docs/server.networkmetrics.reset.md new file mode 100644 index 00000000..d89791fd --- /dev/null +++ b/sdk/docs/server.networkmetrics.reset.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetrics](./server.networkmetrics.md) > [reset](./server.networkmetrics.reset.md) + +## NetworkMetrics.reset() method + +**Signature:** + +```typescript +reset(): void; +``` +**Returns:** + +void + diff --git a/sdk/docs/server.networkmetrics.setconnectedplayers.md b/sdk/docs/server.networkmetrics.setconnectedplayers.md new file mode 100644 index 00000000..3041dd80 --- /dev/null +++ b/sdk/docs/server.networkmetrics.setconnectedplayers.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetrics](./server.networkmetrics.md) > [setConnectedPlayers](./server.networkmetrics.setconnectedplayers.md) + +## NetworkMetrics.setConnectedPlayers() method + +**Signature:** + +```typescript +setConnectedPlayers(count: number): void; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +count + + + + +number + + + + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.networkmetricssnapshot.avgserializationms.md b/sdk/docs/server.networkmetricssnapshot.avgserializationms.md new file mode 100644 index 00000000..024e272b --- /dev/null +++ b/sdk/docs/server.networkmetricssnapshot.avgserializationms.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetricsSnapshot](./server.networkmetricssnapshot.md) > [avgSerializationMs](./server.networkmetricssnapshot.avgserializationms.md) + +## NetworkMetricsSnapshot.avgSerializationMs property + +**Signature:** + +```typescript +avgSerializationMs: number; +``` diff --git a/sdk/docs/server.networkmetricssnapshot.bytesreceivedpersecond.md b/sdk/docs/server.networkmetricssnapshot.bytesreceivedpersecond.md new file mode 100644 index 00000000..1718d717 --- /dev/null +++ b/sdk/docs/server.networkmetricssnapshot.bytesreceivedpersecond.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetricsSnapshot](./server.networkmetricssnapshot.md) > [bytesReceivedPerSecond](./server.networkmetricssnapshot.bytesreceivedpersecond.md) + +## NetworkMetricsSnapshot.bytesReceivedPerSecond property + +**Signature:** + +```typescript +bytesReceivedPerSecond: number; +``` diff --git a/sdk/docs/server.networkmetricssnapshot.bytesreceivedtotal.md b/sdk/docs/server.networkmetricssnapshot.bytesreceivedtotal.md new file mode 100644 index 00000000..8a024d0c --- /dev/null +++ b/sdk/docs/server.networkmetricssnapshot.bytesreceivedtotal.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetricsSnapshot](./server.networkmetricssnapshot.md) > [bytesReceivedTotal](./server.networkmetricssnapshot.bytesreceivedtotal.md) + +## NetworkMetricsSnapshot.bytesReceivedTotal property + +**Signature:** + +```typescript +bytesReceivedTotal: number; +``` diff --git a/sdk/docs/server.networkmetricssnapshot.bytessentpersecond.md b/sdk/docs/server.networkmetricssnapshot.bytessentpersecond.md new file mode 100644 index 00000000..8d7eff57 --- /dev/null +++ b/sdk/docs/server.networkmetricssnapshot.bytessentpersecond.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetricsSnapshot](./server.networkmetricssnapshot.md) > [bytesSentPerSecond](./server.networkmetricssnapshot.bytessentpersecond.md) + +## NetworkMetricsSnapshot.bytesSentPerSecond property + +**Signature:** + +```typescript +bytesSentPerSecond: number; +``` diff --git a/sdk/docs/server.networkmetricssnapshot.bytessenttotal.md b/sdk/docs/server.networkmetricssnapshot.bytessenttotal.md new file mode 100644 index 00000000..2f753b4b --- /dev/null +++ b/sdk/docs/server.networkmetricssnapshot.bytessenttotal.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetricsSnapshot](./server.networkmetricssnapshot.md) > [bytesSentTotal](./server.networkmetricssnapshot.bytessenttotal.md) + +## NetworkMetricsSnapshot.bytesSentTotal property + +**Signature:** + +```typescript +bytesSentTotal: number; +``` diff --git a/sdk/docs/server.networkmetricssnapshot.compressioncount.md b/sdk/docs/server.networkmetricssnapshot.compressioncount.md new file mode 100644 index 00000000..541c2d99 --- /dev/null +++ b/sdk/docs/server.networkmetricssnapshot.compressioncount.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetricsSnapshot](./server.networkmetricssnapshot.md) > [compressionCount](./server.networkmetricssnapshot.compressioncount.md) + +## NetworkMetricsSnapshot.compressionCount property + +**Signature:** + +```typescript +compressionCount: number; +``` diff --git a/sdk/docs/server.networkmetricssnapshot.connectedplayers.md b/sdk/docs/server.networkmetricssnapshot.connectedplayers.md new file mode 100644 index 00000000..4cc3c5e3 --- /dev/null +++ b/sdk/docs/server.networkmetricssnapshot.connectedplayers.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetricsSnapshot](./server.networkmetricssnapshot.md) > [connectedPlayers](./server.networkmetricssnapshot.connectedplayers.md) + +## NetworkMetricsSnapshot.connectedPlayers property + +**Signature:** + +```typescript +connectedPlayers: number; +``` diff --git a/sdk/docs/server.networkmetricssnapshot.md b/sdk/docs/server.networkmetricssnapshot.md new file mode 100644 index 00000000..b0c452af --- /dev/null +++ b/sdk/docs/server.networkmetricssnapshot.md @@ -0,0 +1,189 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetricsSnapshot](./server.networkmetricssnapshot.md) + +## NetworkMetricsSnapshot interface + +**Signature:** + +```typescript +export interface NetworkMetricsSnapshot +``` + +## Properties + + + + + + + + + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[avgSerializationMs](./server.networkmetricssnapshot.avgserializationms.md) + + + + + + + +number + + + + + +
+ +[bytesReceivedPerSecond](./server.networkmetricssnapshot.bytesreceivedpersecond.md) + + + + + + + +number + + + + + +
+ +[bytesReceivedTotal](./server.networkmetricssnapshot.bytesreceivedtotal.md) + + + + + + + +number + + + + + +
+ +[bytesSentPerSecond](./server.networkmetricssnapshot.bytessentpersecond.md) + + + + + + + +number + + + + + +
+ +[bytesSentTotal](./server.networkmetricssnapshot.bytessenttotal.md) + + + + + + + +number + + + + + +
+ +[compressionCount](./server.networkmetricssnapshot.compressioncount.md) + + + + + + + +number + + + + + +
+ +[connectedPlayers](./server.networkmetricssnapshot.connectedplayers.md) + + + + + + + +number + + + + + +
+ +[packetsReceivedPerSecond](./server.networkmetricssnapshot.packetsreceivedpersecond.md) + + + + + + + +number + + + + + +
+ +[packetsSentPerSecond](./server.networkmetricssnapshot.packetssentpersecond.md) + + + + + + + +number + + + + + +
diff --git a/sdk/docs/server.networkmetricssnapshot.packetsreceivedpersecond.md b/sdk/docs/server.networkmetricssnapshot.packetsreceivedpersecond.md new file mode 100644 index 00000000..3cf3e83d --- /dev/null +++ b/sdk/docs/server.networkmetricssnapshot.packetsreceivedpersecond.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetricsSnapshot](./server.networkmetricssnapshot.md) > [packetsReceivedPerSecond](./server.networkmetricssnapshot.packetsreceivedpersecond.md) + +## NetworkMetricsSnapshot.packetsReceivedPerSecond property + +**Signature:** + +```typescript +packetsReceivedPerSecond: number; +``` diff --git a/sdk/docs/server.networkmetricssnapshot.packetssentpersecond.md b/sdk/docs/server.networkmetricssnapshot.packetssentpersecond.md new file mode 100644 index 00000000..c95aa93a --- /dev/null +++ b/sdk/docs/server.networkmetricssnapshot.packetssentpersecond.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [NetworkMetricsSnapshot](./server.networkmetricssnapshot.md) > [packetsSentPerSecond](./server.networkmetricssnapshot.packetssentpersecond.md) + +## NetworkMetricsSnapshot.packetsSentPerSecond property + +**Signature:** + +```typescript +packetsSentPerSecond: number; +``` diff --git a/sdk/docs/server.operationstats.avgms.md b/sdk/docs/server.operationstats.avgms.md new file mode 100644 index 00000000..b05e0d38 --- /dev/null +++ b/sdk/docs/server.operationstats.avgms.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [OperationStats](./server.operationstats.md) > [avgMs](./server.operationstats.avgms.md) + +## OperationStats.avgMs property + +**Signature:** + +```typescript +avgMs: number; +``` diff --git a/sdk/docs/server.operationstats.count.md b/sdk/docs/server.operationstats.count.md new file mode 100644 index 00000000..d97295f8 --- /dev/null +++ b/sdk/docs/server.operationstats.count.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [OperationStats](./server.operationstats.md) > [count](./server.operationstats.count.md) + +## OperationStats.count property + +**Signature:** + +```typescript +count: number; +``` diff --git a/sdk/docs/server.operationstats.lastms.md b/sdk/docs/server.operationstats.lastms.md new file mode 100644 index 00000000..8f3a6ab2 --- /dev/null +++ b/sdk/docs/server.operationstats.lastms.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [OperationStats](./server.operationstats.md) > [lastMs](./server.operationstats.lastms.md) + +## OperationStats.lastMs property + +**Signature:** + +```typescript +lastMs: number; +``` diff --git a/sdk/docs/server.operationstats.maxms.md b/sdk/docs/server.operationstats.maxms.md new file mode 100644 index 00000000..23a7eec9 --- /dev/null +++ b/sdk/docs/server.operationstats.maxms.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [OperationStats](./server.operationstats.md) > [maxMs](./server.operationstats.maxms.md) + +## OperationStats.maxMs property + +**Signature:** + +```typescript +maxMs: number; +``` diff --git a/sdk/docs/server.operationstats.md b/sdk/docs/server.operationstats.md new file mode 100644 index 00000000..d97e2225 --- /dev/null +++ b/sdk/docs/server.operationstats.md @@ -0,0 +1,189 @@ + + +[Home](./index.md) > [server](./server.md) > [OperationStats](./server.operationstats.md) + +## OperationStats interface + +**Signature:** + +```typescript +export interface OperationStats +``` + +## Properties + + + + + + + + + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[avgMs](./server.operationstats.avgms.md) + + + + + + + +number + + + + + +
+ +[count](./server.operationstats.count.md) + + + + + + + +number + + + + + +
+ +[lastMs](./server.operationstats.lastms.md) + + + + + + + +number + + + + + +
+ +[maxMs](./server.operationstats.maxms.md) + + + + + + + +number + + + + + +
+ +[minMs](./server.operationstats.minms.md) + + + + + + + +number + + + + + +
+ +[p50Ms](./server.operationstats.p50ms.md) + + + + + + + +number + + + + + +
+ +[p95Ms](./server.operationstats.p95ms.md) + + + + + + + +number + + + + + +
+ +[p99Ms](./server.operationstats.p99ms.md) + + + + + + + +number + + + + + +
+ +[totalMs](./server.operationstats.totalms.md) + + + + + + + +number + + + + + +
diff --git a/sdk/docs/server.operationstats.minms.md b/sdk/docs/server.operationstats.minms.md new file mode 100644 index 00000000..99d1d2ca --- /dev/null +++ b/sdk/docs/server.operationstats.minms.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [OperationStats](./server.operationstats.md) > [minMs](./server.operationstats.minms.md) + +## OperationStats.minMs property + +**Signature:** + +```typescript +minMs: number; +``` diff --git a/sdk/docs/server.operationstats.p50ms.md b/sdk/docs/server.operationstats.p50ms.md new file mode 100644 index 00000000..c6248ba4 --- /dev/null +++ b/sdk/docs/server.operationstats.p50ms.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [OperationStats](./server.operationstats.md) > [p50Ms](./server.operationstats.p50ms.md) + +## OperationStats.p50Ms property + +**Signature:** + +```typescript +p50Ms: number; +``` diff --git a/sdk/docs/server.operationstats.p95ms.md b/sdk/docs/server.operationstats.p95ms.md new file mode 100644 index 00000000..4b984f68 --- /dev/null +++ b/sdk/docs/server.operationstats.p95ms.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [OperationStats](./server.operationstats.md) > [p95Ms](./server.operationstats.p95ms.md) + +## OperationStats.p95Ms property + +**Signature:** + +```typescript +p95Ms: number; +``` diff --git a/sdk/docs/server.operationstats.p99ms.md b/sdk/docs/server.operationstats.p99ms.md new file mode 100644 index 00000000..837ab220 --- /dev/null +++ b/sdk/docs/server.operationstats.p99ms.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [OperationStats](./server.operationstats.md) > [p99Ms](./server.operationstats.p99ms.md) + +## OperationStats.p99Ms property + +**Signature:** + +```typescript +p99Ms: number; +``` diff --git a/sdk/docs/server.operationstats.totalms.md b/sdk/docs/server.operationstats.totalms.md new file mode 100644 index 00000000..44d85661 --- /dev/null +++ b/sdk/docs/server.operationstats.totalms.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [OperationStats](./server.operationstats.md) > [totalMs](./server.operationstats.totalms.md) + +## OperationStats.totalMs property + +**Signature:** + +```typescript +totalMs: number; +``` diff --git a/sdk/docs/server.performancemonitor.begintick.md b/sdk/docs/server.performancemonitor.begintick.md new file mode 100644 index 00000000..a2544e83 --- /dev/null +++ b/sdk/docs/server.performancemonitor.begintick.md @@ -0,0 +1,93 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitor](./server.performancemonitor.md) > [beginTick](./server.performancemonitor.begintick.md) + +## PerformanceMonitor.beginTick() method + +**Signature:** + +```typescript +beginTick(tick: number, entityCount: number, playerCount: number, worldId?: number): void; +``` + +## Parameters + + + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +tick + + + + +number + + + + + +
+ +entityCount + + + + +number + + + + + +
+ +playerCount + + + + +number + + + + + +
+ +worldId + + + + +number + + + + +_(Optional)_ + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.performancemonitor.disable.md b/sdk/docs/server.performancemonitor.disable.md new file mode 100644 index 00000000..609aad60 --- /dev/null +++ b/sdk/docs/server.performancemonitor.disable.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitor](./server.performancemonitor.md) > [disable](./server.performancemonitor.disable.md) + +## PerformanceMonitor.disable() method + +**Signature:** + +```typescript +disable(): void; +``` +**Returns:** + +void + diff --git a/sdk/docs/server.performancemonitor.enable.md b/sdk/docs/server.performancemonitor.enable.md new file mode 100644 index 00000000..42814477 --- /dev/null +++ b/sdk/docs/server.performancemonitor.enable.md @@ -0,0 +1,51 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitor](./server.performancemonitor.md) > [enable](./server.performancemonitor.enable.md) + +## PerformanceMonitor.enable() method + +**Signature:** + +```typescript +enable(options?: PerformanceMonitorOptions): void; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +options + + + + +[PerformanceMonitorOptions](./server.performancemonitoroptions.md) + + + + +_(Optional)_ + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.performancemonitor.enableentityprofiling.md b/sdk/docs/server.performancemonitor.enableentityprofiling.md new file mode 100644 index 00000000..997bd3c9 --- /dev/null +++ b/sdk/docs/server.performancemonitor.enableentityprofiling.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitor](./server.performancemonitor.md) > [enableEntityProfiling](./server.performancemonitor.enableentityprofiling.md) + +## PerformanceMonitor.enableEntityProfiling() method + +**Signature:** + +```typescript +enableEntityProfiling(enabled: boolean): void; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +enabled + + + + +boolean + + + + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.performancemonitor.endtick.md b/sdk/docs/server.performancemonitor.endtick.md new file mode 100644 index 00000000..6cda9824 --- /dev/null +++ b/sdk/docs/server.performancemonitor.endtick.md @@ -0,0 +1,51 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitor](./server.performancemonitor.md) > [endTick](./server.performancemonitor.endtick.md) + +## PerformanceMonitor.endTick() method + +**Signature:** + +```typescript +endTick(worldId?: number): void; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +worldId + + + + +number + + + + +_(Optional)_ + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.performancemonitor.getentitycosts.md b/sdk/docs/server.performancemonitor.getentitycosts.md new file mode 100644 index 00000000..11ed8b63 --- /dev/null +++ b/sdk/docs/server.performancemonitor.getentitycosts.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitor](./server.performancemonitor.md) > [getEntityCosts](./server.performancemonitor.getentitycosts.md) + +## PerformanceMonitor.getEntityCosts() method + +**Signature:** + +```typescript +getEntityCosts(): Map; +``` +**Returns:** + +Map<number, { tickMs: number; name: string; }> + diff --git a/sdk/docs/server.performancemonitor.getsnapshot.md b/sdk/docs/server.performancemonitor.getsnapshot.md new file mode 100644 index 00000000..ad8b371d --- /dev/null +++ b/sdk/docs/server.performancemonitor.getsnapshot.md @@ -0,0 +1,51 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitor](./server.performancemonitor.md) > [getSnapshot](./server.performancemonitor.getsnapshot.md) + +## PerformanceMonitor.getSnapshot() method + +**Signature:** + +```typescript +getSnapshot(worldId?: number): PerformanceSnapshot; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +worldId + + + + +number + + + + +_(Optional)_ + + +
+**Returns:** + +[PerformanceSnapshot](./server.performancesnapshot.md) + diff --git a/sdk/docs/server.performancemonitor.instance.md b/sdk/docs/server.performancemonitor.instance.md new file mode 100644 index 00000000..dbc5c151 --- /dev/null +++ b/sdk/docs/server.performancemonitor.instance.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitor](./server.performancemonitor.md) > [instance](./server.performancemonitor.instance.md) + +## PerformanceMonitor.instance property + +**Signature:** + +```typescript +static get instance(): PerformanceMonitor; +``` diff --git a/sdk/docs/server.performancemonitor.isenabled.md b/sdk/docs/server.performancemonitor.isenabled.md new file mode 100644 index 00000000..d694eb54 --- /dev/null +++ b/sdk/docs/server.performancemonitor.isenabled.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitor](./server.performancemonitor.md) > [isEnabled](./server.performancemonitor.isenabled.md) + +## PerformanceMonitor.isEnabled property + +**Signature:** + +```typescript +get isEnabled(): boolean; +``` diff --git a/sdk/docs/server.performancemonitor.isentityprofilingenabled.md b/sdk/docs/server.performancemonitor.isentityprofilingenabled.md new file mode 100644 index 00000000..3237621b --- /dev/null +++ b/sdk/docs/server.performancemonitor.isentityprofilingenabled.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitor](./server.performancemonitor.md) > [isEntityProfilingEnabled](./server.performancemonitor.isentityprofilingenabled.md) + +## PerformanceMonitor.isEntityProfilingEnabled property + +**Signature:** + +```typescript +get isEntityProfilingEnabled(): boolean; +``` diff --git a/sdk/docs/server.performancemonitor.md b/sdk/docs/server.performancemonitor.md new file mode 100644 index 00000000..802e7c42 --- /dev/null +++ b/sdk/docs/server.performancemonitor.md @@ -0,0 +1,272 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitor](./server.performancemonitor.md) + +## PerformanceMonitor class + +**Signature:** + +```typescript +export default class PerformanceMonitor extends EventRouter +``` +**Extends:** [EventRouter](./server.eventrouter.md) + +## Properties + + + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[instance](./server.performancemonitor.instance.md) + + + + +`static` + +`readonly` + + + + +[PerformanceMonitor](./server.performancemonitor.md) + + + + + +
+ +[isEnabled](./server.performancemonitor.isenabled.md) + + + + +`readonly` + + + + +boolean + + + + + +
+ +[isEntityProfilingEnabled](./server.performancemonitor.isentityprofilingenabled.md) + + + + +`readonly` + + + + +boolean + + + + + +
+ +## Methods + + + + + + + + + + + + + + + +
+ +Method + + + + +Modifiers + + + + +Description + + +
+ +[beginTick(tick, entityCount, playerCount, worldId)](./server.performancemonitor.begintick.md) + + + + + + + + +
+ +[disable()](./server.performancemonitor.disable.md) + + + + + + + + +
+ +[enable(options)](./server.performancemonitor.enable.md) + + + + + + + + +
+ +[enableEntityProfiling(enabled)](./server.performancemonitor.enableentityprofiling.md) + + + + + + + + +
+ +[endTick(worldId)](./server.performancemonitor.endtick.md) + + + + + + + + +
+ +[getEntityCosts()](./server.performancemonitor.getentitycosts.md) + + + + + + + + +
+ +[getSnapshot(worldId)](./server.performancemonitor.getsnapshot.md) + + + + + + + + +
+ +[measure(name, fn)](./server.performancemonitor.measure.md) + + + + + + + + +
+ +[measureAsync(name, fn)](./server.performancemonitor.measureasync.md) + + + + + + + + +
+ +[recordEntityCost(entityId, name, tickMs)](./server.performancemonitor.recordentitycost.md) + + + + + + + + +
+ +[recordPhase(phaseName, durationMs, worldId)](./server.performancemonitor.recordphase.md) + + + + + + + + +
+ +[resetStats()](./server.performancemonitor.resetstats.md) + + + + + + + + +
+ +[startTiming(name)](./server.performancemonitor.starttiming.md) + + + + + + + + +
diff --git a/sdk/docs/server.performancemonitor.measure.md b/sdk/docs/server.performancemonitor.measure.md new file mode 100644 index 00000000..deffdee5 --- /dev/null +++ b/sdk/docs/server.performancemonitor.measure.md @@ -0,0 +1,63 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitor](./server.performancemonitor.md) > [measure](./server.performancemonitor.measure.md) + +## PerformanceMonitor.measure() method + +**Signature:** + +```typescript +measure(name: string, fn: () => T): T; +``` + +## Parameters + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +name + + + + +string + + + + + +
+ +fn + + + + +() => T + + + + + +
+**Returns:** + +T + diff --git a/sdk/docs/server.performancemonitor.measureasync.md b/sdk/docs/server.performancemonitor.measureasync.md new file mode 100644 index 00000000..ba084327 --- /dev/null +++ b/sdk/docs/server.performancemonitor.measureasync.md @@ -0,0 +1,63 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitor](./server.performancemonitor.md) > [measureAsync](./server.performancemonitor.measureasync.md) + +## PerformanceMonitor.measureAsync() method + +**Signature:** + +```typescript +measureAsync(name: string, fn: () => Promise): Promise; +``` + +## Parameters + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +name + + + + +string + + + + + +
+ +fn + + + + +() => Promise<T> + + + + + +
+**Returns:** + +Promise<T> + diff --git a/sdk/docs/server.performancemonitor.recordentitycost.md b/sdk/docs/server.performancemonitor.recordentitycost.md new file mode 100644 index 00000000..6808fd0e --- /dev/null +++ b/sdk/docs/server.performancemonitor.recordentitycost.md @@ -0,0 +1,77 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitor](./server.performancemonitor.md) > [recordEntityCost](./server.performancemonitor.recordentitycost.md) + +## PerformanceMonitor.recordEntityCost() method + +**Signature:** + +```typescript +recordEntityCost(entityId: number, name: string, tickMs: number): void; +``` + +## Parameters + + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +entityId + + + + +number + + + + + +
+ +name + + + + +string + + + + + +
+ +tickMs + + + + +number + + + + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.performancemonitor.recordphase.md b/sdk/docs/server.performancemonitor.recordphase.md new file mode 100644 index 00000000..25d74151 --- /dev/null +++ b/sdk/docs/server.performancemonitor.recordphase.md @@ -0,0 +1,79 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitor](./server.performancemonitor.md) > [recordPhase](./server.performancemonitor.recordphase.md) + +## PerformanceMonitor.recordPhase() method + +**Signature:** + +```typescript +recordPhase(phaseName: string, durationMs: number, worldId?: number): void; +``` + +## Parameters + + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +phaseName + + + + +string + + + + + +
+ +durationMs + + + + +number + + + + + +
+ +worldId + + + + +number + + + + +_(Optional)_ + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.performancemonitor.resetstats.md b/sdk/docs/server.performancemonitor.resetstats.md new file mode 100644 index 00000000..33b62df0 --- /dev/null +++ b/sdk/docs/server.performancemonitor.resetstats.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitor](./server.performancemonitor.md) > [resetStats](./server.performancemonitor.resetstats.md) + +## PerformanceMonitor.resetStats() method + +**Signature:** + +```typescript +resetStats(): void; +``` +**Returns:** + +void + diff --git a/sdk/docs/server.performancemonitor.starttiming.md b/sdk/docs/server.performancemonitor.starttiming.md new file mode 100644 index 00000000..89d6471d --- /dev/null +++ b/sdk/docs/server.performancemonitor.starttiming.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitor](./server.performancemonitor.md) > [startTiming](./server.performancemonitor.starttiming.md) + +## PerformanceMonitor.startTiming() method + +**Signature:** + +```typescript +startTiming(name: string): () => void; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +name + + + + +string + + + + + +
+**Returns:** + +() => void + diff --git a/sdk/docs/server.performancemonitorevent.md b/sdk/docs/server.performancemonitorevent.md new file mode 100644 index 00000000..44e68ece --- /dev/null +++ b/sdk/docs/server.performancemonitorevent.md @@ -0,0 +1,73 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitorEvent](./server.performancemonitorevent.md) + +## PerformanceMonitorEvent enum + +**Signature:** + +```typescript +export declare enum PerformanceMonitorEvent +``` + +## Enumeration Members + + + + + +
+ +Member + + + + +Value + + + + +Description + + +
+ +SNAPSHOT + + + + +`"PERFORMANCE_MONITOR.SNAPSHOT"` + + + + + +
+ +SPIKE\_DETECTED + + + + +`"PERFORMANCE_MONITOR.SPIKE_DETECTED"` + + + + + +
+ +TICK\_REPORT + + + + +`"PERFORMANCE_MONITOR.TICK_REPORT"` + + + + + +
diff --git a/sdk/docs/server.performancemonitoreventpayloads._performance_monitor.snapshot_.md b/sdk/docs/server.performancemonitoreventpayloads._performance_monitor.snapshot_.md new file mode 100644 index 00000000..75b35921 --- /dev/null +++ b/sdk/docs/server.performancemonitoreventpayloads._performance_monitor.snapshot_.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitorEventPayloads](./server.performancemonitoreventpayloads.md) > ["PERFORMANCE\_MONITOR.SNAPSHOT"](./server.performancemonitoreventpayloads._performance_monitor.snapshot_.md) + +## PerformanceMonitorEventPayloads."PERFORMANCE\_MONITOR.SNAPSHOT" property + +**Signature:** + +```typescript +[PerformanceMonitorEvent.SNAPSHOT]: PerformanceSnapshot; +``` diff --git a/sdk/docs/server.performancemonitoreventpayloads._performance_monitor.spike_detected_.md b/sdk/docs/server.performancemonitoreventpayloads._performance_monitor.spike_detected_.md new file mode 100644 index 00000000..f02c9203 --- /dev/null +++ b/sdk/docs/server.performancemonitoreventpayloads._performance_monitor.spike_detected_.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitorEventPayloads](./server.performancemonitoreventpayloads.md) > ["PERFORMANCE\_MONITOR.SPIKE\_DETECTED"](./server.performancemonitoreventpayloads._performance_monitor.spike_detected_.md) + +## PerformanceMonitorEventPayloads."PERFORMANCE\_MONITOR.SPIKE\_DETECTED" property + +**Signature:** + +```typescript +[PerformanceMonitorEvent.SPIKE_DETECTED]: TickReport; +``` diff --git a/sdk/docs/server.performancemonitoreventpayloads._performance_monitor.tick_report_.md b/sdk/docs/server.performancemonitoreventpayloads._performance_monitor.tick_report_.md new file mode 100644 index 00000000..d3a9bcfc --- /dev/null +++ b/sdk/docs/server.performancemonitoreventpayloads._performance_monitor.tick_report_.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitorEventPayloads](./server.performancemonitoreventpayloads.md) > ["PERFORMANCE\_MONITOR.TICK\_REPORT"](./server.performancemonitoreventpayloads._performance_monitor.tick_report_.md) + +## PerformanceMonitorEventPayloads."PERFORMANCE\_MONITOR.TICK\_REPORT" property + +**Signature:** + +```typescript +[PerformanceMonitorEvent.TICK_REPORT]: TickReport; +``` diff --git a/sdk/docs/server.performancemonitoreventpayloads.md b/sdk/docs/server.performancemonitoreventpayloads.md new file mode 100644 index 00000000..e0218291 --- /dev/null +++ b/sdk/docs/server.performancemonitoreventpayloads.md @@ -0,0 +1,87 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitorEventPayloads](./server.performancemonitoreventpayloads.md) + +## PerformanceMonitorEventPayloads interface + +**Signature:** + +```typescript +export interface PerformanceMonitorEventPayloads +``` + +## Properties + + + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +["PERFORMANCE\_MONITOR.SNAPSHOT"](./server.performancemonitoreventpayloads._performance_monitor.snapshot_.md) + + + + + + + +[PerformanceSnapshot](./server.performancesnapshot.md) + + + + + +
+ +["PERFORMANCE\_MONITOR.SPIKE\_DETECTED"](./server.performancemonitoreventpayloads._performance_monitor.spike_detected_.md) + + + + + + + +[TickReport](./server.tickreport.md) + + + + + +
+ +["PERFORMANCE\_MONITOR.TICK\_REPORT"](./server.performancemonitoreventpayloads._performance_monitor.tick_report_.md) + + + + + + + +[TickReport](./server.tickreport.md) + + + + + +
diff --git a/sdk/docs/server.performancemonitoroptions.historysize.md b/sdk/docs/server.performancemonitoroptions.historysize.md new file mode 100644 index 00000000..d136d55e --- /dev/null +++ b/sdk/docs/server.performancemonitoroptions.historysize.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitorOptions](./server.performancemonitoroptions.md) > [historySize](./server.performancemonitoroptions.historysize.md) + +## PerformanceMonitorOptions.historySize property + +**Signature:** + +```typescript +historySize?: number; +``` diff --git a/sdk/docs/server.performancemonitoroptions.md b/sdk/docs/server.performancemonitoroptions.md new file mode 100644 index 00000000..a98af08f --- /dev/null +++ b/sdk/docs/server.performancemonitoroptions.md @@ -0,0 +1,112 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitorOptions](./server.performancemonitoroptions.md) + +## PerformanceMonitorOptions interface + +**Signature:** + +```typescript +export interface PerformanceMonitorOptions +``` + +## Properties + + + + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[historySize?](./server.performancemonitoroptions.historysize.md) + + + + + + + +number + + + + +_(Optional)_ + + +
+ +[snapshotIntervalMs?](./server.performancemonitoroptions.snapshotintervalms.md) + + + + + + + +number + + + + +_(Optional)_ + + +
+ +[spikeThresholdMs?](./server.performancemonitoroptions.spikethresholdms.md) + + + + + + + +number + + + + +_(Optional)_ + + +
+ +[tickBudgetMs?](./server.performancemonitoroptions.tickbudgetms.md) + + + + + + + +number + + + + +_(Optional)_ + + +
diff --git a/sdk/docs/server.performancemonitoroptions.snapshotintervalms.md b/sdk/docs/server.performancemonitoroptions.snapshotintervalms.md new file mode 100644 index 00000000..8e85ade8 --- /dev/null +++ b/sdk/docs/server.performancemonitoroptions.snapshotintervalms.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitorOptions](./server.performancemonitoroptions.md) > [snapshotIntervalMs](./server.performancemonitoroptions.snapshotintervalms.md) + +## PerformanceMonitorOptions.snapshotIntervalMs property + +**Signature:** + +```typescript +snapshotIntervalMs?: number; +``` diff --git a/sdk/docs/server.performancemonitoroptions.spikethresholdms.md b/sdk/docs/server.performancemonitoroptions.spikethresholdms.md new file mode 100644 index 00000000..42756b06 --- /dev/null +++ b/sdk/docs/server.performancemonitoroptions.spikethresholdms.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitorOptions](./server.performancemonitoroptions.md) > [spikeThresholdMs](./server.performancemonitoroptions.spikethresholdms.md) + +## PerformanceMonitorOptions.spikeThresholdMs property + +**Signature:** + +```typescript +spikeThresholdMs?: number; +``` diff --git a/sdk/docs/server.performancemonitoroptions.tickbudgetms.md b/sdk/docs/server.performancemonitoroptions.tickbudgetms.md new file mode 100644 index 00000000..15acd2fb --- /dev/null +++ b/sdk/docs/server.performancemonitoroptions.tickbudgetms.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceMonitorOptions](./server.performancemonitoroptions.md) > [tickBudgetMs](./server.performancemonitoroptions.tickbudgetms.md) + +## PerformanceMonitorOptions.tickBudgetMs property + +**Signature:** + +```typescript +tickBudgetMs?: number; +``` diff --git a/sdk/docs/server.performancesnapshot.avgtickms.md b/sdk/docs/server.performancesnapshot.avgtickms.md new file mode 100644 index 00000000..6ef84ae7 --- /dev/null +++ b/sdk/docs/server.performancesnapshot.avgtickms.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceSnapshot](./server.performancesnapshot.md) > [avgTickMs](./server.performancesnapshot.avgtickms.md) + +## PerformanceSnapshot.avgTickMs property + +**Signature:** + +```typescript +avgTickMs: number; +``` diff --git a/sdk/docs/server.performancesnapshot.budgetms.md b/sdk/docs/server.performancesnapshot.budgetms.md new file mode 100644 index 00000000..4b405481 --- /dev/null +++ b/sdk/docs/server.performancesnapshot.budgetms.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceSnapshot](./server.performancesnapshot.md) > [budgetMs](./server.performancesnapshot.budgetms.md) + +## PerformanceSnapshot.budgetMs property + +**Signature:** + +```typescript +budgetMs: number; +``` diff --git a/sdk/docs/server.performancesnapshot.maxtickms.md b/sdk/docs/server.performancesnapshot.maxtickms.md new file mode 100644 index 00000000..2d8d420e --- /dev/null +++ b/sdk/docs/server.performancesnapshot.maxtickms.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceSnapshot](./server.performancesnapshot.md) > [maxTickMs](./server.performancesnapshot.maxtickms.md) + +## PerformanceSnapshot.maxTickMs property + +**Signature:** + +```typescript +maxTickMs: number; +``` diff --git a/sdk/docs/server.performancesnapshot.md b/sdk/docs/server.performancesnapshot.md new file mode 100644 index 00000000..6712ec4a --- /dev/null +++ b/sdk/docs/server.performancesnapshot.md @@ -0,0 +1,223 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceSnapshot](./server.performancesnapshot.md) + +## PerformanceSnapshot interface + +**Signature:** + +```typescript +export interface PerformanceSnapshot +``` + +## Properties + + + + + + + + + + + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[avgTickMs](./server.performancesnapshot.avgtickms.md) + + + + + + + +number + + + + + +
+ +[budgetMs](./server.performancesnapshot.budgetms.md) + + + + + + + +number + + + + + +
+ +[maxTickMs](./server.performancesnapshot.maxtickms.md) + + + + + + + +number + + + + + +
+ +[memory](./server.performancesnapshot.memory.md) + + + + + + + +{ heapUsedMb: number; heapTotalMb: number; rssMb: number; } + + + + + +
+ +[operations](./server.performancesnapshot.operations.md) + + + + + + + +Record<string, [OperationStats](./server.operationstats.md)> + + + + + +
+ +[p95TickMs](./server.performancesnapshot.p95tickms.md) + + + + + + + +number + + + + + +
+ +[p99TickMs](./server.performancesnapshot.p99tickms.md) + + + + + + + +number + + + + + +
+ +[tickRate](./server.performancesnapshot.tickrate.md) + + + + + + + +number + + + + + +
+ +[ticksOverBudget](./server.performancesnapshot.ticksoverbudget.md) + + + + + + + +number + + + + + +
+ +[totalTicks](./server.performancesnapshot.totalticks.md) + + + + + + + +number + + + + + +
+ +[uptimeMs](./server.performancesnapshot.uptimems.md) + + + + + + + +number + + + + + +
diff --git a/sdk/docs/server.performancesnapshot.memory.md b/sdk/docs/server.performancesnapshot.memory.md new file mode 100644 index 00000000..1a155f40 --- /dev/null +++ b/sdk/docs/server.performancesnapshot.memory.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceSnapshot](./server.performancesnapshot.md) > [memory](./server.performancesnapshot.memory.md) + +## PerformanceSnapshot.memory property + +**Signature:** + +```typescript +memory: { + heapUsedMb: number; + heapTotalMb: number; + rssMb: number; + }; +``` diff --git a/sdk/docs/server.performancesnapshot.operations.md b/sdk/docs/server.performancesnapshot.operations.md new file mode 100644 index 00000000..97ad7afa --- /dev/null +++ b/sdk/docs/server.performancesnapshot.operations.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceSnapshot](./server.performancesnapshot.md) > [operations](./server.performancesnapshot.operations.md) + +## PerformanceSnapshot.operations property + +**Signature:** + +```typescript +operations: Record; +``` diff --git a/sdk/docs/server.performancesnapshot.p95tickms.md b/sdk/docs/server.performancesnapshot.p95tickms.md new file mode 100644 index 00000000..7b90852f --- /dev/null +++ b/sdk/docs/server.performancesnapshot.p95tickms.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceSnapshot](./server.performancesnapshot.md) > [p95TickMs](./server.performancesnapshot.p95tickms.md) + +## PerformanceSnapshot.p95TickMs property + +**Signature:** + +```typescript +p95TickMs: number; +``` diff --git a/sdk/docs/server.performancesnapshot.p99tickms.md b/sdk/docs/server.performancesnapshot.p99tickms.md new file mode 100644 index 00000000..a3e298e9 --- /dev/null +++ b/sdk/docs/server.performancesnapshot.p99tickms.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceSnapshot](./server.performancesnapshot.md) > [p99TickMs](./server.performancesnapshot.p99tickms.md) + +## PerformanceSnapshot.p99TickMs property + +**Signature:** + +```typescript +p99TickMs: number; +``` diff --git a/sdk/docs/server.performancesnapshot.tickrate.md b/sdk/docs/server.performancesnapshot.tickrate.md new file mode 100644 index 00000000..e0917761 --- /dev/null +++ b/sdk/docs/server.performancesnapshot.tickrate.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceSnapshot](./server.performancesnapshot.md) > [tickRate](./server.performancesnapshot.tickrate.md) + +## PerformanceSnapshot.tickRate property + +**Signature:** + +```typescript +tickRate: number; +``` diff --git a/sdk/docs/server.performancesnapshot.ticksoverbudget.md b/sdk/docs/server.performancesnapshot.ticksoverbudget.md new file mode 100644 index 00000000..62de23a3 --- /dev/null +++ b/sdk/docs/server.performancesnapshot.ticksoverbudget.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceSnapshot](./server.performancesnapshot.md) > [ticksOverBudget](./server.performancesnapshot.ticksoverbudget.md) + +## PerformanceSnapshot.ticksOverBudget property + +**Signature:** + +```typescript +ticksOverBudget: number; +``` diff --git a/sdk/docs/server.performancesnapshot.totalticks.md b/sdk/docs/server.performancesnapshot.totalticks.md new file mode 100644 index 00000000..d69f6197 --- /dev/null +++ b/sdk/docs/server.performancesnapshot.totalticks.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceSnapshot](./server.performancesnapshot.md) > [totalTicks](./server.performancesnapshot.totalticks.md) + +## PerformanceSnapshot.totalTicks property + +**Signature:** + +```typescript +totalTicks: number; +``` diff --git a/sdk/docs/server.performancesnapshot.uptimems.md b/sdk/docs/server.performancesnapshot.uptimems.md new file mode 100644 index 00000000..fa9b7cc8 --- /dev/null +++ b/sdk/docs/server.performancesnapshot.uptimems.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [PerformanceSnapshot](./server.performancesnapshot.md) > [uptimeMs](./server.performancesnapshot.uptimems.md) + +## PerformanceSnapshot.uptimeMs property + +**Signature:** + +```typescript +uptimeMs: number; +``` diff --git a/sdk/docs/server.randomwalkbehavior._constructor_.md b/sdk/docs/server.randomwalkbehavior._constructor_.md new file mode 100644 index 00000000..e8804cdb --- /dev/null +++ b/sdk/docs/server.randomwalkbehavior._constructor_.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [server](./server.md) > [RandomWalkBehavior](./server.randomwalkbehavior.md) > [(constructor)](./server.randomwalkbehavior._constructor_.md) + +## RandomWalkBehavior.(constructor) + +Constructs a new instance of the `RandomWalkBehavior` class + +**Signature:** + +```typescript +constructor(options?: RandomWalkOptions); +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +options + + + + +[RandomWalkOptions](./server.randomwalkoptions.md) + + + + +_(Optional)_ + + +
diff --git a/sdk/docs/server.randomwalkbehavior.md b/sdk/docs/server.randomwalkbehavior.md new file mode 100644 index 00000000..5280047f --- /dev/null +++ b/sdk/docs/server.randomwalkbehavior.md @@ -0,0 +1,122 @@ + + +[Home](./index.md) > [server](./server.md) > [RandomWalkBehavior](./server.randomwalkbehavior.md) + +## RandomWalkBehavior class + +**Signature:** + +```typescript +export default class RandomWalkBehavior implements BotBehavior +``` +**Implements:** [BotBehavior](./server.botbehavior.md) + +## Constructors + + + +
+ +Constructor + + + + +Modifiers + + + + +Description + + +
+ +[(constructor)(options)](./server.randomwalkbehavior._constructor_.md) + + + + + + + +Constructs a new instance of the `RandomWalkBehavior` class + + +
+ +## Properties + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[name](./server.randomwalkbehavior.name.md) + + + + +`readonly` + + + + +(not declared) + + + + + +
+ +## Methods + + + +
+ +Method + + + + +Modifiers + + + + +Description + + +
+ +[tick(bot, \_world, deltaTimeMs)](./server.randomwalkbehavior.tick.md) + + + + + + + + +
diff --git a/sdk/docs/server.randomwalkbehavior.name.md b/sdk/docs/server.randomwalkbehavior.name.md new file mode 100644 index 00000000..dc6df1e4 --- /dev/null +++ b/sdk/docs/server.randomwalkbehavior.name.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [RandomWalkBehavior](./server.randomwalkbehavior.md) > [name](./server.randomwalkbehavior.name.md) + +## RandomWalkBehavior.name property + +**Signature:** + +```typescript +readonly name = "random_walk"; +``` diff --git a/sdk/docs/server.randomwalkbehavior.tick.md b/sdk/docs/server.randomwalkbehavior.tick.md new file mode 100644 index 00000000..23c2afa4 --- /dev/null +++ b/sdk/docs/server.randomwalkbehavior.tick.md @@ -0,0 +1,77 @@ + + +[Home](./index.md) > [server](./server.md) > [RandomWalkBehavior](./server.randomwalkbehavior.md) > [tick](./server.randomwalkbehavior.tick.md) + +## RandomWalkBehavior.tick() method + +**Signature:** + +```typescript +tick(bot: BotPlayer, _world: World, deltaTimeMs: number): void; +``` + +## Parameters + + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +bot + + + + +[BotPlayer](./server.botplayer.md) + + + + + +
+ +\_world + + + + +[World](./server.world.md) + + + + + +
+ +deltaTimeMs + + + + +number + + + + + +
+**Returns:** + +void + diff --git a/sdk/docs/server.randomwalkoptions.changedirectionintervalms.md b/sdk/docs/server.randomwalkoptions.changedirectionintervalms.md new file mode 100644 index 00000000..2d2dc147 --- /dev/null +++ b/sdk/docs/server.randomwalkoptions.changedirectionintervalms.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [RandomWalkOptions](./server.randomwalkoptions.md) > [changeDirectionIntervalMs](./server.randomwalkoptions.changedirectionintervalms.md) + +## RandomWalkOptions.changeDirectionIntervalMs property + +**Signature:** + +```typescript +changeDirectionIntervalMs?: number; +``` diff --git a/sdk/docs/server.randomwalkoptions.md b/sdk/docs/server.randomwalkoptions.md new file mode 100644 index 00000000..abe61a2a --- /dev/null +++ b/sdk/docs/server.randomwalkoptions.md @@ -0,0 +1,93 @@ + + +[Home](./index.md) > [server](./server.md) > [RandomWalkOptions](./server.randomwalkoptions.md) + +## RandomWalkOptions interface + +**Signature:** + +```typescript +export interface RandomWalkOptions +``` + +## Properties + + + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[changeDirectionIntervalMs?](./server.randomwalkoptions.changedirectionintervalms.md) + + + + + + + +number + + + + +_(Optional)_ + + +
+ +[moveRadius?](./server.randomwalkoptions.moveradius.md) + + + + + + + +number + + + + +_(Optional)_ + + +
+ +[moveSpeed?](./server.randomwalkoptions.movespeed.md) + + + + + + + +number + + + + +_(Optional)_ + + +
diff --git a/sdk/docs/server.randomwalkoptions.moveradius.md b/sdk/docs/server.randomwalkoptions.moveradius.md new file mode 100644 index 00000000..800d6640 --- /dev/null +++ b/sdk/docs/server.randomwalkoptions.moveradius.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [RandomWalkOptions](./server.randomwalkoptions.md) > [moveRadius](./server.randomwalkoptions.moveradius.md) + +## RandomWalkOptions.moveRadius property + +**Signature:** + +```typescript +moveRadius?: number; +``` diff --git a/sdk/docs/server.randomwalkoptions.movespeed.md b/sdk/docs/server.randomwalkoptions.movespeed.md new file mode 100644 index 00000000..fbafc3a5 --- /dev/null +++ b/sdk/docs/server.randomwalkoptions.movespeed.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [RandomWalkOptions](./server.randomwalkoptions.md) > [moveSpeed](./server.randomwalkoptions.movespeed.md) + +## RandomWalkOptions.moveSpeed property + +**Signature:** + +```typescript +moveSpeed?: number; +``` diff --git a/sdk/docs/server.tickreport.budgetms.md b/sdk/docs/server.tickreport.budgetms.md new file mode 100644 index 00000000..8ed95793 --- /dev/null +++ b/sdk/docs/server.tickreport.budgetms.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [TickReport](./server.tickreport.md) > [budgetMs](./server.tickreport.budgetms.md) + +## TickReport.budgetMs property + +**Signature:** + +```typescript +budgetMs: number; +``` diff --git a/sdk/docs/server.tickreport.budgetpercent.md b/sdk/docs/server.tickreport.budgetpercent.md new file mode 100644 index 00000000..6b56cc33 --- /dev/null +++ b/sdk/docs/server.tickreport.budgetpercent.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [TickReport](./server.tickreport.md) > [budgetPercent](./server.tickreport.budgetpercent.md) + +## TickReport.budgetPercent property + +**Signature:** + +```typescript +budgetPercent: number; +``` diff --git a/sdk/docs/server.tickreport.durationms.md b/sdk/docs/server.tickreport.durationms.md new file mode 100644 index 00000000..6606d995 --- /dev/null +++ b/sdk/docs/server.tickreport.durationms.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [TickReport](./server.tickreport.md) > [durationMs](./server.tickreport.durationms.md) + +## TickReport.durationMs property + +**Signature:** + +```typescript +durationMs: number; +``` diff --git a/sdk/docs/server.tickreport.entitycount.md b/sdk/docs/server.tickreport.entitycount.md new file mode 100644 index 00000000..d5eddcfa --- /dev/null +++ b/sdk/docs/server.tickreport.entitycount.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [TickReport](./server.tickreport.md) > [entityCount](./server.tickreport.entitycount.md) + +## TickReport.entityCount property + +**Signature:** + +```typescript +entityCount: number; +``` diff --git a/sdk/docs/server.tickreport.heapusedmb.md b/sdk/docs/server.tickreport.heapusedmb.md new file mode 100644 index 00000000..429f20b0 --- /dev/null +++ b/sdk/docs/server.tickreport.heapusedmb.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [TickReport](./server.tickreport.md) > [heapUsedMb](./server.tickreport.heapusedmb.md) + +## TickReport.heapUsedMb property + +**Signature:** + +```typescript +heapUsedMb: number; +``` diff --git a/sdk/docs/server.tickreport.md b/sdk/docs/server.tickreport.md new file mode 100644 index 00000000..f8084e12 --- /dev/null +++ b/sdk/docs/server.tickreport.md @@ -0,0 +1,189 @@ + + +[Home](./index.md) > [server](./server.md) > [TickReport](./server.tickreport.md) + +## TickReport interface + +**Signature:** + +```typescript +export interface TickReport +``` + +## Properties + + + + + + + + + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[budgetMs](./server.tickreport.budgetms.md) + + + + + + + +number + + + + + +
+ +[budgetPercent](./server.tickreport.budgetpercent.md) + + + + + + + +number + + + + + +
+ +[durationMs](./server.tickreport.durationms.md) + + + + + + + +number + + + + + +
+ +[entityCount](./server.tickreport.entitycount.md) + + + + + + + +number + + + + + +
+ +[heapUsedMb](./server.tickreport.heapusedmb.md) + + + + + + + +number + + + + + +
+ +[phases](./server.tickreport.phases.md) + + + + + + + +Record<string, number> + + + + + +
+ +[playerCount](./server.tickreport.playercount.md) + + + + + + + +number + + + + + +
+ +[tick](./server.tickreport.tick.md) + + + + + + + +number + + + + + +
+ +[worldId](./server.tickreport.worldid.md) + + + + + + + +number + + + + + +
diff --git a/sdk/docs/server.tickreport.phases.md b/sdk/docs/server.tickreport.phases.md new file mode 100644 index 00000000..7cf7a2d8 --- /dev/null +++ b/sdk/docs/server.tickreport.phases.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [TickReport](./server.tickreport.md) > [phases](./server.tickreport.phases.md) + +## TickReport.phases property + +**Signature:** + +```typescript +phases: Record; +``` diff --git a/sdk/docs/server.tickreport.playercount.md b/sdk/docs/server.tickreport.playercount.md new file mode 100644 index 00000000..0891958b --- /dev/null +++ b/sdk/docs/server.tickreport.playercount.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [TickReport](./server.tickreport.md) > [playerCount](./server.tickreport.playercount.md) + +## TickReport.playerCount property + +**Signature:** + +```typescript +playerCount: number; +``` diff --git a/sdk/docs/server.tickreport.tick.md b/sdk/docs/server.tickreport.tick.md new file mode 100644 index 00000000..6ff6cb8a --- /dev/null +++ b/sdk/docs/server.tickreport.tick.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [TickReport](./server.tickreport.md) > [tick](./server.tickreport.tick.md) + +## TickReport.tick property + +**Signature:** + +```typescript +tick: number; +``` diff --git a/sdk/docs/server.tickreport.worldid.md b/sdk/docs/server.tickreport.worldid.md new file mode 100644 index 00000000..043e2e90 --- /dev/null +++ b/sdk/docs/server.tickreport.worldid.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [TickReport](./server.tickreport.md) > [worldId](./server.tickreport.worldid.md) + +## TickReport.worldId property + +**Signature:** + +```typescript +worldId: number; +``` diff --git a/sdk/docs/server.world.loadmap.md b/sdk/docs/server.world.loadmap.md index 7bdf2333..249ddc06 100644 --- a/sdk/docs/server.world.loadmap.md +++ b/sdk/docs/server.world.loadmap.md @@ -11,7 +11,10 @@ Use for: initializing or fully resetting a world from serialized map data. Do NO **Signature:** ```typescript -loadMap(map: WorldMap): void; +loadMap(map: WorldMap | CompressedWorldMap | WorldMapChunkCache | string, options?: { + spawnEntities?: boolean; + preferMapArtifacts?: boolean; + }): void; ``` ## Parameters @@ -39,18 +42,34 @@ map -[WorldMap](./server.worldmap.md) +[WorldMap](./server.worldmap.md) \| [CompressedWorldMap](./server.compressedworldmap.md) \| [WorldMapChunkCache](./server.worldmapchunkcache.md) \| string -The map to load. +The map to load. Can be a map object (WorldMap, CompressedWorldMap, WorldMapChunkCache) or a string file path. When a string is provided, WorldMapFileLoader auto-detects the best available format. \*\*Side effects:\*\* Clears the chunk lattice, registers block types, and spawns entities. \*\*Category:\*\* Core + + + +options + + + + +{ spawnEntities?: boolean; preferMapArtifacts?: boolean; } + + + + +_(Optional)_ + + **Returns:** diff --git a/sdk/docs/server.world.md b/sdk/docs/server.world.md index e9016bed..685847dc 100644 --- a/sdk/docs/server.world.md +++ b/sdk/docs/server.world.md @@ -623,7 +623,7 @@ Description -[loadMap(map)](./server.world.loadmap.md) +[loadMap(map, options)](./server.world.loadmap.md) diff --git a/sdk/docs/server.worldmapartifacts.md b/sdk/docs/server.worldmapartifacts.md new file mode 100644 index 00000000..6c44f8cb --- /dev/null +++ b/sdk/docs/server.worldmapartifacts.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapArtifacts](./server.worldmapartifacts.md) + +## WorldMapArtifacts type + +**Signature:** + +```typescript +export type WorldMapArtifacts = { + compressedMap: CompressedWorldMap; + compressedMapJson: string; + compressedMapSha256: string; + chunkCache: WorldMapChunkCache; + chunkCacheBuffer: Buffer; +}; +``` +**References:** [CompressedWorldMap](./server.compressedworldmap.md), [WorldMapChunkCache](./server.worldmapchunkcache.md) + diff --git a/sdk/docs/server.worldmapartifactsgenerator.create.md b/sdk/docs/server.worldmapartifactsgenerator.create.md new file mode 100644 index 00000000..ee54a311 --- /dev/null +++ b/sdk/docs/server.worldmapartifactsgenerator.create.md @@ -0,0 +1,68 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapArtifactsGenerator](./server.worldmapartifactsgenerator.md) > [create](./server.worldmapartifactsgenerator.create.md) + +## WorldMapArtifactsGenerator.create() method + +**Signature:** + +```typescript +static create(worldMap: WorldMap, options?: { + compressed?: CompressWorldMapOptions; + chunkCache?: Omit; + }): WorldMapArtifacts; +``` + +## Parameters + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +worldMap + + + + +[WorldMap](./server.worldmap.md) + + + + + +
+ +options + + + + +{ compressed?: [CompressWorldMapOptions](./server.compressworldmapoptions.md); chunkCache?: Omit<[CreateWorldMapChunkCacheOptions](./server.createworldmapchunkcacheoptions.md), 'sourceSha256'>; } + + + + +_(Optional)_ + + +
+**Returns:** + +[WorldMapArtifacts](./server.worldmapartifacts.md) + diff --git a/sdk/docs/server.worldmapartifactsgenerator.md b/sdk/docs/server.worldmapartifactsgenerator.md new file mode 100644 index 00000000..2c0466db --- /dev/null +++ b/sdk/docs/server.worldmapartifactsgenerator.md @@ -0,0 +1,45 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapArtifactsGenerator](./server.worldmapartifactsgenerator.md) + +## WorldMapArtifactsGenerator class + +**Signature:** + +```typescript +export default class WorldMapArtifactsGenerator +``` + +## Methods + + + +
+ +Method + + + + +Modifiers + + + + +Description + + +
+ +[create(worldMap, options)](./server.worldmapartifactsgenerator.create.md) + + + + +`static` + + + + + +
diff --git a/sdk/docs/server.worldmapchunkcache.algorithm.md b/sdk/docs/server.worldmapchunkcache.algorithm.md new file mode 100644 index 00000000..30ae32d6 --- /dev/null +++ b/sdk/docs/server.worldmapchunkcache.algorithm.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCache](./server.worldmapchunkcache.md) > [algorithm](./server.worldmapchunkcache.algorithm.md) + +## WorldMapChunkCache.algorithm property + +**Signature:** + +```typescript +algorithm?: WorldMapChunkCacheAlgorithm; +``` diff --git a/sdk/docs/server.worldmapchunkcache.blocktypes.md b/sdk/docs/server.worldmapchunkcache.blocktypes.md new file mode 100644 index 00000000..594eb988 --- /dev/null +++ b/sdk/docs/server.worldmapchunkcache.blocktypes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCache](./server.worldmapchunkcache.md) > [blockTypes](./server.worldmapchunkcache.blocktypes.md) + +## WorldMapChunkCache.blockTypes property + +**Signature:** + +```typescript +blockTypes?: BlockTypeOptions[] | Record; +``` diff --git a/sdk/docs/server.worldmapchunkcache.codecversion.md b/sdk/docs/server.worldmapchunkcache.codecversion.md new file mode 100644 index 00000000..9d31567e --- /dev/null +++ b/sdk/docs/server.worldmapchunkcache.codecversion.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCache](./server.worldmapchunkcache.md) > [codecVersion](./server.worldmapchunkcache.codecversion.md) + +## WorldMapChunkCache.codecVersion property + +**Signature:** + +```typescript +codecVersion?: number; +``` diff --git a/sdk/docs/server.worldmapchunkcache.data.md b/sdk/docs/server.worldmapchunkcache.data.md new file mode 100644 index 00000000..c7f24d07 --- /dev/null +++ b/sdk/docs/server.worldmapchunkcache.data.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCache](./server.worldmapchunkcache.md) > [data](./server.worldmapchunkcache.data.md) + +## WorldMapChunkCache.data property + +**Signature:** + +```typescript +data: string; +``` diff --git a/sdk/docs/server.worldmapchunkcache.entities.md b/sdk/docs/server.worldmapchunkcache.entities.md new file mode 100644 index 00000000..e0f673ee --- /dev/null +++ b/sdk/docs/server.worldmapchunkcache.entities.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCache](./server.worldmapchunkcache.md) > [entities](./server.worldmapchunkcache.entities.md) + +## WorldMapChunkCache.entities property + +**Signature:** + +```typescript +entities?: WorldMap['entities']; +``` diff --git a/sdk/docs/server.worldmapchunkcache.format.md b/sdk/docs/server.worldmapchunkcache.format.md new file mode 100644 index 00000000..7e026c7e --- /dev/null +++ b/sdk/docs/server.worldmapchunkcache.format.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCache](./server.worldmapchunkcache.md) > [format](./server.worldmapchunkcache.format.md) + +## WorldMapChunkCache.format property + +**Signature:** + +```typescript +format?: 'hytopia.worldmap.chunk-cache'; +``` diff --git a/sdk/docs/server.worldmapchunkcache.md b/sdk/docs/server.worldmapchunkcache.md new file mode 100644 index 00000000..89c78ddf --- /dev/null +++ b/sdk/docs/server.worldmapchunkcache.md @@ -0,0 +1,167 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCache](./server.worldmapchunkcache.md) + +## WorldMapChunkCache interface + +**Signature:** + +```typescript +export interface WorldMapChunkCache +``` + +## Properties + + + + + + + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[algorithm?](./server.worldmapchunkcache.algorithm.md) + + + + + + + +[WorldMapChunkCacheAlgorithm](./server.worldmapchunkcachealgorithm.md) + + + + +_(Optional)_ + + +
+ +[blockTypes?](./server.worldmapchunkcache.blocktypes.md) + + + + + + + +[BlockTypeOptions](./server.blocktypeoptions.md)\[\] \| Record<string, [BlockTypeOptions](./server.blocktypeoptions.md)> + + + + +_(Optional)_ + + +
+ +[codecVersion?](./server.worldmapchunkcache.codecversion.md) + + + + + + + +number + + + + +_(Optional)_ + + +
+ +[data](./server.worldmapchunkcache.data.md) + + + + + + + +string + + + + + +
+ +[entities?](./server.worldmapchunkcache.entities.md) + + + + + + + +[WorldMap](./server.worldmap.md)\['entities'\] + + + + +_(Optional)_ + + +
+ +[format?](./server.worldmapchunkcache.format.md) + + + + + + + +'hytopia.worldmap.chunk-cache' + + + + +_(Optional)_ + + +
+ +[version?](./server.worldmapchunkcache.version.md) + + + + + + + +string + + + + +_(Optional)_ + + +
diff --git a/sdk/docs/server.worldmapchunkcache.version.md b/sdk/docs/server.worldmapchunkcache.version.md new file mode 100644 index 00000000..cacefed9 --- /dev/null +++ b/sdk/docs/server.worldmapchunkcache.version.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCache](./server.worldmapchunkcache.md) > [version](./server.worldmapchunkcache.version.md) + +## WorldMapChunkCache.version property + +**Signature:** + +```typescript +version?: string; +``` diff --git a/sdk/docs/server.worldmapchunkcachealgorithm.md b/sdk/docs/server.worldmapchunkcachealgorithm.md new file mode 100644 index 00000000..c95568a1 --- /dev/null +++ b/sdk/docs/server.worldmapchunkcachealgorithm.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCacheAlgorithm](./server.worldmapchunkcachealgorithm.md) + +## WorldMapChunkCacheAlgorithm type + +**Signature:** + +```typescript +export type WorldMapChunkCacheAlgorithm = 'brotli' | 'gzip' | 'none'; +``` diff --git a/sdk/docs/server.worldmapchunkcachecodec.create.md b/sdk/docs/server.worldmapchunkcachecodec.create.md new file mode 100644 index 00000000..62920c9c --- /dev/null +++ b/sdk/docs/server.worldmapchunkcachecodec.create.md @@ -0,0 +1,65 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCacheCodec](./server.worldmapchunkcachecodec.md) > [create](./server.worldmapchunkcachecodec.create.md) + +## WorldMapChunkCacheCodec.create() method + +**Signature:** + +```typescript +static create(map: WorldMap | CompressedWorldMap, options?: CreateWorldMapChunkCacheOptions): WorldMapChunkCache; +``` + +## Parameters + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +map + + + + +[WorldMap](./server.worldmap.md) \| [CompressedWorldMap](./server.compressedworldmap.md) + + + + + +
+ +options + + + + +[CreateWorldMapChunkCacheOptions](./server.createworldmapchunkcacheoptions.md) + + + + +_(Optional)_ + + +
+**Returns:** + +[WorldMapChunkCache](./server.worldmapchunkcache.md) + diff --git a/sdk/docs/server.worldmapchunkcachecodec.decode.md b/sdk/docs/server.worldmapchunkcachecodec.decode.md new file mode 100644 index 00000000..0492d0b4 --- /dev/null +++ b/sdk/docs/server.worldmapchunkcachecodec.decode.md @@ -0,0 +1,52 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCacheCodec](./server.worldmapchunkcachecodec.md) > [decode](./server.worldmapchunkcachecodec.decode.md) + +## WorldMapChunkCacheCodec.decode() method + +**Signature:** + +```typescript +static decode(cache: WorldMapChunkCache): { + metadata: WorldMapChunkCacheMetadata; + chunks: Iterable; + }; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +cache + + + + +[WorldMapChunkCache](./server.worldmapchunkcache.md) + + + + + +
+**Returns:** + +{ metadata: [WorldMapChunkCacheMetadata](./server.worldmapchunkcachemetadata.md); chunks: Iterable<ChunkCacheChunk>; } + diff --git a/sdk/docs/server.worldmapchunkcachecodec.decodechunks.md b/sdk/docs/server.worldmapchunkcachecodec.decodechunks.md new file mode 100644 index 00000000..9bfee60e --- /dev/null +++ b/sdk/docs/server.worldmapchunkcachecodec.decodechunks.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCacheCodec](./server.worldmapchunkcachecodec.md) > [decodeChunks](./server.worldmapchunkcachecodec.decodechunks.md) + +## WorldMapChunkCacheCodec.decodeChunks() method + +**Signature:** + +```typescript +static decodeChunks(cache: WorldMapChunkCache): Iterable; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +cache + + + + +[WorldMapChunkCache](./server.worldmapchunkcache.md) + + + + + +
+**Returns:** + +Iterable<ChunkCacheChunk> + diff --git a/sdk/docs/server.worldmapchunkcachecodec.decodemetadata.md b/sdk/docs/server.worldmapchunkcachecodec.decodemetadata.md new file mode 100644 index 00000000..3abbd91c --- /dev/null +++ b/sdk/docs/server.worldmapchunkcachecodec.decodemetadata.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCacheCodec](./server.worldmapchunkcachecodec.md) > [decodeMetadata](./server.worldmapchunkcachecodec.decodemetadata.md) + +## WorldMapChunkCacheCodec.decodeMetadata() method + +**Signature:** + +```typescript +static decodeMetadata(cache: WorldMapChunkCache): WorldMapChunkCacheMetadata; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +cache + + + + +[WorldMapChunkCache](./server.worldmapchunkcache.md) + + + + + +
+**Returns:** + +[WorldMapChunkCacheMetadata](./server.worldmapchunkcachemetadata.md) + diff --git a/sdk/docs/server.worldmapchunkcachecodec.decompresstoworldmap.md b/sdk/docs/server.worldmapchunkcachecodec.decompresstoworldmap.md new file mode 100644 index 00000000..1d42c6ad --- /dev/null +++ b/sdk/docs/server.worldmapchunkcachecodec.decompresstoworldmap.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCacheCodec](./server.worldmapchunkcachecodec.md) > [decompressToWorldMap](./server.worldmapchunkcachecodec.decompresstoworldmap.md) + +## WorldMapChunkCacheCodec.decompressToWorldMap() method + +**Signature:** + +```typescript +static decompressToWorldMap(cache: WorldMapChunkCache): WorldMap; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +cache + + + + +[WorldMapChunkCache](./server.worldmapchunkcache.md) + + + + + +
+**Returns:** + +[WorldMap](./server.worldmap.md) + diff --git a/sdk/docs/server.worldmapchunkcachecodec.isworldmapchunkcache.md b/sdk/docs/server.worldmapchunkcachecodec.isworldmapchunkcache.md new file mode 100644 index 00000000..f067858a --- /dev/null +++ b/sdk/docs/server.worldmapchunkcachecodec.isworldmapchunkcache.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCacheCodec](./server.worldmapchunkcachecodec.md) > [isWorldMapChunkCache](./server.worldmapchunkcachecodec.isworldmapchunkcache.md) + +## WorldMapChunkCacheCodec.isWorldMapChunkCache() method + +**Signature:** + +```typescript +static isWorldMapChunkCache(value: unknown): value is WorldMapChunkCache; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +value + + + + +unknown + + + + + +
+**Returns:** + +value is [WorldMapChunkCache](./server.worldmapchunkcache.md) + diff --git a/sdk/docs/server.worldmapchunkcachecodec.md b/sdk/docs/server.worldmapchunkcachecodec.md new file mode 100644 index 00000000..f96b9ea3 --- /dev/null +++ b/sdk/docs/server.worldmapchunkcachecodec.md @@ -0,0 +1,115 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCacheCodec](./server.worldmapchunkcachecodec.md) + +## WorldMapChunkCacheCodec class + +**Signature:** + +```typescript +export default class WorldMapChunkCacheCodec +``` + +## Methods + + + + + + + + +
+ +Method + + + + +Modifiers + + + + +Description + + +
+ +[create(map, options)](./server.worldmapchunkcachecodec.create.md) + + + + +`static` + + + + + +
+ +[decode(cache)](./server.worldmapchunkcachecodec.decode.md) + + + + +`static` + + + + + +
+ +[decodeChunks(cache)](./server.worldmapchunkcachecodec.decodechunks.md) + + + + +`static` + + + + + +
+ +[decodeMetadata(cache)](./server.worldmapchunkcachecodec.decodemetadata.md) + + + + +`static` + + + + + +
+ +[decompressToWorldMap(cache)](./server.worldmapchunkcachecodec.decompresstoworldmap.md) + + + + +`static` + + + + + +
+ +[isWorldMapChunkCache(value)](./server.worldmapchunkcachecodec.isworldmapchunkcache.md) + + + + +`static` + + + + + +
diff --git a/sdk/docs/server.worldmapchunkcachemetadata.blocktypes.md b/sdk/docs/server.worldmapchunkcachemetadata.blocktypes.md new file mode 100644 index 00000000..80d22118 --- /dev/null +++ b/sdk/docs/server.worldmapchunkcachemetadata.blocktypes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCacheMetadata](./server.worldmapchunkcachemetadata.md) > [blockTypes](./server.worldmapchunkcachemetadata.blocktypes.md) + +## WorldMapChunkCacheMetadata.blockTypes property + +**Signature:** + +```typescript +blockTypes?: BlockTypeOptions[]; +``` diff --git a/sdk/docs/server.worldmapchunkcachemetadata.entities.md b/sdk/docs/server.worldmapchunkcachemetadata.entities.md new file mode 100644 index 00000000..ee99c6eb --- /dev/null +++ b/sdk/docs/server.worldmapchunkcachemetadata.entities.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCacheMetadata](./server.worldmapchunkcachemetadata.md) > [entities](./server.worldmapchunkcachemetadata.entities.md) + +## WorldMapChunkCacheMetadata.entities property + +**Signature:** + +```typescript +entities?: WorldMap['entities']; +``` diff --git a/sdk/docs/server.worldmapchunkcachemetadata.mapversion.md b/sdk/docs/server.worldmapchunkcachemetadata.mapversion.md new file mode 100644 index 00000000..60f5a785 --- /dev/null +++ b/sdk/docs/server.worldmapchunkcachemetadata.mapversion.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCacheMetadata](./server.worldmapchunkcachemetadata.md) > [mapVersion](./server.worldmapchunkcachemetadata.mapversion.md) + +## WorldMapChunkCacheMetadata.mapVersion property + +**Signature:** + +```typescript +mapVersion?: unknown; +``` diff --git a/sdk/docs/server.worldmapchunkcachemetadata.md b/sdk/docs/server.worldmapchunkcachemetadata.md new file mode 100644 index 00000000..f351607f --- /dev/null +++ b/sdk/docs/server.worldmapchunkcachemetadata.md @@ -0,0 +1,150 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCacheMetadata](./server.worldmapchunkcachemetadata.md) + +## WorldMapChunkCacheMetadata interface + +**Signature:** + +```typescript +export interface WorldMapChunkCacheMetadata +``` + +## Properties + + + + + + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[blockTypes?](./server.worldmapchunkcachemetadata.blocktypes.md) + + + + + + + +[BlockTypeOptions](./server.blocktypeoptions.md)\[\] + + + + +_(Optional)_ + + +
+ +[entities?](./server.worldmapchunkcachemetadata.entities.md) + + + + + + + +[WorldMap](./server.worldmap.md)\['entities'\] + + + + +_(Optional)_ + + +
+ +[mapVersion?](./server.worldmapchunkcachemetadata.mapversion.md) + + + + + + + +unknown + + + + +_(Optional)_ + + +
+ +[metadata?](./server.worldmapchunkcachemetadata.metadata.md) + + + + + + + +unknown + + + + +_(Optional)_ + + +
+ +[options?](./server.worldmapchunkcachemetadata.options.md) + + + + + + + +[WorldMapChunkCacheOptions](./server.worldmapchunkcacheoptions.md) + + + + +_(Optional)_ + + +
+ +[source?](./server.worldmapchunkcachemetadata.source.md) + + + + + + + +{ sha256?: string; } + + + + +_(Optional)_ + + +
diff --git a/sdk/docs/server.worldmapchunkcachemetadata.metadata.md b/sdk/docs/server.worldmapchunkcachemetadata.metadata.md new file mode 100644 index 00000000..90e9cf05 --- /dev/null +++ b/sdk/docs/server.worldmapchunkcachemetadata.metadata.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCacheMetadata](./server.worldmapchunkcachemetadata.md) > [metadata](./server.worldmapchunkcachemetadata.metadata.md) + +## WorldMapChunkCacheMetadata.metadata property + +**Signature:** + +```typescript +metadata?: unknown; +``` diff --git a/sdk/docs/server.worldmapchunkcachemetadata.options.md b/sdk/docs/server.worldmapchunkcachemetadata.options.md new file mode 100644 index 00000000..e551d6b8 --- /dev/null +++ b/sdk/docs/server.worldmapchunkcachemetadata.options.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCacheMetadata](./server.worldmapchunkcachemetadata.md) > [options](./server.worldmapchunkcachemetadata.options.md) + +## WorldMapChunkCacheMetadata.options property + +**Signature:** + +```typescript +options?: WorldMapChunkCacheOptions; +``` diff --git a/sdk/docs/server.worldmapchunkcachemetadata.source.md b/sdk/docs/server.worldmapchunkcachemetadata.source.md new file mode 100644 index 00000000..8964183a --- /dev/null +++ b/sdk/docs/server.worldmapchunkcachemetadata.source.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCacheMetadata](./server.worldmapchunkcachemetadata.md) > [source](./server.worldmapchunkcachemetadata.source.md) + +## WorldMapChunkCacheMetadata.source property + +**Signature:** + +```typescript +source?: { + sha256?: string; + }; +``` diff --git a/sdk/docs/server.worldmapchunkcacheoptions.md b/sdk/docs/server.worldmapchunkcacheoptions.md new file mode 100644 index 00000000..24340617 --- /dev/null +++ b/sdk/docs/server.worldmapchunkcacheoptions.md @@ -0,0 +1,55 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCacheOptions](./server.worldmapchunkcacheoptions.md) + +## WorldMapChunkCacheOptions interface + +**Signature:** + +```typescript +export interface WorldMapChunkCacheOptions +``` + +## Properties + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[rotations?](./server.worldmapchunkcacheoptions.rotations.md) + + + + + + + +boolean + + + + +_(Optional)_ + + +
diff --git a/sdk/docs/server.worldmapchunkcacheoptions.rotations.md b/sdk/docs/server.worldmapchunkcacheoptions.rotations.md new file mode 100644 index 00000000..81e5fc3e --- /dev/null +++ b/sdk/docs/server.worldmapchunkcacheoptions.rotations.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapChunkCacheOptions](./server.worldmapchunkcacheoptions.md) > [rotations](./server.worldmapchunkcacheoptions.rotations.md) + +## WorldMapChunkCacheOptions.rotations property + +**Signature:** + +```typescript +rotations?: boolean; +``` diff --git a/sdk/docs/server.worldmapcodec.compress.md b/sdk/docs/server.worldmapcodec.compress.md new file mode 100644 index 00000000..f9b93da4 --- /dev/null +++ b/sdk/docs/server.worldmapcodec.compress.md @@ -0,0 +1,65 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapCodec](./server.worldmapcodec.md) > [compress](./server.worldmapcodec.compress.md) + +## WorldMapCodec.compress() method + +**Signature:** + +```typescript +static compress(map: WorldMap, options?: CompressWorldMapOptions): CompressedWorldMap; +``` + +## Parameters + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +map + + + + +[WorldMap](./server.worldmap.md) + + + + + +
+ +options + + + + +[CompressWorldMapOptions](./server.compressworldmapoptions.md) + + + + +_(Optional)_ + + +
+**Returns:** + +[CompressedWorldMap](./server.compressedworldmap.md) + diff --git a/sdk/docs/server.worldmapcodec.decodeblockentries.md b/sdk/docs/server.worldmapcodec.decodeblockentries.md new file mode 100644 index 00000000..cceac58f --- /dev/null +++ b/sdk/docs/server.worldmapcodec.decodeblockentries.md @@ -0,0 +1,53 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapCodec](./server.worldmapcodec.md) > [decodeBlockEntries](./server.worldmapcodec.decodeblockentries.md) + +## WorldMapCodec.decodeBlockEntries() method + +**Signature:** + +```typescript +static decodeBlockEntries(map: CompressedWorldMap): Iterable<{ + globalCoordinate: Vector3Like; + blockTypeId: number; + blockRotation?: BlockRotation; + }>; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +map + + + + +[CompressedWorldMap](./server.compressedworldmap.md) + + + + + +
+**Returns:** + +Iterable<{ globalCoordinate: [Vector3Like](./server.vector3like.md); blockTypeId: number; blockRotation?: [BlockRotation](./server.blockrotation.md); }> + diff --git a/sdk/docs/server.worldmapcodec.decompresstoworldmap.md b/sdk/docs/server.worldmapcodec.decompresstoworldmap.md new file mode 100644 index 00000000..aa220fea --- /dev/null +++ b/sdk/docs/server.worldmapcodec.decompresstoworldmap.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapCodec](./server.worldmapcodec.md) > [decompressToWorldMap](./server.worldmapcodec.decompresstoworldmap.md) + +## WorldMapCodec.decompressToWorldMap() method + +**Signature:** + +```typescript +static decompressToWorldMap(map: CompressedWorldMap): WorldMap; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +map + + + + +[CompressedWorldMap](./server.compressedworldmap.md) + + + + + +
+**Returns:** + +[WorldMap](./server.worldmap.md) + diff --git a/sdk/docs/server.worldmapcodec.iscompressedworldmap.md b/sdk/docs/server.worldmapcodec.iscompressedworldmap.md new file mode 100644 index 00000000..3362375d --- /dev/null +++ b/sdk/docs/server.worldmapcodec.iscompressedworldmap.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapCodec](./server.worldmapcodec.md) > [isCompressedWorldMap](./server.worldmapcodec.iscompressedworldmap.md) + +## WorldMapCodec.isCompressedWorldMap() method + +**Signature:** + +```typescript +static isCompressedWorldMap(value: unknown): value is CompressedWorldMap; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +value + + + + +unknown + + + + + +
+**Returns:** + +value is [CompressedWorldMap](./server.compressedworldmap.md) + diff --git a/sdk/docs/server.worldmapcodec.md b/sdk/docs/server.worldmapcodec.md new file mode 100644 index 00000000..32b4234b --- /dev/null +++ b/sdk/docs/server.worldmapcodec.md @@ -0,0 +1,87 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapCodec](./server.worldmapcodec.md) + +## WorldMapCodec class + +**Signature:** + +```typescript +export default class WorldMapCodec +``` + +## Methods + + + + + + +
+ +Method + + + + +Modifiers + + + + +Description + + +
+ +[compress(map, options)](./server.worldmapcodec.compress.md) + + + + +`static` + + + + + +
+ +[decodeBlockEntries(map)](./server.worldmapcodec.decodeblockentries.md) + + + + +`static` + + + + + +
+ +[decompressToWorldMap(map)](./server.worldmapcodec.decompresstoworldmap.md) + + + + +`static` + + + + + +
+ +[isCompressedWorldMap(value)](./server.worldmapcodec.iscompressedworldmap.md) + + + + +`static` + + + + + +
diff --git a/sdk/docs/server.worldmapfileloader.load.md b/sdk/docs/server.worldmapfileloader.load.md new file mode 100644 index 00000000..e4443aa4 --- /dev/null +++ b/sdk/docs/server.worldmapfileloader.load.md @@ -0,0 +1,68 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapFileLoader](./server.worldmapfileloader.md) > [load](./server.worldmapfileloader.load.md) + +## WorldMapFileLoader.load() method + +**Signature:** + +```typescript +static load(mapPath: string, options?: { + preferChunkCache?: boolean; + warnings?: 'auto' | 'always' | 'never'; + }): AnyWorldMap; +``` + +## Parameters + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +mapPath + + + + +string + + + + + +
+ +options + + + + +{ preferChunkCache?: boolean; warnings?: 'auto' \| 'always' \| 'never'; } + + + + +_(Optional)_ + + +
+**Returns:** + +[AnyWorldMap](./server.anyworldmap.md) + diff --git a/sdk/docs/server.worldmapfileloader.md b/sdk/docs/server.worldmapfileloader.md new file mode 100644 index 00000000..1281321c --- /dev/null +++ b/sdk/docs/server.worldmapfileloader.md @@ -0,0 +1,45 @@ + + +[Home](./index.md) > [server](./server.md) > [WorldMapFileLoader](./server.worldmapfileloader.md) + +## WorldMapFileLoader class + +**Signature:** + +```typescript +export default class WorldMapFileLoader +``` + +## Methods + + + +
+ +Method + + + + +Modifiers + + + + +Description + + +
+ +[load(mapPath, options)](./server.worldmapfileloader.load.md) + + + + +`static` + + + + + +
diff --git a/sdk/docs/server.worldoptions.map.md b/sdk/docs/server.worldoptions.map.md index 8707c113..77b009d1 100644 --- a/sdk/docs/server.worldoptions.map.md +++ b/sdk/docs/server.worldoptions.map.md @@ -9,5 +9,5 @@ The map of the world. **Signature:** ```typescript -map?: WorldMap; +map?: WorldMap | CompressedWorldMap | WorldMapChunkCache | string; ``` diff --git a/sdk/docs/server.worldoptions.md b/sdk/docs/server.worldoptions.md index de1b151f..901a31c3 100644 --- a/sdk/docs/server.worldoptions.md +++ b/sdk/docs/server.worldoptions.md @@ -243,7 +243,7 @@ The unique ID of the world. -[WorldMap](./server.worldmap.md) +[WorldMap](./server.worldmap.md) \| [CompressedWorldMap](./server.compressedworldmap.md) \| [WorldMapChunkCache](./server.worldmapchunkcache.md) \| string diff --git a/sdk/server.api.json b/sdk/server.api.json index acb8833c..b34a7d19 100644 --- a/sdk/server.api.json +++ b/sdk/server.api.json @@ -173,6 +173,51 @@ "name": "", "preserveMemberOrder": false, "members": [ + { + "kind": "TypeAlias", + "canonicalReference": "server!AnyWorldMap:type", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "export type AnyWorldMap = " + }, + { + "kind": "Reference", + "text": "WorldMap", + "canonicalReference": "server!WorldMap:interface" + }, + { + "kind": "Content", + "text": " | " + }, + { + "kind": "Reference", + "text": "CompressedWorldMap", + "canonicalReference": "server!CompressedWorldMap:interface" + }, + { + "kind": "Content", + "text": " | " + }, + { + "kind": "Reference", + "text": "WorldMapChunkCache", + "canonicalReference": "server!WorldMapChunkCache:interface" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "src/worlds/maps/WorldMapFileLoader.ts", + "releaseTag": "Public", + "name": "AnyWorldMap", + "typeTokenRange": { + "startIndex": 1, + "endIndex": 6 + } + }, { "kind": "Class", "canonicalReference": "server!AssetsLibrary:class", @@ -6748,40 +6793,31 @@ }, { "kind": "Interface", - "canonicalReference": "server!CapsuleColliderOptions:interface", - "docComment": "/**\n * The options for a capsule collider.\n *\n * Use for: capsule-shaped colliders. Do NOT use for: other shapes; use the matching collider option type.\n *\n * **Category:** Physics\n *\n * @public\n */\n", + "canonicalReference": "server!BotBehavior:interface", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "export interface CapsuleColliderOptions extends " - }, - { - "kind": "Reference", - "text": "BaseColliderOptions", - "canonicalReference": "server!BaseColliderOptions:interface" - }, - { - "kind": "Content", - "text": " " + "text": "export interface BotBehavior " } ], - "fileUrlPath": "src/worlds/physics/Collider.ts", + "fileUrlPath": "src/bots/BotPlayer.ts", "releaseTag": "Public", - "name": "CapsuleColliderOptions", + "name": "BotBehavior", "preserveMemberOrder": false, "members": [ { "kind": "PropertySignature", - "canonicalReference": "server!CapsuleColliderOptions#halfHeight:member", - "docComment": "/**\n * The half height of the capsule collider.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!BotBehavior#name:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "halfHeight?: " + "text": "name: " }, { "kind": "Content", - "text": "number" + "text": "string" }, { "kind": "Content", @@ -6789,290 +6825,185 @@ } ], "isReadonly": false, - "isOptional": true, + "isOptional": false, "releaseTag": "Public", - "name": "halfHeight", + "name": "name", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 } }, { - "kind": "PropertySignature", - "canonicalReference": "server!CapsuleColliderOptions#radius:member", - "docComment": "/**\n * The radius of the capsule collider.\n *\n * **Category:** Physics\n */\n", + "kind": "MethodSignature", + "canonicalReference": "server!BotBehavior#tick:member(1)", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "radius?: " + "text": "tick(bot: " }, { - "kind": "Content", - "text": "number" + "kind": "Reference", + "text": "BotPlayer", + "canonicalReference": "server!BotPlayer:class" }, { "kind": "Content", - "text": ";" - } - ], - "isReadonly": false, - "isOptional": true, - "releaseTag": "Public", - "name": "radius", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } - }, - { - "kind": "PropertySignature", - "canonicalReference": "server!CapsuleColliderOptions#shape:member", - "docComment": "", - "excerptTokens": [ + "text": ", world: " + }, + { + "kind": "Reference", + "text": "World", + "canonicalReference": "server!World:class" + }, { "kind": "Content", - "text": "shape: " + "text": ", deltaTimeMs: " }, { - "kind": "Reference", - "text": "ColliderShape.CAPSULE", - "canonicalReference": "server!ColliderShape.CAPSULE:member" + "kind": "Content", + "text": "number" }, { "kind": "Content", - "text": ";" - } - ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "shape", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } - } - ], - "extendsTokenRanges": [ - { - "startIndex": 1, - "endIndex": 2 - } - ] - }, - { - "kind": "Enum", - "canonicalReference": "server!ChatEvent:enum", - "docComment": "/**\n * Event types a ChatManager instance can emit.\n *\n * See `ChatEventPayloads` for the payloads.\n *\n * **Category:** Events\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export declare enum ChatEvent " - } - ], - "fileUrlPath": "src/worlds/chat/ChatManager.ts", - "releaseTag": "Public", - "name": "ChatEvent", - "preserveMemberOrder": false, - "members": [ - { - "kind": "EnumMember", - "canonicalReference": "server!ChatEvent.BROADCAST_MESSAGE:member", - "docComment": "", - "excerptTokens": [ + "text": "): " + }, { "kind": "Content", - "text": "BROADCAST_MESSAGE = " + "text": "void" }, { "kind": "Content", - "text": "\"CHAT.BROADCAST_MESSAGE\"" + "text": ";" } ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 + "isOptional": false, + "returnTypeTokenRange": { + "startIndex": 7, + "endIndex": 8 }, "releaseTag": "Public", - "name": "BROADCAST_MESSAGE" - }, - { - "kind": "EnumMember", - "canonicalReference": "server!ChatEvent.PLAYER_MESSAGE:member", - "docComment": "", - "excerptTokens": [ + "overloadIndex": 1, + "parameters": [ { - "kind": "Content", - "text": "PLAYER_MESSAGE = " + "parameterName": "bot", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false }, { - "kind": "Content", - "text": "\"CHAT.PLAYER_MESSAGE\"" + "parameterName": "world", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": false + }, + { + "parameterName": "deltaTimeMs", + "parameterTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "isOptional": false } ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "name": "PLAYER_MESSAGE" + "name": "tick" } - ] + ], + "extendsTokenRanges": [] }, { - "kind": "Interface", - "canonicalReference": "server!ChatEventPayloads:interface", - "docComment": "/**\n * Event payloads for ChatManager emitted events.\n *\n * **Category:** Events\n *\n * @public\n */\n", + "kind": "Class", + "canonicalReference": "server!BotManager:class", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "export interface ChatEventPayloads " + "text": "export default class BotManager " } ], - "fileUrlPath": "src/worlds/chat/ChatManager.ts", + "fileUrlPath": "src/bots/BotManager.ts", "releaseTag": "Public", - "name": "ChatEventPayloads", + "isAbstract": false, + "name": "BotManager", "preserveMemberOrder": false, "members": [ { - "kind": "PropertySignature", - "canonicalReference": "server!ChatEventPayloads#\"CHAT.BROADCAST_MESSAGE\":member", - "docComment": "/**\n * Emitted when a broadcast message is sent.\n */\n", + "kind": "Property", + "canonicalReference": "server!BotManager#botCount:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "[" - }, - { - "kind": "Reference", - "text": "ChatEvent.BROADCAST_MESSAGE", - "canonicalReference": "server!ChatEvent.BROADCAST_MESSAGE:member" - }, - { - "kind": "Content", - "text": "]: " - }, - { - "kind": "Content", - "text": "{\n player: " - }, - { - "kind": "Reference", - "text": "Player", - "canonicalReference": "server!Player:class" + "text": "get botCount(): " }, { "kind": "Content", - "text": " | undefined;\n message: string;\n color?: string;\n }" + "text": "number" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, + "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "\"CHAT.BROADCAST_MESSAGE\"", + "name": "botCount", "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 6 - } + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "PropertySignature", - "canonicalReference": "server!ChatEventPayloads#\"CHAT.PLAYER_MESSAGE\":member", - "docComment": "/**\n * Emitted when a message is sent to a specific player.\n */\n", + "kind": "Method", + "canonicalReference": "server!BotManager#despawnAll:member(1)", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "[" - }, - { - "kind": "Reference", - "text": "ChatEvent.PLAYER_MESSAGE", - "canonicalReference": "server!ChatEvent.PLAYER_MESSAGE:member" - }, - { - "kind": "Content", - "text": "]: " - }, - { - "kind": "Content", - "text": "{\n player: " - }, - { - "kind": "Reference", - "text": "Player", - "canonicalReference": "server!Player:class" + "text": "despawnAll(): " }, { "kind": "Content", - "text": ";\n message: string;\n color?: string;\n }" + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": false, + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, "releaseTag": "Public", - "name": "\"CHAT.PLAYER_MESSAGE\"", - "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 6 - } - } - ], - "extendsTokenRanges": [] - }, - { - "kind": "Class", - "canonicalReference": "server!ChatManager:class", - "docComment": "/**\n * Manages chat and commands in a world.\n *\n * When to use: broadcasting chat, sending system messages, or registering chat commands. Do NOT use for: player HUD/menus; use `PlayerUI` for rich UI.\n *\n * @remarks\n *\n * The ChatManager is created internally as a singleton for each `World` instance in a game server. The ChatManager allows you to broadcast messages, send messages to specific players, and register commands that can be used in chat to execute game logic.\n *\n * Pattern: register commands during world initialization and keep callbacks fast. Anti-pattern: assuming commands are permission-checked; always validate access in callbacks.\n *\n *

Events

\n *\n * This class is an EventRouter, and instances of it emit events with payloads listed under `ChatEventPayloads`\n *\n * The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `ChatManager` class.\n *\n * @example\n * ```typescript\n * world.chatManager.registerCommand('/kick', (player, args, message) => {\n * const admins = [ 'arkdev', 'testuser123' ];\n * if (admins.includes(player.username)) {\n * const targetUsername = args[0];\n * const targetPlayer = world.playerManager.getConnectedPlayerByUsername(targetUsername);\n *\n * if (targetPlayer) {\n * targetPlayer.disconnect();\n * }\n * }\n * });\n * ```\n *\n * **Category:** Chat\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export default class ChatManager extends " - }, - { - "kind": "Reference", - "text": "EventRouter", - "canonicalReference": "server!EventRouter:class" + "isProtected": false, + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "despawnAll" }, - { - "kind": "Content", - "text": " " - } - ], - "fileUrlPath": "src/worlds/chat/ChatManager.ts", - "releaseTag": "Public", - "isAbstract": false, - "name": "ChatManager", - "preserveMemberOrder": false, - "members": [ { "kind": "Method", - "canonicalReference": "server!ChatManager#handleCommand:member(1)", - "docComment": "/**\n * Handle a command if it exists.\n *\n * @remarks\n *\n * The command is parsed as the first space-delimited token in the message.\n *\n * **Category:** Chat\n *\n * @param player - The player that sent the command.\n *\n * @param message - The full message.\n *\n * @returns True if a command was handled, false otherwise.\n */\n", + "canonicalReference": "server!BotManager#despawnBot:member(1)", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "handleCommand(player: " - }, - { - "kind": "Reference", - "text": "Player", - "canonicalReference": "server!Player:class" - }, - { - "kind": "Content", - "text": ", message: " + "text": "despawnBot(id: " }, { "kind": "Content", - "text": "string" + "text": "number" }, { "kind": "Content", @@ -7080,7 +7011,7 @@ }, { "kind": "Content", - "text": "boolean" + "text": "void" }, { "kind": "Content", @@ -7089,63 +7020,87 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 5, - "endIndex": 6 + "startIndex": 3, + "endIndex": 4 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "player", + "parameterName": "id", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, "isOptional": false - }, - { - "parameterName": "message", - "parameterTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, - "isOptional": false } ], "isOptional": false, "isAbstract": false, - "name": "handleCommand" + "name": "despawnBot" }, { "kind": "Method", - "canonicalReference": "server!ChatManager#registerCommand:member(1)", - "docComment": "/**\n * Register a command and its callback.\n *\n * @remarks\n *\n * Commands are matched by exact string equality against the first token in a chat message.\n *\n * @param command - The command to register.\n *\n * @param callback - The callback function to execute when the command is used.\n *\n * **Requires:** Use a consistent command prefix (for example, `/kick`) if you want slash commands.\n *\n * @see\n *\n * `ChatManager.unregisterCommand`\n *\n * **Category:** Chat\n */\n", + "canonicalReference": "server!BotManager#getAllBots:member(1)", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "registerCommand(command: " + "text": "getAllBots(): " + }, + { + "kind": "Reference", + "text": "BotPlayer", + "canonicalReference": "server!BotPlayer:class" }, { "kind": "Content", - "text": "string" + "text": "[]" }, { "kind": "Content", - "text": ", callback: " + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "getAllBots" + }, + { + "kind": "Method", + "canonicalReference": "server!BotManager#getBot:member(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "getBot(id: " }, { - "kind": "Reference", - "text": "CommandCallback", - "canonicalReference": "server!CommandCallback:type" + "kind": "Content", + "text": "number" }, { "kind": "Content", "text": "): " }, + { + "kind": "Reference", + "text": "BotPlayer", + "canonicalReference": "server!BotPlayer:class" + }, { "kind": "Content", - "text": "void" + "text": " | undefined" }, { "kind": "Content", @@ -7154,62 +7109,88 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 5, - "endIndex": 6 + "startIndex": 3, + "endIndex": 5 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "command", + "parameterName": "id", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "getBot" + }, + { + "kind": "Property", + "canonicalReference": "server!BotManager.instance:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "static get instance(): " }, { - "parameterName": "callback", - "parameterTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, - "isOptional": false + "kind": "Reference", + "text": "BotManager", + "canonicalReference": "server!BotManager:class" + }, + { + "kind": "Content", + "text": ";" } ], + "isReadonly": true, "isOptional": false, - "isAbstract": false, - "name": "registerCommand" + "releaseTag": "Public", + "name": "instance", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": true, + "isProtected": false, + "isAbstract": false }, { "kind": "Method", - "canonicalReference": "server!ChatManager#sendBroadcastMessage:member(1)", - "docComment": "/**\n * Send a system broadcast message to all players in the world.\n *\n * @param message - The message to send.\n *\n * @param color - The color of the message as a hex color code, excluding #.\n *\n * @example\n * ```typescript\n * chatManager.sendBroadcastMessage('Hello, world!', 'FF00AA');\n * ```\n *\n * **Side effects:** Emits `ChatEvent.BROADCAST_MESSAGE` for network sync.\n *\n * @see\n *\n * `ChatManager.sendPlayerMessage`\n *\n * **Category:** Chat\n */\n", + "canonicalReference": "server!BotManager#spawnBot:member(1)", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "sendBroadcastMessage(message: " + "text": "spawnBot(world: " }, { - "kind": "Content", - "text": "string" + "kind": "Reference", + "text": "World", + "canonicalReference": "server!World:class" }, { "kind": "Content", - "text": ", color?: " + "text": ", options?: " }, { - "kind": "Content", - "text": "string" + "kind": "Reference", + "text": "BotPlayerOptions", + "canonicalReference": "server!BotPlayerOptions:interface" }, { "kind": "Content", "text": "): " }, { - "kind": "Content", - "text": "void" + "kind": "Reference", + "text": "BotPlayer", + "canonicalReference": "server!BotPlayer:class" }, { "kind": "Content", @@ -7226,7 +7207,7 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "message", + "parameterName": "world", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -7234,7 +7215,7 @@ "isOptional": false }, { - "parameterName": "color", + "parameterName": "options", "parameterTypeTokenRange": { "startIndex": 3, "endIndex": 4 @@ -7244,45 +7225,51 @@ ], "isOptional": false, "isAbstract": false, - "name": "sendBroadcastMessage" + "name": "spawnBot" }, { "kind": "Method", - "canonicalReference": "server!ChatManager#sendPlayerMessage:member(1)", - "docComment": "/**\n * Send a system message to a specific player, only visible to them.\n *\n * @param player - The player to send the message to.\n *\n * @param message - The message to send.\n *\n * @param color - The color of the message as a hex color code, excluding #.\n *\n * @example\n * ```typescript\n * chatManager.sendPlayerMessage(player, 'Hello, player!', 'FF00AA');\n * ```\n *\n * **Side effects:** Emits `ChatEvent.PLAYER_MESSAGE` for network sync.\n *\n * @see\n *\n * `ChatManager.sendBroadcastMessage`\n *\n * **Category:** Chat\n */\n", + "canonicalReference": "server!BotManager#spawnBots:member(1)", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "sendPlayerMessage(player: " + "text": "spawnBots(world: " }, { "kind": "Reference", - "text": "Player", - "canonicalReference": "server!Player:class" + "text": "World", + "canonicalReference": "server!World:class" }, { "kind": "Content", - "text": ", message: " + "text": ", count: " }, { "kind": "Content", - "text": "string" + "text": "number" }, { "kind": "Content", - "text": ", color?: " + "text": ", options?: " }, { - "kind": "Content", - "text": "string" + "kind": "Reference", + "text": "BotPlayerOptions", + "canonicalReference": "server!BotPlayerOptions:interface" }, { "kind": "Content", "text": "): " }, + { + "kind": "Reference", + "text": "BotPlayer", + "canonicalReference": "server!BotPlayer:class" + }, { "kind": "Content", - "text": "void" + "text": "[]" }, { "kind": "Content", @@ -7292,14 +7279,14 @@ "isStatic": false, "returnTypeTokenRange": { "startIndex": 7, - "endIndex": 8 + "endIndex": 9 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "player", + "parameterName": "world", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -7307,7 +7294,7 @@ "isOptional": false }, { - "parameterName": "message", + "parameterName": "count", "parameterTypeTokenRange": { "startIndex": 3, "endIndex": 4 @@ -7315,7 +7302,7 @@ "isOptional": false }, { - "parameterName": "color", + "parameterName": "options", "parameterTypeTokenRange": { "startIndex": 5, "endIndex": 6 @@ -7325,205 +7312,152 @@ ], "isOptional": false, "isAbstract": false, - "name": "sendPlayerMessage" - }, + "name": "spawnBots" + } + ], + "implementsTokenRanges": [] + }, + { + "kind": "Class", + "canonicalReference": "server!BotPlayer:class", + "docComment": "", + "excerptTokens": [ { - "kind": "Method", - "canonicalReference": "server!ChatManager#unregisterCommand:member(1)", - "docComment": "/**\n * Unregister a command.\n *\n * @param command - The command to unregister.\n *\n * @see\n *\n * `ChatManager.registerCommand`\n *\n * **Category:** Chat\n */\n", + "kind": "Content", + "text": "export default class BotPlayer " + } + ], + "fileUrlPath": "src/bots/BotPlayer.ts", + "releaseTag": "Public", + "isAbstract": false, + "name": "BotPlayer", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Constructor", + "canonicalReference": "server!BotPlayer:constructor(1)", + "docComment": "/**\n * Constructs a new instance of the `BotPlayer` class\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "unregisterCommand(command: " + "text": "constructor(world: " }, { - "kind": "Content", - "text": "string" + "kind": "Reference", + "text": "World", + "canonicalReference": "server!World:class" }, { "kind": "Content", - "text": "): " + "text": ", options?: " }, { - "kind": "Content", - "text": "void" + "kind": "Reference", + "text": "BotPlayerOptions", + "canonicalReference": "server!BotPlayerOptions:interface" }, { "kind": "Content", - "text": ";" + "text": ");" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "command", + "parameterName": "world", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, "isOptional": false + }, + { + "parameterName": "options", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": true } - ], - "isOptional": false, - "isAbstract": false, - "name": "unregisterCommand" - } - ], - "extendsTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "implementsTokenRanges": [] - }, - { - "kind": "Class", - "canonicalReference": "server!Chunk:class", - "docComment": "/**\n * A 16^3 chunk of blocks representing a slice of world terrain.\n *\n * When to use: reading chunk data or working with bulk block operations. Do NOT use for: creating terrain directly; prefer `ChunkLattice`.\n *\n * @remarks\n *\n * Chunks are fixed-size (16×16×16) and store block IDs by local coordinates.\n *\n *

Coordinate System

\n *\n * - **Global (world) coordinates:** integer block positions in world space. - **Chunk origin:** the world coordinate at the chunk's minimum corner (multiples of 16). - **Local coordinates:** 0..15 per axis within the chunk.\n *\n * **Category:** Blocks\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export default class Chunk implements " - }, - { - "kind": "Reference", - "text": "protocol.Serializable", - "canonicalReference": "@hytopia.com/server-protocol!Serializable:interface" + ] }, { - "kind": "Content", - "text": " " - } - ], - "fileUrlPath": "src/worlds/blocks/Chunk.ts", - "releaseTag": "Public", - "isAbstract": false, - "name": "Chunk", - "preserveMemberOrder": false, - "members": [ - { - "kind": "Constructor", - "canonicalReference": "server!Chunk:constructor(1)", - "docComment": "/**\n * Creates a new chunk instance.\n */\n", + "kind": "Property", + "canonicalReference": "server!BotPlayer#controller:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "constructor(originCoordinate: " + "text": "get controller(): " }, { "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" + "text": "SimpleEntityController", + "canonicalReference": "server!SimpleEntityController:class" }, { "kind": "Content", - "text": ");" + "text": ";" } ], + "isReadonly": true, + "isOptional": false, "releaseTag": "Public", + "name": "controller", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "originCoordinate", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ] + "isAbstract": false }, { "kind": "Method", - "canonicalReference": "server!Chunk.blockIndexToLocalCoordinate:member(1)", - "docComment": "/**\n * Converts a block index to a local coordinate.\n *\n * @param index - The index of the block to convert.\n *\n * @returns The local coordinate of the block.\n *\n * **Category:** Blocks\n */\n", + "canonicalReference": "server!BotPlayer#despawn:member(1)", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "static blockIndexToLocalCoordinate(index: " - }, - { - "kind": "Content", - "text": "number" + "text": "despawn(): " }, { "kind": "Content", - "text": "): " - }, - { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isStatic": true, + "isStatic": false, "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 + "startIndex": 1, + "endIndex": 2 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, - "parameters": [ - { - "parameterName": "index", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], + "parameters": [], "isOptional": false, "isAbstract": false, - "name": "blockIndexToLocalCoordinate" + "name": "despawn" }, { "kind": "Property", - "canonicalReference": "server!Chunk#blockRotations:member", - "docComment": "/**\n * The rotations of the blocks in the chunk as a map of block index to rotation.\n *\n * **Category:** Blocks\n */\n", + "canonicalReference": "server!BotPlayer#entity:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "get blockRotations(): " - }, - { - "kind": "Reference", - "text": "Readonly", - "canonicalReference": "!Readonly:type" - }, - { - "kind": "Content", - "text": "<" - }, - { - "kind": "Reference", - "text": "Map", - "canonicalReference": "!Map:interface" - }, - { - "kind": "Content", - "text": ">" + "text": "Entity", + "canonicalReference": "server!Entity:class" }, { "kind": "Content", @@ -7533,10 +7467,10 @@ "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "blockRotations", + "name": "entity", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 7 + "endIndex": 2 }, "isStatic": false, "isProtected": false, @@ -7544,30 +7478,16 @@ }, { "kind": "Property", - "canonicalReference": "server!Chunk#blocks:member", - "docComment": "/**\n * The blocks in the chunk as a flat Uint8Array[4096], each index as 0 or a block type ID.\n *\n * **Category:** Blocks\n */\n", + "canonicalReference": "server!BotPlayer#id:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "get blocks(): " - }, - { - "kind": "Reference", - "text": "Readonly", - "canonicalReference": "!Readonly:type" - }, - { - "kind": "Content", - "text": "<" - }, - { - "kind": "Reference", - "text": "Uint8Array", - "canonicalReference": "!Uint8Array:interface" + "text": "readonly id: " }, { "kind": "Content", - "text": ">" + "text": "number" }, { "kind": "Content", @@ -7577,143 +7497,103 @@ "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "blocks", + "name": "id", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 5 + "endIndex": 2 }, "isStatic": false, "isProtected": false, "isAbstract": false }, { - "kind": "Method", - "canonicalReference": "server!Chunk#getBlockId:member(1)", - "docComment": "/**\n * Gets the block type ID at a specific local coordinate.\n *\n * @remarks\n *\n * Expects local coordinates in the range 0..15 for each axis.\n *\n * @param localCoordinate - The local coordinate of the block to get.\n *\n * @returns The block type ID.\n *\n * **Category:** Blocks\n */\n", + "kind": "Property", + "canonicalReference": "server!BotPlayer#isSpawned:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "getBlockId(localCoordinate: " - }, - { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" - }, - { - "kind": "Content", - "text": "): " + "text": "get isSpawned(): " }, { "kind": "Content", - "text": "number" + "text": "boolean" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, + "isReadonly": true, + "isOptional": false, "releaseTag": "Public", + "name": "isSpawned", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "localCoordinate", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "getBlockId" + "isAbstract": false }, { - "kind": "Method", - "canonicalReference": "server!Chunk#getBlockRotation:member(1)", - "docComment": "/**\n * Gets the rotation of a block at a specific local coordinate.\n *\n * @param localCoordinate - The local coordinate of the block to get the rotation of.\n *\n * @returns The rotation of the block (defaults to identity rotation).\n *\n * **Category:** Blocks\n */\n", + "kind": "Property", + "canonicalReference": "server!BotPlayer#name:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "getBlockRotation(localCoordinate: " - }, - { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" + "text": "readonly name: " }, { "kind": "Content", - "text": "): " - }, - { - "kind": "Reference", - "text": "BlockRotation", - "canonicalReference": "server!BlockRotation:type" + "text": "string" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, + "isReadonly": true, + "isOptional": false, "releaseTag": "Public", + "name": "name", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "localCoordinate", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "getBlockRotation" + "isAbstract": false }, { "kind": "Method", - "canonicalReference": "server!Chunk.globalCoordinateToLocalCoordinate:member(1)", - "docComment": "/**\n * Converts a global coordinate to a local coordinate.\n *\n * @param globalCoordinate - The global coordinate to convert.\n *\n * @returns The local coordinate.\n *\n * **Category:** Blocks\n */\n", + "canonicalReference": "server!BotPlayer#setBehavior:member(1)", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "static globalCoordinateToLocalCoordinate(globalCoordinate: " + "text": "setBehavior(behavior: " }, { "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" + "text": "BotBehavior", + "canonicalReference": "server!BotBehavior:interface" }, { "kind": "Content", "text": "): " }, { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" + "kind": "Content", + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isStatic": true, + "isStatic": false, "returnTypeTokenRange": { "startIndex": 3, "endIndex": 4 @@ -7723,7 +7603,7 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "globalCoordinate", + "parameterName": "behavior", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -7733,16 +7613,16 @@ ], "isOptional": false, "isAbstract": false, - "name": "globalCoordinateToLocalCoordinate" + "name": "setBehavior" }, { "kind": "Method", - "canonicalReference": "server!Chunk.globalCoordinateToOriginCoordinate:member(1)", - "docComment": "/**\n * Converts a global coordinate to a chunk origin coordinate.\n *\n * @param globalCoordinate - The global coordinate to convert.\n *\n * @returns The origin coordinate.\n *\n * **Category:** Blocks\n */\n", + "canonicalReference": "server!BotPlayer#spawn:member(1)", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "static globalCoordinateToOriginCoordinate(globalCoordinate: " + "text": "spawn(position?: " }, { "kind": "Reference", @@ -7754,16 +7634,15 @@ "text": "): " }, { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" + "kind": "Content", + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isStatic": true, + "isStatic": false, "returnTypeTokenRange": { "startIndex": 3, "endIndex": 4 @@ -7773,26 +7652,26 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "globalCoordinate", + "parameterName": "position", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isOptional": false + "isOptional": true } ], "isOptional": false, "isAbstract": false, - "name": "globalCoordinateToOriginCoordinate" + "name": "spawn" }, { "kind": "Method", - "canonicalReference": "server!Chunk#hasBlock:member(1)", - "docComment": "/**\n * Checks if a block exists at a specific local coordinate.\n *\n * @param localCoordinate - The local coordinate of the block to check.\n *\n * @returns Whether a block exists.\n *\n * **Category:** Blocks\n */\n", + "canonicalReference": "server!BotPlayer#teleport:member(1)", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "hasBlock(localCoordinate: " + "text": "teleport(position: " }, { "kind": "Reference", @@ -7805,7 +7684,7 @@ }, { "kind": "Content", - "text": "boolean" + "text": "void" }, { "kind": "Content", @@ -7822,7 +7701,7 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "localCoordinate", + "parameterName": "position", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -7832,21 +7711,21 @@ ], "isOptional": false, "isAbstract": false, - "name": "hasBlock" + "name": "teleport" }, { "kind": "Property", - "canonicalReference": "server!Chunk#originCoordinate:member", - "docComment": "/**\n * The origin coordinate of the chunk (world-space, multiples of 16).\n *\n * **Category:** Blocks\n */\n", + "canonicalReference": "server!BotPlayer#world:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "get originCoordinate(): " + "text": "get world(): " }, { "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" + "text": "World", + "canonicalReference": "server!World:class" }, { "kind": "Content", @@ -7856,7 +7735,7 @@ "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "originCoordinate", + "name": "world", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -7866,79 +7745,59 @@ "isAbstract": false } ], - "implementsTokenRanges": [ - { - "startIndex": 1, - "endIndex": 2 - } - ] + "implementsTokenRanges": [] }, { - "kind": "Class", - "canonicalReference": "server!ChunkLattice:class", - "docComment": "/**\n * A lattice of chunks that represent a world's terrain.\n *\n * When to use: reading or mutating blocks in world space. Do NOT use for: per-entity placement logic; prefer higher-level game systems.\n *\n * @remarks\n *\n * The lattice owns all chunks and keeps physics colliders in sync with blocks.\n *\n *

Coordinate System

\n *\n * - **Global (world) coordinates:** integer block positions in world space. - **Chunk origin:** world coordinate at the chunk's minimum corner (multiples of 16). - **Local coordinates:** 0..15 per axis within a chunk. - **Axes:** +X right, +Y up, -Z forward. - **Origin:** (0,0,0) is the world origin.\n *\n * **Category:** Blocks\n *\n * @public\n */\n", + "kind": "Interface", + "canonicalReference": "server!BotPlayerOptions:interface", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "export default class ChunkLattice extends " - }, - { - "kind": "Reference", - "text": "EventRouter", - "canonicalReference": "server!EventRouter:class" - }, - { - "kind": "Content", - "text": " " + "text": "export interface BotPlayerOptions " } ], - "fileUrlPath": "src/worlds/blocks/ChunkLattice.ts", + "fileUrlPath": "src/bots/BotPlayer.ts", "releaseTag": "Public", - "isAbstract": false, - "name": "ChunkLattice", + "name": "BotPlayerOptions", "preserveMemberOrder": false, "members": [ { - "kind": "Constructor", - "canonicalReference": "server!ChunkLattice:constructor(1)", - "docComment": "/**\n * Creates a new chunk lattice instance.\n *\n * @param world - The world the chunk lattice is for.\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!BotPlayerOptions#behavior:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "constructor(world: " + "text": "behavior?: " }, { "kind": "Reference", - "text": "World", - "canonicalReference": "server!World:class" + "text": "BotBehavior", + "canonicalReference": "server!BotBehavior:interface" }, { "kind": "Content", - "text": ");" + "text": ";" } ], + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "world", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ] + "name": "behavior", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, { - "kind": "Property", - "canonicalReference": "server!ChunkLattice#chunkCount:member", - "docComment": "/**\n * The number of chunks in the lattice.\n *\n * **Category:** Blocks\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!BotPlayerOptions#modelScale:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "get chunkCount(): " + "text": "modelScale?: " }, { "kind": "Content", @@ -7949,204 +7808,160 @@ "text": ";" } ], - "isReadonly": true, - "isOptional": false, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "name": "chunkCount", + "name": "modelScale", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + } }, { - "kind": "Method", - "canonicalReference": "server!ChunkLattice#clear:member(1)", - "docComment": "/**\n * Removes and clears all chunks and their blocks from the lattice.\n *\n * Use for: full world resets or map reloads. Do NOT use for: incremental changes; use `ChunkLattice.setBlock`.\n *\n * @remarks\n *\n * **Removes colliders:** All block type colliders are removed from the physics simulation.\n *\n * **Emits events:** Emits `REMOVE_CHUNK` for each chunk before clearing.\n *\n * **Side effects:** Clears all chunks, placements, and block colliders.\n *\n * **Category:** Blocks\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!BotPlayerOptions#modelUri:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "clear(): " + "text": "modelUri?: " }, { "kind": "Content", - "text": "void" + "text": "string" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "modelUri", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 - }, - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [], - "isOptional": false, - "isAbstract": false, - "name": "clear" + } }, { - "kind": "Method", - "canonicalReference": "server!ChunkLattice#getAllChunks:member(1)", - "docComment": "/**\n * Gets all chunks in the lattice.\n *\n * @returns An array of all chunks in the lattice.\n *\n * **Category:** Blocks\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!BotPlayerOptions#name:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "getAllChunks(): " - }, - { - "kind": "Reference", - "text": "Chunk", - "canonicalReference": "server!Chunk:class" + "text": "name?: " }, { "kind": "Content", - "text": "[]" + "text": "string" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - }, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [], - "isOptional": false, - "isAbstract": false, - "name": "getAllChunks" + "name": "name", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, { - "kind": "Method", - "canonicalReference": "server!ChunkLattice#getBlockId:member(1)", - "docComment": "/**\n * Gets the block type ID at a specific global coordinate.\n *\n * @param globalCoordinate - The global coordinate of the block to get.\n *\n * @returns The block type ID, or 0 if no block is set.\n *\n * **Category:** Blocks\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!BotPlayerOptions#rigidBodyType:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "getBlockId(globalCoordinate: " + "text": "rigidBodyType?: " }, { "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" - }, - { - "kind": "Content", - "text": "): " - }, - { - "kind": "Content", - "text": "number" + "text": "RigidBodyType", + "canonicalReference": "server!RigidBodyType:enum" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "globalCoordinate", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "getBlockId" + "name": "rigidBodyType", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, { - "kind": "Method", - "canonicalReference": "server!ChunkLattice#getBlockType:member(1)", - "docComment": "/**\n * Gets the block type at a specific global coordinate.\n *\n * @param globalCoordinate - The global coordinate of the block to get.\n *\n * @returns The block type, or null if no block is set.\n *\n * **Category:** Blocks\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!BotPlayerOptions#spawnPosition:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "getBlockType(globalCoordinate: " + "text": "spawnPosition?: " }, { "kind": "Reference", "text": "Vector3Like", "canonicalReference": "server!Vector3Like:interface" }, - { - "kind": "Content", - "text": "): " - }, - { - "kind": "Reference", - "text": "BlockType", - "canonicalReference": "server!BlockType:class" - }, - { - "kind": "Content", - "text": " | null" - }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 5 - }, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "globalCoordinate", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "getBlockType" + "name": "spawnPosition", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + } + ], + "extendsTokenRanges": [] + }, + { + "kind": "Interface", + "canonicalReference": "server!CapsuleColliderOptions:interface", + "docComment": "/**\n * The options for a capsule collider.\n *\n * Use for: capsule-shaped colliders. Do NOT use for: other shapes; use the matching collider option type.\n *\n * **Category:** Physics\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export interface CapsuleColliderOptions extends " }, { - "kind": "Method", - "canonicalReference": "server!ChunkLattice#getBlockTypeCount:member(1)", - "docComment": "/**\n * Gets the number of blocks of a specific block type in the lattice.\n *\n * @param blockTypeId - The block type ID to count.\n *\n * @returns The number of blocks of the block type.\n *\n * **Category:** Blocks\n */\n", + "kind": "Reference", + "text": "BaseColliderOptions", + "canonicalReference": "server!BaseColliderOptions:interface" + }, + { + "kind": "Content", + "text": " " + } + ], + "fileUrlPath": "src/worlds/physics/Collider.ts", + "releaseTag": "Public", + "name": "CapsuleColliderOptions", + "preserveMemberOrder": false, + "members": [ + { + "kind": "PropertySignature", + "canonicalReference": "server!CapsuleColliderOptions#halfHeight:member", + "docComment": "/**\n * The half height of the capsule collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "getBlockTypeCount(blockTypeId: " - }, - { - "kind": "Content", - "text": "number" - }, - { - "kind": "Content", - "text": "): " + "text": "halfHeight?: " }, { "kind": "Content", @@ -8157,318 +7972,201 @@ "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "blockTypeId", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "getBlockTypeCount" + "name": "halfHeight", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, { - "kind": "Method", - "canonicalReference": "server!ChunkLattice#getChunk:member(1)", - "docComment": "/**\n * Gets the chunk that contains the given global coordinate.\n *\n * @param globalCoordinate - The global coordinate to get the chunk for.\n *\n * @returns The chunk that contains the given global coordinate or undefined if not found.\n *\n * **Category:** Blocks\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!CapsuleColliderOptions#radius:member", + "docComment": "/**\n * The radius of the capsule collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "getChunk(globalCoordinate: " - }, - { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" - }, - { - "kind": "Content", - "text": "): " - }, - { - "kind": "Reference", - "text": "Chunk", - "canonicalReference": "server!Chunk:class" + "text": "radius?: " }, { "kind": "Content", - "text": " | undefined" + "text": "number" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 5 - }, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "globalCoordinate", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "getChunk" + "name": "radius", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, { - "kind": "Method", - "canonicalReference": "server!ChunkLattice#getOrCreateChunk:member(1)", - "docComment": "/**\n * Gets the chunk for a given global coordinate, creating it if it doesn't exist.\n *\n * @remarks\n *\n * Creates a new chunk and emits `ChunkLatticeEvent.ADD_CHUNK` if needed.\n *\n * @param globalCoordinate - The global coordinate of the chunk to get.\n *\n * @returns The chunk at the given global coordinate (created if needed).\n *\n * **Side effects:** May create and register a new chunk.\n *\n * **Category:** Blocks\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!CapsuleColliderOptions#shape:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "getOrCreateChunk(globalCoordinate: " - }, - { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" - }, - { - "kind": "Content", - "text": "): " + "text": "shape: " }, { "kind": "Reference", - "text": "Chunk", - "canonicalReference": "server!Chunk:class" + "text": "ColliderShape.CAPSULE", + "canonicalReference": "server!ColliderShape.CAPSULE:member" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "globalCoordinate", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], + "isReadonly": false, "isOptional": false, - "isAbstract": false, - "name": "getOrCreateChunk" + "releaseTag": "Public", + "name": "shape", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + } + ], + "extendsTokenRanges": [ + { + "startIndex": 1, + "endIndex": 2 + } + ] + }, + { + "kind": "Class", + "canonicalReference": "server!ChaseBehavior:class", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "export default class ChaseBehavior implements " }, { - "kind": "Method", - "canonicalReference": "server!ChunkLattice#hasBlock:member(1)", - "docComment": "/**\n * Checks if a block exists at a specific global coordinate.\n *\n * @param globalCoordinate - The global coordinate of the block to check.\n *\n * @returns Whether a block exists.\n *\n * **Category:** Blocks\n */\n", + "kind": "Reference", + "text": "BotBehavior", + "canonicalReference": "server!BotBehavior:interface" + }, + { + "kind": "Content", + "text": " " + } + ], + "fileUrlPath": "src/bots/behaviors/ChaseBehavior.ts", + "releaseTag": "Public", + "isAbstract": false, + "name": "ChaseBehavior", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Constructor", + "canonicalReference": "server!ChaseBehavior:constructor(1)", + "docComment": "/**\n * Constructs a new instance of the `ChaseBehavior` class\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "hasBlock(globalCoordinate: " + "text": "constructor(options?: " }, { "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" - }, - { - "kind": "Content", - "text": "): " - }, - { - "kind": "Content", - "text": "boolean" + "text": "ChaseBehaviorOptions", + "canonicalReference": "server!ChaseBehaviorOptions:interface" }, { "kind": "Content", - "text": ";" + "text": ");" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "globalCoordinate", + "parameterName": "options", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isOptional": false + "isOptional": true } - ], - "isOptional": false, - "isAbstract": false, - "name": "hasBlock" + ] }, { - "kind": "Method", - "canonicalReference": "server!ChunkLattice#hasChunk:member(1)", - "docComment": "/**\n * Checks if a chunk exists for a given global coordinate.\n *\n * @param globalCoordinate - The global coordinate of the chunk to check.\n *\n * @returns Whether the chunk exists.\n *\n * **Category:** Blocks\n */\n", + "kind": "Property", + "canonicalReference": "server!ChaseBehavior#name:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "hasChunk(globalCoordinate: " - }, - { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" - }, - { - "kind": "Content", - "text": "): " + "text": "readonly name = " }, { "kind": "Content", - "text": "boolean" + "text": "\"chase\"" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, + "isReadonly": true, + "isOptional": false, "releaseTag": "Public", + "name": "name", + "propertyTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "globalCoordinate", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "hasChunk" + "isAbstract": false }, { "kind": "Method", - "canonicalReference": "server!ChunkLattice#initializeBlocks:member(1)", - "docComment": "/**\n * Initializes all blocks in the lattice in bulk, replacing existing blocks.\n *\n * Use for: loading maps or generating terrain in one pass. Do NOT use for: incremental edits; use `ChunkLattice.setBlock`.\n *\n * @remarks\n *\n * **Clears first:** Calls `ChunkLattice.clear` before initializing, removing all existing blocks and colliders.\n *\n * **Collider optimization:** Creates one collider per block type with all placements combined. Voxel colliders have their states combined for efficient neighbor collision detection.\n *\n * @param blocks - The blocks to initialize, keyed by block type ID.\n *\n * **Side effects:** Clears existing data, creates colliders, and emits `ChunkLatticeEvent.SET_BLOCK` per block.\n *\n * **Category:** Blocks\n */\n", + "canonicalReference": "server!ChaseBehavior#tick:member(1)", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "initializeBlocks(blocks: " - }, - { - "kind": "Content", - "text": "{\n [blockTypeId: number]: " + "text": "tick(bot: " }, { "kind": "Reference", - "text": "BlockPlacement", - "canonicalReference": "server!BlockPlacement:interface" - }, - { - "kind": "Content", - "text": "[];\n }" - }, - { - "kind": "Content", - "text": "): " - }, - { - "kind": "Content", - "text": "void" + "text": "BotPlayer", + "canonicalReference": "server!BotPlayer:class" }, { "kind": "Content", - "text": ";" - } - ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 5, - "endIndex": 6 - }, - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "blocks", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 4 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "initializeBlocks" - }, - { - "kind": "Method", - "canonicalReference": "server!ChunkLattice#setBlock:member(1)", - "docComment": "/**\n * Sets the block at a global coordinate by block type ID.\n *\n * Use for: incremental terrain edits. Do NOT use for: bulk terrain loading; use `ChunkLattice.initializeBlocks`.\n *\n * @remarks\n *\n * **Air:** Use block type ID `0` to remove a block (set to air).\n *\n * **Collider updates:** For voxel block types, updates the existing collider. For trimesh block types, recreates the entire collider.\n *\n * **Removes previous:** If replacing an existing block, removes it from its collider first. If the previous block type has no remaining blocks, its collider is removed from simulation.\n *\n * @param globalCoordinate - The global coordinate of the block to set.\n *\n * @param blockTypeId - The block type ID to set. Use 0 to remove the block and replace with air.\n *\n * @param blockRotation - The rotation of the block.\n *\n * **Side effects:** Emits `ChunkLatticeEvent.SET_BLOCK` and mutates block colliders.\n *\n * **Category:** Blocks\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "setBlock(globalCoordinate: " + "text": ", world: " }, { "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" + "text": "World", + "canonicalReference": "server!World:class" }, { "kind": "Content", - "text": ", blockTypeId: " + "text": ", deltaTimeMs: " }, { "kind": "Content", "text": "number" }, - { - "kind": "Content", - "text": ", blockRotation?: " - }, - { - "kind": "Reference", - "text": "BlockRotation", - "canonicalReference": "server!BlockRotation:type" - }, { "kind": "Content", "text": "): " @@ -8492,7 +8190,7 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "globalCoordinate", + "parameterName": "bot", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -8500,7 +8198,7 @@ "isOptional": false }, { - "parameterName": "blockTypeId", + "parameterName": "world", "parameterTypeTokenRange": { "startIndex": 3, "endIndex": 4 @@ -8508,73 +8206,152 @@ "isOptional": false }, { - "parameterName": "blockRotation", + "parameterName": "deltaTimeMs", "parameterTypeTokenRange": { "startIndex": 5, "endIndex": 6 }, - "isOptional": true + "isOptional": false } ], "isOptional": false, "isAbstract": false, - "name": "setBlock" + "name": "tick" } ], - "extendsTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "implementsTokenRanges": [] + "implementsTokenRanges": [ + { + "startIndex": 1, + "endIndex": 2 + } + ] }, { - "kind": "Enum", - "canonicalReference": "server!ChunkLatticeEvent:enum", - "docComment": "/**\n * Event types a ChunkLattice instance can emit.\n *\n * See `ChunkLatticeEventPayloads` for the payloads.\n *\n * **Category:** Events\n *\n * @public\n */\n", + "kind": "Interface", + "canonicalReference": "server!ChaseBehaviorOptions:interface", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "export declare enum ChunkLatticeEvent " + "text": "export interface ChaseBehaviorOptions " } ], - "fileUrlPath": "src/worlds/blocks/ChunkLattice.ts", + "fileUrlPath": "src/bots/behaviors/ChaseBehavior.ts", "releaseTag": "Public", - "name": "ChunkLatticeEvent", + "name": "ChaseBehaviorOptions", "preserveMemberOrder": false, "members": [ { - "kind": "EnumMember", - "canonicalReference": "server!ChunkLatticeEvent.ADD_CHUNK:member", + "kind": "PropertySignature", + "canonicalReference": "server!ChaseBehaviorOptions#chaseSpeed:member", "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "ADD_CHUNK = " + "text": "chaseSpeed?: " }, { "kind": "Content", - "text": "\"CHUNK_LATTICE.ADD_CHUNK\"" + "text": "number" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "chaseSpeed", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 - }, + } + }, + { + "kind": "PropertySignature", + "canonicalReference": "server!ChaseBehaviorOptions#detectionRadius:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "detectionRadius?: " + }, + { + "kind": "Content", + "text": "number" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "name": "ADD_CHUNK" + "name": "detectionRadius", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, + { + "kind": "PropertySignature", + "canonicalReference": "server!ChaseBehaviorOptions#updateIntervalMs:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "updateIntervalMs?: " + }, + { + "kind": "Content", + "text": "number" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "updateIntervalMs", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + } + ], + "extendsTokenRanges": [] + }, + { + "kind": "Enum", + "canonicalReference": "server!ChatEvent:enum", + "docComment": "/**\n * Event types a ChatManager instance can emit.\n *\n * See `ChatEventPayloads` for the payloads.\n *\n * **Category:** Events\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare enum ChatEvent " + } + ], + "fileUrlPath": "src/worlds/chat/ChatManager.ts", + "releaseTag": "Public", + "name": "ChatEvent", + "preserveMemberOrder": false, + "members": [ { "kind": "EnumMember", - "canonicalReference": "server!ChunkLatticeEvent.REMOVE_CHUNK:member", + "canonicalReference": "server!ChatEvent.BROADCAST_MESSAGE:member", "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "REMOVE_CHUNK = " + "text": "BROADCAST_MESSAGE = " }, { "kind": "Content", - "text": "\"CHUNK_LATTICE.REMOVE_CHUNK\"" + "text": "\"CHAT.BROADCAST_MESSAGE\"" } ], "initializerTokenRange": { @@ -8582,20 +8359,20 @@ "endIndex": 2 }, "releaseTag": "Public", - "name": "REMOVE_CHUNK" + "name": "BROADCAST_MESSAGE" }, { "kind": "EnumMember", - "canonicalReference": "server!ChunkLatticeEvent.SET_BLOCK:member", + "canonicalReference": "server!ChatEvent.PLAYER_MESSAGE:member", "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "SET_BLOCK = " + "text": "PLAYER_MESSAGE = " }, { "kind": "Content", - "text": "\"CHUNK_LATTICE.SET_BLOCK\"" + "text": "\"CHAT.PLAYER_MESSAGE\"" } ], "initializerTokenRange": { @@ -8603,29 +8380,29 @@ "endIndex": 2 }, "releaseTag": "Public", - "name": "SET_BLOCK" + "name": "PLAYER_MESSAGE" } ] }, { "kind": "Interface", - "canonicalReference": "server!ChunkLatticeEventPayloads:interface", - "docComment": "/**\n * Event payloads for ChunkLattice emitted events.\n *\n * **Category:** Events\n *\n * @public\n */\n", + "canonicalReference": "server!ChatEventPayloads:interface", + "docComment": "/**\n * Event payloads for ChatManager emitted events.\n *\n * **Category:** Events\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "export interface ChunkLatticeEventPayloads " + "text": "export interface ChatEventPayloads " } ], - "fileUrlPath": "src/worlds/blocks/ChunkLattice.ts", + "fileUrlPath": "src/worlds/chat/ChatManager.ts", "releaseTag": "Public", - "name": "ChunkLatticeEventPayloads", + "name": "ChatEventPayloads", "preserveMemberOrder": false, "members": [ { "kind": "PropertySignature", - "canonicalReference": "server!ChunkLatticeEventPayloads#\"CHUNK_LATTICE.ADD_CHUNK\":member", - "docComment": "/**\n * Emitted when a chunk is added to the lattice.\n */\n", + "canonicalReference": "server!ChatEventPayloads#\"CHAT.BROADCAST_MESSAGE\":member", + "docComment": "/**\n * Emitted when a broadcast message is sent.\n */\n", "excerptTokens": [ { "kind": "Content", @@ -8633,8 +8410,8 @@ }, { "kind": "Reference", - "text": "ChunkLatticeEvent.ADD_CHUNK", - "canonicalReference": "server!ChunkLatticeEvent.ADD_CHUNK:member" + "text": "ChatEvent.BROADCAST_MESSAGE", + "canonicalReference": "server!ChatEvent.BROADCAST_MESSAGE:member" }, { "kind": "Content", @@ -8642,79 +8419,16 @@ }, { "kind": "Content", - "text": "{\n chunkLattice: " - }, - { - "kind": "Reference", - "text": "ChunkLattice", - "canonicalReference": "server!ChunkLattice:class" - }, - { - "kind": "Content", - "text": ";\n chunk: " - }, - { - "kind": "Reference", - "text": "Chunk", - "canonicalReference": "server!Chunk:class" - }, - { - "kind": "Content", - "text": ";\n }" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "\"CHUNK_LATTICE.ADD_CHUNK\"", - "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 8 - } - }, - { - "kind": "PropertySignature", - "canonicalReference": "server!ChunkLatticeEventPayloads#\"CHUNK_LATTICE.REMOVE_CHUNK\":member", - "docComment": "/**\n * Emitted when a chunk is removed from the lattice.\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "[" - }, - { - "kind": "Reference", - "text": "ChunkLatticeEvent.REMOVE_CHUNK", - "canonicalReference": "server!ChunkLatticeEvent.REMOVE_CHUNK:member" - }, - { - "kind": "Content", - "text": "]: " - }, - { - "kind": "Content", - "text": "{\n chunkLattice: " - }, - { - "kind": "Reference", - "text": "ChunkLattice", - "canonicalReference": "server!ChunkLattice:class" - }, - { - "kind": "Content", - "text": ";\n chunk: " + "text": "{\n player: " }, { "kind": "Reference", - "text": "Chunk", - "canonicalReference": "server!Chunk:class" + "text": "Player", + "canonicalReference": "server!Player:class" }, { "kind": "Content", - "text": ";\n }" + "text": " | undefined;\n message: string;\n color?: string;\n }" }, { "kind": "Content", @@ -8724,16 +8438,16 @@ "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "\"CHUNK_LATTICE.REMOVE_CHUNK\"", + "name": "\"CHAT.BROADCAST_MESSAGE\"", "propertyTypeTokenRange": { "startIndex": 3, - "endIndex": 8 + "endIndex": 6 } }, { "kind": "PropertySignature", - "canonicalReference": "server!ChunkLatticeEventPayloads#\"CHUNK_LATTICE.SET_BLOCK\":member", - "docComment": "/**\n * Emitted when a block is set in the lattice.\n */\n", + "canonicalReference": "server!ChatEventPayloads#\"CHAT.PLAYER_MESSAGE\":member", + "docComment": "/**\n * Emitted when a message is sent to a specific player.\n */\n", "excerptTokens": [ { "kind": "Content", @@ -8741,8 +8455,8 @@ }, { "kind": "Reference", - "text": "ChunkLatticeEvent.SET_BLOCK", - "canonicalReference": "server!ChunkLatticeEvent.SET_BLOCK:member" + "text": "ChatEvent.PLAYER_MESSAGE", + "canonicalReference": "server!ChatEvent.PLAYER_MESSAGE:member" }, { "kind": "Content", @@ -8750,52 +8464,16 @@ }, { "kind": "Content", - "text": "{\n chunkLattice: " - }, - { - "kind": "Reference", - "text": "ChunkLattice", - "canonicalReference": "server!ChunkLattice:class" - }, - { - "kind": "Content", - "text": ";\n chunk: " - }, - { - "kind": "Reference", - "text": "Chunk", - "canonicalReference": "server!Chunk:class" - }, - { - "kind": "Content", - "text": ";\n globalCoordinate: " - }, - { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" - }, - { - "kind": "Content", - "text": ";\n localCoordinate: " - }, - { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" - }, - { - "kind": "Content", - "text": ";\n blockTypeId: number;\n blockRotation?: " + "text": "{\n player: " }, { "kind": "Reference", - "text": "BlockRotation", - "canonicalReference": "server!BlockRotation:type" + "text": "Player", + "canonicalReference": "server!Player:class" }, { "kind": "Content", - "text": ";\n }" + "text": ";\n message: string;\n color?: string;\n }" }, { "kind": "Content", @@ -8805,124 +8483,23 @@ "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "\"CHUNK_LATTICE.SET_BLOCK\"", + "name": "\"CHAT.PLAYER_MESSAGE\"", "propertyTypeTokenRange": { "startIndex": 3, - "endIndex": 14 + "endIndex": 6 } } ], "extendsTokenRanges": [] }, - { - "kind": "Enum", - "canonicalReference": "server!CoefficientCombineRule:enum", - "docComment": "/**\n * The coefficient for friction or bounciness combine rule.\n *\n * **Category:** Physics\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export declare enum CoefficientCombineRule " - } - ], - "fileUrlPath": "src/worlds/physics/Collider.ts", - "releaseTag": "Public", - "name": "CoefficientCombineRule", - "preserveMemberOrder": false, - "members": [ - { - "kind": "EnumMember", - "canonicalReference": "server!CoefficientCombineRule.Average:member", - "docComment": "", - "excerptTokens": [ - { - "kind": "Content", - "text": "Average = " - }, - { - "kind": "Content", - "text": "0" - } - ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "name": "Average" - }, - { - "kind": "EnumMember", - "canonicalReference": "server!CoefficientCombineRule.Max:member", - "docComment": "", - "excerptTokens": [ - { - "kind": "Content", - "text": "Max = " - }, - { - "kind": "Content", - "text": "3" - } - ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "name": "Max" - }, - { - "kind": "EnumMember", - "canonicalReference": "server!CoefficientCombineRule.Min:member", - "docComment": "", - "excerptTokens": [ - { - "kind": "Content", - "text": "Min = " - }, - { - "kind": "Content", - "text": "1" - } - ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "name": "Min" - }, - { - "kind": "EnumMember", - "canonicalReference": "server!CoefficientCombineRule.Multiply:member", - "docComment": "", - "excerptTokens": [ - { - "kind": "Content", - "text": "Multiply = " - }, - { - "kind": "Content", - "text": "2" - } - ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "name": "Multiply" - } - ] - }, { "kind": "Class", - "canonicalReference": "server!Collider:class", - "docComment": "/**\n * Represents a collider in a world's physics simulation.\n *\n * When to use: defining collision shapes for rigid bodies or entities. Do NOT use for: gameplay queries; use `Simulation.raycast` or intersection APIs instead.\n *\n * @remarks\n *\n * Colliders are usually created via `RigidBody` or `Entity` options. You can also create and manage them directly for advanced use cases.\n *\n * **Category:** Physics\n *\n * @public\n */\n", + "canonicalReference": "server!ChatManager:class", + "docComment": "/**\n * Manages chat and commands in a world.\n *\n * When to use: broadcasting chat, sending system messages, or registering chat commands. Do NOT use for: player HUD/menus; use `PlayerUI` for rich UI.\n *\n * @remarks\n *\n * The ChatManager is created internally as a singleton for each `World` instance in a game server. The ChatManager allows you to broadcast messages, send messages to specific players, and register commands that can be used in chat to execute game logic.\n *\n * Pattern: register commands during world initialization and keep callbacks fast. Anti-pattern: assuming commands are permission-checked; always validate access in callbacks.\n *\n *

Events

\n *\n * This class is an EventRouter, and instances of it emit events with payloads listed under `ChatEventPayloads`\n *\n * The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `ChatManager` class.\n *\n * @example\n * ```typescript\n * world.chatManager.registerCommand('/kick', (player, args, message) => {\n * const admins = [ 'arkdev', 'testuser123' ];\n * if (admins.includes(player.username)) {\n * const targetUsername = args[0];\n * const targetPlayer = world.playerManager.getConnectedPlayerByUsername(targetUsername);\n *\n * if (targetPlayer) {\n * targetPlayer.disconnect();\n * }\n * }\n * });\n * ```\n *\n * **Category:** Chat\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "export default class Collider extends " + "text": "export default class ChatManager extends " }, { "kind": "Reference", @@ -8934,67 +8511,98 @@ "text": " " } ], - "fileUrlPath": "src/worlds/physics/Collider.ts", + "fileUrlPath": "src/worlds/chat/ChatManager.ts", "releaseTag": "Public", "isAbstract": false, - "name": "Collider", + "name": "ChatManager", "preserveMemberOrder": false, "members": [ { - "kind": "Constructor", - "canonicalReference": "server!Collider:constructor(1)", - "docComment": "/**\n * Creates a collider with the provided options.\n *\n * Use for: configuring a collider before adding it to a simulation or rigid body.\n *\n * @param colliderOptions - The options for the collider instance.\n *\n * **Category:** Physics\n */\n", + "kind": "Method", + "canonicalReference": "server!ChatManager#handleCommand:member(1)", + "docComment": "/**\n * Handle a command if it exists.\n *\n * @remarks\n *\n * The command is parsed as the first space-delimited token in the message.\n *\n * **Category:** Chat\n *\n * @param player - The player that sent the command.\n *\n * @param message - The full message.\n *\n * @returns True if a command was handled, false otherwise.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "constructor(colliderOptions: " + "text": "handleCommand(player: " }, { "kind": "Reference", - "text": "ColliderOptions", - "canonicalReference": "server!ColliderOptions:type" + "text": "Player", + "canonicalReference": "server!Player:class" }, { "kind": "Content", - "text": ");" + "text": ", message: " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "boolean" + }, + { + "kind": "Content", + "text": ";" } ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "colliderOptions", + "parameterName": "player", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, "isOptional": false + }, + { + "parameterName": "message", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": false } - ] + ], + "isOptional": false, + "isAbstract": false, + "name": "handleCommand" }, { "kind": "Method", - "canonicalReference": "server!Collider#addToSimulation:member(1)", - "docComment": "/**\n * Adds the collider to the simulation.\n *\n * @remarks\n *\n * **Parent linking:** Links the collider to the parent rigid body if provided.\n *\n * **Collision callback:** Applies any configured `onCollision` callback.\n *\n * @param simulation - The simulation to add the collider to.\n *\n * @param parentRigidBody - The parent rigid body of the collider.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!ChatManager#registerCommand:member(1)", + "docComment": "/**\n * Register a command and its callback.\n *\n * @remarks\n *\n * Commands are matched by exact string equality against the first token in a chat message.\n *\n * @param command - The command to register.\n *\n * @param callback - The callback function to execute when the command is used.\n *\n * **Requires:** Use a consistent command prefix (for example, `/kick`) if you want slash commands.\n *\n * @see\n *\n * `ChatManager.unregisterCommand`\n *\n * **Category:** Chat\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "addToSimulation(simulation: " + "text": "registerCommand(command: " }, { - "kind": "Reference", - "text": "Simulation", - "canonicalReference": "server!Simulation:class" + "kind": "Content", + "text": "string" }, { "kind": "Content", - "text": ", parentRigidBody?: " + "text": ", callback: " }, { "kind": "Reference", - "text": "RigidBody", - "canonicalReference": "server!RigidBody:class" + "text": "CommandCallback", + "canonicalReference": "server!CommandCallback:type" }, { "kind": "Content", @@ -9019,7 +8627,7 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "simulation", + "parameterName": "command", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -9027,122 +8635,111 @@ "isOptional": false }, { - "parameterName": "parentRigidBody", + "parameterName": "callback", "parameterTypeTokenRange": { "startIndex": 3, "endIndex": 4 }, - "isOptional": true + "isOptional": false } ], "isOptional": false, "isAbstract": false, - "name": "addToSimulation" + "name": "registerCommand" }, { - "kind": "Property", - "canonicalReference": "server!Collider#bounciness:member", - "docComment": "/**\n * The bounciness of the collider.\n *\n * **Category:** Physics\n */\n", + "kind": "Method", + "canonicalReference": "server!ChatManager#sendBroadcastMessage:member(1)", + "docComment": "/**\n * Send a system broadcast message to all players in the world.\n *\n * @param message - The message to send.\n *\n * @param color - The color of the message as a hex color code, excluding #.\n *\n * @example\n * ```typescript\n * chatManager.sendBroadcastMessage('Hello, world!', 'FF00AA');\n * ```\n *\n * **Side effects:** Emits `ChatEvent.BROADCAST_MESSAGE` for network sync.\n *\n * @see\n *\n * `ChatManager.sendPlayerMessage`\n *\n * **Category:** Chat\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get bounciness(): " + "text": "sendBroadcastMessage(message: " }, { "kind": "Content", - "text": "number" + "text": "string" }, { "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "bounciness", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "server!Collider#bouncinessCombineRule:member", - "docComment": "/**\n * The bounciness combine rule of the collider.\n *\n * **Category:** Physics\n */\n", - "excerptTokens": [ + "text": ", color?: " + }, { "kind": "Content", - "text": "get bouncinessCombineRule(): " + "text": "string" }, { - "kind": "Reference", - "text": "CoefficientCombineRule", - "canonicalReference": "server!CoefficientCombineRule:enum" + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "bouncinessCombineRule", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "releaseTag": "Public", "isProtected": false, - "isAbstract": false + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "message", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "color", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": true + } + ], + "isOptional": false, + "isAbstract": false, + "name": "sendBroadcastMessage" }, { - "kind": "Property", - "canonicalReference": "server!Collider#collisionGroups:member", - "docComment": "/**\n * The collision groups the collider belongs to.\n *\n * **Category:** Physics\n */\n", + "kind": "Method", + "canonicalReference": "server!ChatManager#sendPlayerMessage:member(1)", + "docComment": "/**\n * Send a system message to a specific player, only visible to them.\n *\n * @param player - The player to send the message to.\n *\n * @param message - The message to send.\n *\n * @param color - The color of the message as a hex color code, excluding #.\n *\n * @example\n * ```typescript\n * chatManager.sendPlayerMessage(player, 'Hello, player!', 'FF00AA');\n * ```\n *\n * **Side effects:** Emits `ChatEvent.PLAYER_MESSAGE` for network sync.\n *\n * @see\n *\n * `ChatManager.sendBroadcastMessage`\n *\n * **Category:** Chat\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get collisionGroups(): " + "text": "sendPlayerMessage(player: " }, { "kind": "Reference", - "text": "CollisionGroups", - "canonicalReference": "server!CollisionGroups:type" + "text": "Player", + "canonicalReference": "server!Player:class" }, { "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "collisionGroups", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Method", - "canonicalReference": "server!Collider#enableCollisionEvents:member(1)", - "docComment": "/**\n * Enables or disables collision events for the collider. This is automatically enabled if an on collision callback is set.\n *\n * @param enabled - Whether collision events are enabled.\n *\n * **Category:** Physics\n */\n", - "excerptTokens": [ + "text": ", message: " + }, { "kind": "Content", - "text": "enableCollisionEvents(enabled: " + "text": "string" }, { "kind": "Content", - "text": "boolean" + "text": ", color?: " + }, + { + "kind": "Content", + "text": "string" }, { "kind": "Content", @@ -9159,38 +8756,54 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 + "startIndex": 7, + "endIndex": 8 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "enabled", + "parameterName": "player", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, "isOptional": false + }, + { + "parameterName": "message", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": false + }, + { + "parameterName": "color", + "parameterTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "isOptional": true } ], "isOptional": false, "isAbstract": false, - "name": "enableCollisionEvents" + "name": "sendPlayerMessage" }, { "kind": "Method", - "canonicalReference": "server!Collider#enableContactForceEvents:member(1)", - "docComment": "/**\n * Enables or disables contact force events for the collider. This is automatically enabled if an on contact force callback is set.\n *\n * @param enabled - Whether contact force events are enabled.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!ChatManager#unregisterCommand:member(1)", + "docComment": "/**\n * Unregister a command.\n *\n * @param command - The command to unregister.\n *\n * @see\n *\n * `ChatManager.registerCommand`\n *\n * **Category:** Chat\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "enableContactForceEvents(enabled: " + "text": "unregisterCommand(command: " }, { "kind": "Content", - "text": "boolean" + "text": "string" }, { "kind": "Content", @@ -9215,7 +8828,7 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "enabled", + "parameterName": "command", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -9225,81 +8838,157 @@ ], "isOptional": false, "isAbstract": false, - "name": "enableContactForceEvents" + "name": "unregisterCommand" + } + ], + "extendsTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "implementsTokenRanges": [] + }, + { + "kind": "Class", + "canonicalReference": "server!Chunk:class", + "docComment": "/**\n * A 16^3 chunk of blocks representing a slice of world terrain.\n *\n * When to use: reading chunk data or working with bulk block operations. Do NOT use for: creating terrain directly; prefer `ChunkLattice`.\n *\n * @remarks\n *\n * Chunks are fixed-size (16×16×16) and store block IDs by local coordinates.\n *\n *

Coordinate System

\n *\n * - **Global (world) coordinates:** integer block positions in world space. - **Chunk origin:** the world coordinate at the chunk's minimum corner (multiples of 16). - **Local coordinates:** 0..15 per axis within the chunk.\n *\n * **Category:** Blocks\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export default class Chunk implements " }, { - "kind": "Property", - "canonicalReference": "server!Collider#friction:member", - "docComment": "/**\n * The friction of the collider.\n *\n * **Category:** Physics\n */\n", + "kind": "Reference", + "text": "protocol.Serializable", + "canonicalReference": "@hytopia.com/server-protocol!Serializable:interface" + }, + { + "kind": "Content", + "text": " " + } + ], + "fileUrlPath": "src/worlds/blocks/Chunk.ts", + "releaseTag": "Public", + "isAbstract": false, + "name": "Chunk", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Constructor", + "canonicalReference": "server!Chunk:constructor(1)", + "docComment": "/**\n * Creates a new chunk instance.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get friction(): " + "text": "constructor(originCoordinate: " }, { - "kind": "Content", - "text": "number" + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", - "text": ";" + "text": ");" } ], - "isReadonly": true, - "isOptional": false, "releaseTag": "Public", - "name": "friction", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, "isProtected": false, - "isAbstract": false + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "originCoordinate", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ] }, { - "kind": "Property", - "canonicalReference": "server!Collider#frictionCombineRule:member", - "docComment": "/**\n * The friction combine rule of the collider.\n *\n * **Category:** Physics\n */\n", + "kind": "Method", + "canonicalReference": "server!Chunk.blockIndexToLocalCoordinate:member(1)", + "docComment": "/**\n * Converts a block index to a local coordinate.\n *\n * @param index - The index of the block to convert.\n *\n * @returns The local coordinate of the block.\n *\n * **Category:** Blocks\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get frictionCombineRule(): " + "text": "static blockIndexToLocalCoordinate(index: " + }, + { + "kind": "Content", + "text": "number" + }, + { + "kind": "Content", + "text": "): " }, { "kind": "Reference", - "text": "CoefficientCombineRule", - "canonicalReference": "server!CoefficientCombineRule:enum" + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "frictionCombineRule", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 + "isStatic": true, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 }, - "isStatic": false, + "releaseTag": "Public", "isProtected": false, - "isAbstract": false + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "index", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "blockIndexToLocalCoordinate" }, { "kind": "Property", - "canonicalReference": "server!Collider#isBall:member", - "docComment": "/**\n * Whether the collider is a ball collider.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!Chunk#blockRotations:member", + "docComment": "/**\n * The rotations of the blocks in the chunk as a map of block index to rotation.\n *\n * **Category:** Blocks\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get isBall(): " + "text": "get blockRotations(): " + }, + { + "kind": "Reference", + "text": "Readonly", + "canonicalReference": "!Readonly:type" }, { "kind": "Content", - "text": "boolean" + "text": "<" + }, + { + "kind": "Reference", + "text": "Map", + "canonicalReference": "!Map:interface" + }, + { + "kind": "Content", + "text": ">" }, { "kind": "Content", @@ -9309,10 +8998,10 @@ "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "isBall", + "name": "blockRotations", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 2 + "endIndex": 7 }, "isStatic": false, "isProtected": false, @@ -9320,16 +9009,30 @@ }, { "kind": "Property", - "canonicalReference": "server!Collider#isBlock:member", - "docComment": "/**\n * Whether the collider is a block collider.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!Chunk#blocks:member", + "docComment": "/**\n * The blocks in the chunk as a flat Uint8Array[4096], each index as 0 or a block type ID.\n *\n * **Category:** Blocks\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get isBlock(): " + "text": "get blocks(): " + }, + { + "kind": "Reference", + "text": "Readonly", + "canonicalReference": "!Readonly:type" }, { "kind": "Content", - "text": "boolean" + "text": "<" + }, + { + "kind": "Reference", + "text": "Uint8Array", + "canonicalReference": "!Uint8Array:interface" + }, + { + "kind": "Content", + "text": ">" }, { "kind": "Content", @@ -9339,143 +9042,231 @@ "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "isBlock", + "name": "blocks", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 2 + "endIndex": 5 }, "isStatic": false, "isProtected": false, "isAbstract": false }, { - "kind": "Property", - "canonicalReference": "server!Collider#isCapsule:member", - "docComment": "/**\n * Whether the collider is a capsule collider.\n *\n * **Category:** Physics\n */\n", + "kind": "Method", + "canonicalReference": "server!Chunk#getBlockId:member(1)", + "docComment": "/**\n * Gets the block type ID at a specific local coordinate.\n *\n * @remarks\n *\n * Expects local coordinates in the range 0..15 for each axis.\n *\n * @param localCoordinate - The local coordinate of the block to get.\n *\n * @returns The block type ID.\n *\n * **Category:** Blocks\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get isCapsule(): " + "text": "getBlockId(localCoordinate: " + }, + { + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", - "text": "boolean" + "text": "): " + }, + { + "kind": "Content", + "text": "number" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "isCapsule", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "releaseTag": "Public", "isProtected": false, - "isAbstract": false + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "localCoordinate", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "getBlockId" }, { - "kind": "Property", - "canonicalReference": "server!Collider#isCone:member", - "docComment": "/**\n * Whether the collider is a cone collider.\n *\n * **Category:** Physics\n */\n", + "kind": "Method", + "canonicalReference": "server!Chunk#getBlockRotation:member(1)", + "docComment": "/**\n * Gets the rotation of a block at a specific local coordinate.\n *\n * @param localCoordinate - The local coordinate of the block to get the rotation of.\n *\n * @returns The rotation of the block (defaults to identity rotation).\n *\n * **Category:** Blocks\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get isCone(): " + "text": "getBlockRotation(localCoordinate: " + }, + { + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", - "text": "boolean" + "text": "): " + }, + { + "kind": "Reference", + "text": "BlockRotation", + "canonicalReference": "server!BlockRotation:type" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "isCone", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "releaseTag": "Public", "isProtected": false, - "isAbstract": false + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "localCoordinate", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "getBlockRotation" }, { - "kind": "Property", - "canonicalReference": "server!Collider#isCylinder:member", - "docComment": "/**\n * Whether the collider is a cylinder collider.\n *\n * **Category:** Physics\n */\n", + "kind": "Method", + "canonicalReference": "server!Chunk.globalCoordinateToLocalCoordinate:member(1)", + "docComment": "/**\n * Converts a global coordinate to a local coordinate.\n *\n * @param globalCoordinate - The global coordinate to convert.\n *\n * @returns The local coordinate.\n *\n * **Category:** Blocks\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get isCylinder(): " + "text": "static globalCoordinateToLocalCoordinate(globalCoordinate: " + }, + { + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", - "text": "boolean" + "text": "): " + }, + { + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "isCylinder", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 + "isStatic": true, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 }, - "isStatic": false, + "releaseTag": "Public", "isProtected": false, - "isAbstract": false + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "globalCoordinate", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "globalCoordinateToLocalCoordinate" }, { - "kind": "Property", - "canonicalReference": "server!Collider#isEnabled:member", - "docComment": "/**\n * Whether the collider is enabled.\n *\n * **Category:** Physics\n */\n", + "kind": "Method", + "canonicalReference": "server!Chunk.globalCoordinateToOriginCoordinate:member(1)", + "docComment": "/**\n * Converts a global coordinate to a chunk origin coordinate.\n *\n * @param globalCoordinate - The global coordinate to convert.\n *\n * @returns The origin coordinate.\n *\n * **Category:** Blocks\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get isEnabled(): " + "text": "static globalCoordinateToOriginCoordinate(globalCoordinate: " + }, + { + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", - "text": "boolean" + "text": "): " + }, + { + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "isEnabled", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 + "isStatic": true, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 }, - "isStatic": false, + "releaseTag": "Public", "isProtected": false, - "isAbstract": false + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "globalCoordinate", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "globalCoordinateToOriginCoordinate" }, { - "kind": "Property", - "canonicalReference": "server!Collider#isNone:member", - "docComment": "/**\n * Whether the collider is a none collider.\n *\n * **Category:** Physics\n */\n", + "kind": "Method", + "canonicalReference": "server!Chunk#hasBlock:member(1)", + "docComment": "/**\n * Checks if a block exists at a specific local coordinate.\n *\n * @param localCoordinate - The local coordinate of the block to check.\n *\n * @returns Whether a block exists.\n *\n * **Category:** Blocks\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get isNone(): " + "text": "hasBlock(localCoordinate: " + }, + { + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" + }, + { + "kind": "Content", + "text": "): " }, { "kind": "Content", @@ -9486,30 +9277,41 @@ "text": ";" } ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "isNone", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "releaseTag": "Public", "isProtected": false, - "isAbstract": false + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "localCoordinate", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "hasBlock" }, { "kind": "Property", - "canonicalReference": "server!Collider#isRemoved:member", - "docComment": "/**\n * Whether the collider has been removed from the simulation.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!Chunk#originCoordinate:member", + "docComment": "/**\n * The origin coordinate of the chunk (world-space, multiples of 16).\n *\n * **Category:** Blocks\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get isRemoved(): " + "text": "get originCoordinate(): " }, { - "kind": "Content", - "text": "boolean" + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", @@ -9519,7 +9321,7 @@ "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "isRemoved", + "name": "originCoordinate", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -9527,49 +9329,85 @@ "isStatic": false, "isProtected": false, "isAbstract": false + } + ], + "implementsTokenRanges": [ + { + "startIndex": 1, + "endIndex": 2 + } + ] + }, + { + "kind": "Class", + "canonicalReference": "server!ChunkLattice:class", + "docComment": "/**\n * A lattice of chunks that represent a world's terrain.\n *\n * When to use: reading or mutating blocks in world space. Do NOT use for: per-entity placement logic; prefer higher-level game systems.\n *\n * @remarks\n *\n * The lattice owns all chunks and keeps physics colliders in sync with blocks.\n *\n *

Coordinate System

\n *\n * - **Global (world) coordinates:** integer block positions in world space. - **Chunk origin:** world coordinate at the chunk's minimum corner (multiples of 16). - **Local coordinates:** 0..15 per axis within a chunk. - **Axes:** +X right, +Y up, -Z forward. - **Origin:** (0,0,0) is the world origin.\n *\n * **Category:** Blocks\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export default class ChunkLattice extends " }, { - "kind": "Property", - "canonicalReference": "server!Collider#isRoundCylinder:member", - "docComment": "/**\n * Whether the collider is a round cylinder collider.\n *\n * **Category:** Physics\n */\n", + "kind": "Reference", + "text": "EventRouter", + "canonicalReference": "server!EventRouter:class" + }, + { + "kind": "Content", + "text": " " + } + ], + "fileUrlPath": "src/worlds/blocks/ChunkLattice.ts", + "releaseTag": "Public", + "isAbstract": false, + "name": "ChunkLattice", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Constructor", + "canonicalReference": "server!ChunkLattice:constructor(1)", + "docComment": "/**\n * Creates a new chunk lattice instance.\n *\n * @param world - The world the chunk lattice is for.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get isRoundCylinder(): " + "text": "constructor(world: " }, { - "kind": "Content", - "text": "boolean" + "kind": "Reference", + "text": "World", + "canonicalReference": "server!World:class" }, { "kind": "Content", - "text": ";" + "text": ");" } ], - "isReadonly": true, - "isOptional": false, "releaseTag": "Public", - "name": "isRoundCylinder", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, "isProtected": false, - "isAbstract": false + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "world", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ] }, { "kind": "Property", - "canonicalReference": "server!Collider#isSensor:member", - "docComment": "/**\n * Whether the collider is a sensor.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!ChunkLattice#chunkCount:member", + "docComment": "/**\n * The number of chunks in the lattice.\n *\n * **Category:** Blocks\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get isSensor(): " + "text": "get chunkCount(): " }, { "kind": "Content", - "text": "boolean" + "text": "number" }, { "kind": "Content", @@ -9579,7 +9417,7 @@ "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "isSensor", + "name": "chunkCount", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -9589,133 +9427,80 @@ "isAbstract": false }, { - "kind": "Property", - "canonicalReference": "server!Collider#isSimulated:member", - "docComment": "/**\n * Whether the collider is simulated.\n *\n * **Category:** Physics\n */\n", + "kind": "Method", + "canonicalReference": "server!ChunkLattice#clear:member(1)", + "docComment": "/**\n * Removes and clears all chunks and their blocks from the lattice.\n *\n * Use for: full world resets or map reloads. Do NOT use for: incremental changes; use `ChunkLattice.setBlock`.\n *\n * @remarks\n *\n * **Removes colliders:** All block type colliders are removed from the physics simulation.\n *\n * **Emits events:** Emits `REMOVE_CHUNK` for each chunk before clearing.\n *\n * **Side effects:** Clears all chunks, placements, and block colliders.\n *\n * **Category:** Blocks\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get isSimulated(): " + "text": "clear(): " }, { "kind": "Content", - "text": "boolean" + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "isSimulated", - "propertyTypeTokenRange": { + "isStatic": false, + "returnTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": false, + "releaseTag": "Public", "isProtected": false, - "isAbstract": false + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "clear" }, { - "kind": "Property", - "canonicalReference": "server!Collider#isTrimesh:member", - "docComment": "/**\n * Whether the collider is a trimesh collider.\n *\n * **Category:** Physics\n */\n", + "kind": "Method", + "canonicalReference": "server!ChunkLattice#getAllChunks:member(1)", + "docComment": "/**\n * Gets all chunks in the lattice.\n *\n * @returns An array of all chunks in the lattice.\n *\n * **Category:** Blocks\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get isTrimesh(): " + "text": "getAllChunks(): " + }, + { + "kind": "Reference", + "text": "Chunk", + "canonicalReference": "server!Chunk:class" }, { "kind": "Content", - "text": "boolean" + "text": "[]" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "isTrimesh", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "server!Collider#isVoxel:member", - "docComment": "/**\n * Whether the collider is a voxel collider.\n *\n * **Category:** Physics\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "get isVoxel(): " - }, - { - "kind": "Content", - "text": "boolean" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "isVoxel", - "propertyTypeTokenRange": { + "returnTypeTokenRange": { "startIndex": 1, - "endIndex": 2 + "endIndex": 3 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "server!Collider#isWedge:member", - "docComment": "/**\n * Whether the collider is a wedge collider.\n *\n * **Category:** Physics\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "get isWedge(): " - }, - { - "kind": "Content", - "text": "boolean" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, "releaseTag": "Public", - "name": "isWedge", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, "isProtected": false, - "isAbstract": false + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "getAllChunks" }, { "kind": "Method", - "canonicalReference": "server!Collider.optionsFromBlockHalfExtents:member(1)", - "docComment": "/**\n * Creates collider options from a block's half extents.\n *\n * @param halfExtents - The half extents of the block.\n *\n * @returns The collider options object.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!ChunkLattice#getBlockId:member(1)", + "docComment": "/**\n * Gets the block type ID at a specific global coordinate.\n *\n * @param globalCoordinate - The global coordinate of the block to get.\n *\n * @returns The block type ID, or 0 if no block is set.\n *\n * **Category:** Blocks\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "static optionsFromBlockHalfExtents(halfExtents: " + "text": "getBlockId(globalCoordinate: " }, { "kind": "Reference", @@ -9727,16 +9512,15 @@ "text": "): " }, { - "kind": "Reference", - "text": "ColliderOptions", - "canonicalReference": "server!ColliderOptions:type" + "kind": "Content", + "text": "number" }, { "kind": "Content", "text": ";" } ], - "isStatic": true, + "isStatic": false, "returnTypeTokenRange": { "startIndex": 3, "endIndex": 4 @@ -9746,7 +9530,7 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "halfExtents", + "parameterName": "globalCoordinate", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -9756,24 +9540,16 @@ ], "isOptional": false, "isAbstract": false, - "name": "optionsFromBlockHalfExtents" + "name": "getBlockId" }, { "kind": "Method", - "canonicalReference": "server!Collider.optionsFromModelUri:member(1)", - "docComment": "/**\n * Creates collider options from a model URI using an approximate shape and size.\n *\n * @remarks\n *\n * Uses model bounds and heuristics unless `preferredShape` is specified.\n *\n * @param modelUri - The URI of the model.\n *\n * @param scale - The scale of the model.\n *\n * @param preferredShape - The preferred shape to use for the collider.\n *\n * @returns The collider options object.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!ChunkLattice#getBlockType:member(1)", + "docComment": "/**\n * Gets the block type at a specific global coordinate.\n *\n * @param globalCoordinate - The global coordinate of the block to get.\n *\n * @returns The block type, or null if no block is set.\n *\n * **Category:** Blocks\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "static optionsFromModelUri(modelUri: " - }, - { - "kind": "Content", - "text": "string" - }, - { - "kind": "Content", - "text": ", scale?: " + "text": "getBlockType(globalCoordinate: " }, { "kind": "Reference", @@ -9782,306 +9558,52 @@ }, { "kind": "Content", - "text": " | number" - }, - { - "kind": "Content", - "text": ", preferredShape?: " + "text": "): " }, { "kind": "Reference", - "text": "ColliderShape", - "canonicalReference": "server!ColliderShape:enum" + "text": "BlockType", + "canonicalReference": "server!BlockType:class" }, { "kind": "Content", - "text": "): " - }, - { - "kind": "Reference", - "text": "ColliderOptions", - "canonicalReference": "server!ColliderOptions:type" + "text": " | null" }, { "kind": "Content", "text": ";" } ], - "isStatic": true, + "isStatic": false, "returnTypeTokenRange": { - "startIndex": 8, - "endIndex": 9 + "startIndex": 3, + "endIndex": 5 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "modelUri", + "parameterName": "globalCoordinate", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, "isOptional": false - }, - { - "parameterName": "scale", - "parameterTypeTokenRange": { - "startIndex": 3, - "endIndex": 5 - }, - "isOptional": true - }, - { - "parameterName": "preferredShape", - "parameterTypeTokenRange": { - "startIndex": 6, - "endIndex": 7 - }, - "isOptional": true - } - ], - "isOptional": false, - "isAbstract": false, - "name": "optionsFromModelUri" - }, - { - "kind": "Property", - "canonicalReference": "server!Collider#parentRigidBody:member", - "docComment": "/**\n * The parent rigid body of the collider.\n *\n * **Category:** Physics\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "get parentRigidBody(): " - }, - { - "kind": "Reference", - "text": "RigidBody", - "canonicalReference": "server!RigidBody:class" - }, - { - "kind": "Content", - "text": " | undefined" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "parentRigidBody", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "server!Collider#rawCollider:member", - "docComment": "/**\n * The raw collider object from the Rapier physics engine.\n *\n * **Category:** Physics\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "get rawCollider(): " - }, - { - "kind": "Reference", - "text": "RawCollider", - "canonicalReference": "server!RawCollider:type" - }, - { - "kind": "Content", - "text": " | undefined" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "rawCollider", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "server!Collider#rawShape:member", - "docComment": "/**\n * The raw shape object from the Rapier physics engine.\n *\n * **Category:** Physics\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "get rawShape(): " - }, - { - "kind": "Reference", - "text": "RawShape", - "canonicalReference": "server!RawShape:type" - }, - { - "kind": "Content", - "text": " | undefined" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "rawShape", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "server!Collider#relativePosition:member", - "docComment": "/**\n * The relative position of the collider to its parent rigid body.\n *\n * **Category:** Physics\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "get relativePosition(): " - }, - { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "relativePosition", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "server!Collider#relativeRotation:member", - "docComment": "/**\n * The relative rotation of the collider.\n *\n * **Category:** Physics\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "get relativeRotation(): " - }, - { - "kind": "Reference", - "text": "QuaternionLike", - "canonicalReference": "server!QuaternionLike:interface" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "relativeRotation", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Method", - "canonicalReference": "server!Collider#removeFromSimulation:member(1)", - "docComment": "/**\n * Removes the collider from the simulation.\n *\n * @remarks\n *\n * **Parent unlinking:** Unlinks from parent rigid body if attached.\n *\n * **Side effects:** Removes the collider from the simulation and unlinks it from any parent rigid body.\n *\n * **Category:** Physics\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "removeFromSimulation(): " - }, - { - "kind": "Content", - "text": "void" - }, - { - "kind": "Content", - "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [], "isOptional": false, "isAbstract": false, - "name": "removeFromSimulation" - }, - { - "kind": "Property", - "canonicalReference": "server!Collider#scale:member", - "docComment": "/**\n * The scale of the collider.\n *\n * **Category:** Physics\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "get scale(): " - }, - { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "scale", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "name": "getBlockType" }, { "kind": "Method", - "canonicalReference": "server!Collider#setBounciness:member(1)", - "docComment": "/**\n * Sets the bounciness of the collider.\n *\n * @param bounciness - The bounciness of the collider.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!ChunkLattice#getBlockTypeCount:member(1)", + "docComment": "/**\n * Gets the number of blocks of a specific block type in the lattice.\n *\n * @param blockTypeId - The block type ID to count.\n *\n * @returns The number of blocks of the block type.\n *\n * **Category:** Blocks\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setBounciness(bounciness: " + "text": "getBlockTypeCount(blockTypeId: " }, { "kind": "Content", @@ -10093,7 +9615,7 @@ }, { "kind": "Content", - "text": "void" + "text": "number" }, { "kind": "Content", @@ -10110,7 +9632,7 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "bounciness", + "parameterName": "blockTypeId", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -10120,29 +9642,34 @@ ], "isOptional": false, "isAbstract": false, - "name": "setBounciness" + "name": "getBlockTypeCount" }, { "kind": "Method", - "canonicalReference": "server!Collider#setBouncinessCombineRule:member(1)", - "docComment": "/**\n * Sets the bounciness combine rule of the collider.\n *\n * @param bouncinessCombineRule - The bounciness combine rule of the collider.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!ChunkLattice#getChunk:member(1)", + "docComment": "/**\n * Gets the chunk that contains the given global coordinate.\n *\n * @param globalCoordinate - The global coordinate to get the chunk for.\n *\n * @returns The chunk that contains the given global coordinate or undefined if not found.\n *\n * **Category:** Blocks\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setBouncinessCombineRule(bouncinessCombineRule: " + "text": "getChunk(globalCoordinate: " }, { "kind": "Reference", - "text": "CoefficientCombineRule", - "canonicalReference": "server!CoefficientCombineRule:enum" + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", "text": "): " }, + { + "kind": "Reference", + "text": "Chunk", + "canonicalReference": "server!Chunk:class" + }, { "kind": "Content", - "text": "void" + "text": " | undefined" }, { "kind": "Content", @@ -10152,14 +9679,14 @@ "isStatic": false, "returnTypeTokenRange": { "startIndex": 3, - "endIndex": 4 + "endIndex": 5 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "bouncinessCombineRule", + "parameterName": "globalCoordinate", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -10169,29 +9696,30 @@ ], "isOptional": false, "isAbstract": false, - "name": "setBouncinessCombineRule" + "name": "getChunk" }, { "kind": "Method", - "canonicalReference": "server!Collider#setCollisionGroups:member(1)", - "docComment": "/**\n * Sets the collision groups of the collider.\n *\n * @param collisionGroups - The collision groups of the collider.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!ChunkLattice#getOrCreateChunk:member(1)", + "docComment": "/**\n * Gets the chunk for a given global coordinate, creating it if it doesn't exist.\n *\n * @remarks\n *\n * Creates a new chunk and emits `ChunkLatticeEvent.ADD_CHUNK` if needed.\n *\n * @param globalCoordinate - The global coordinate of the chunk to get.\n *\n * @returns The chunk at the given global coordinate (created if needed).\n *\n * **Side effects:** May create and register a new chunk.\n *\n * **Category:** Blocks\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setCollisionGroups(collisionGroups: " + "text": "getOrCreateChunk(globalCoordinate: " }, { "kind": "Reference", - "text": "CollisionGroups", - "canonicalReference": "server!CollisionGroups:type" + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", "text": "): " }, { - "kind": "Content", - "text": "void" + "kind": "Reference", + "text": "Chunk", + "canonicalReference": "server!Chunk:class" }, { "kind": "Content", @@ -10208,7 +9736,7 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "collisionGroups", + "parameterName": "globalCoordinate", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -10218,20 +9746,21 @@ ], "isOptional": false, "isAbstract": false, - "name": "setCollisionGroups" + "name": "getOrCreateChunk" }, { "kind": "Method", - "canonicalReference": "server!Collider#setEnabled:member(1)", - "docComment": "/**\n * Sets whether the collider is enabled.\n *\n * @param enabled - Whether the collider is enabled.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!ChunkLattice#hasBlock:member(1)", + "docComment": "/**\n * Checks if a block exists at a specific global coordinate.\n *\n * @param globalCoordinate - The global coordinate of the block to check.\n *\n * @returns Whether a block exists.\n *\n * **Category:** Blocks\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setEnabled(enabled: " + "text": "hasBlock(globalCoordinate: " }, { - "kind": "Content", - "text": "boolean" + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", @@ -10239,7 +9768,7 @@ }, { "kind": "Content", - "text": "void" + "text": "boolean" }, { "kind": "Content", @@ -10256,7 +9785,7 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "enabled", + "parameterName": "globalCoordinate", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -10266,20 +9795,21 @@ ], "isOptional": false, "isAbstract": false, - "name": "setEnabled" + "name": "hasBlock" }, { "kind": "Method", - "canonicalReference": "server!Collider#setFriction:member(1)", - "docComment": "/**\n * Sets the friction of the collider.\n *\n * @param friction - The friction of the collider.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!ChunkLattice#hasChunk:member(1)", + "docComment": "/**\n * Checks if a chunk exists for a given global coordinate.\n *\n * @param globalCoordinate - The global coordinate of the chunk to check.\n *\n * @returns Whether the chunk exists.\n *\n * **Category:** Blocks\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setFriction(friction: " + "text": "hasChunk(globalCoordinate: " }, { - "kind": "Content", - "text": "number" + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", @@ -10287,7 +9817,7 @@ }, { "kind": "Content", - "text": "void" + "text": "boolean" }, { "kind": "Content", @@ -10304,7 +9834,7 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "friction", + "parameterName": "globalCoordinate", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -10314,21 +9844,29 @@ ], "isOptional": false, "isAbstract": false, - "name": "setFriction" + "name": "hasChunk" }, { "kind": "Method", - "canonicalReference": "server!Collider#setFrictionCombineRule:member(1)", - "docComment": "/**\n * Sets the friction combine rule of the collider.\n *\n * @param frictionCombineRule - The friction combine rule of the collider.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!ChunkLattice#initializeBlocks:member(1)", + "docComment": "/**\n * Initializes all blocks in the lattice in bulk, replacing existing blocks.\n *\n * Use for: loading maps or generating terrain in one pass. Do NOT use for: incremental edits; use `ChunkLattice.setBlock`.\n *\n * @remarks\n *\n * **Clears first:** Calls `ChunkLattice.clear` before initializing, removing all existing blocks and colliders.\n *\n * **Collider optimization:** Creates one collider per block type with all placements combined. Voxel colliders have their states combined for efficient neighbor collision detection.\n *\n * @param blocks - The blocks to initialize, keyed by block type ID.\n *\n * **Side effects:** Clears existing data, creates colliders, and emits `ChunkLatticeEvent.SET_BLOCK` per block.\n *\n * **Category:** Blocks\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setFrictionCombineRule(frictionCombineRule: " + "text": "initializeBlocks(blocks: " + }, + { + "kind": "Content", + "text": "{\n [blockTypeId: number]: " }, { "kind": "Reference", - "text": "CoefficientCombineRule", - "canonicalReference": "server!CoefficientCombineRule:enum" + "text": "BlockPlacement", + "canonicalReference": "server!BlockPlacement:interface" + }, + { + "kind": "Content", + "text": "[];\n }" }, { "kind": "Content", @@ -10345,40 +9883,57 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 + "startIndex": 5, + "endIndex": 6 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "frictionCombineRule", + "parameterName": "blocks", "parameterTypeTokenRange": { "startIndex": 1, - "endIndex": 2 + "endIndex": 4 }, "isOptional": false } ], "isOptional": false, "isAbstract": false, - "name": "setFrictionCombineRule" + "name": "initializeBlocks" }, { "kind": "Method", - "canonicalReference": "server!Collider#setHalfExtents:member(1)", - "docComment": "/**\n * Sets the half extents of a simulated block collider.\n *\n * @param halfExtents - The half extents of the block collider.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!ChunkLattice#setBlock:member(1)", + "docComment": "/**\n * Sets the block at a global coordinate by block type ID.\n *\n * Use for: incremental terrain edits. Do NOT use for: bulk terrain loading; use `ChunkLattice.initializeBlocks`.\n *\n * @remarks\n *\n * **Air:** Use block type ID `0` to remove a block (set to air).\n *\n * **Collider updates:** For voxel block types, updates the existing collider. For trimesh block types, recreates the entire collider.\n *\n * **Removes previous:** If replacing an existing block, removes it from its collider first. If the previous block type has no remaining blocks, its collider is removed from simulation.\n *\n * @param globalCoordinate - The global coordinate of the block to set.\n *\n * @param blockTypeId - The block type ID to set. Use 0 to remove the block and replace with air.\n *\n * @param blockRotation - The rotation of the block.\n *\n * **Side effects:** Emits `ChunkLatticeEvent.SET_BLOCK` and mutates block colliders.\n *\n * **Category:** Blocks\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setHalfExtents(halfExtents: " + "text": "setBlock(globalCoordinate: " }, { "kind": "Reference", "text": "Vector3Like", "canonicalReference": "server!Vector3Like:interface" }, + { + "kind": "Content", + "text": ", blockTypeId: " + }, + { + "kind": "Content", + "text": "number" + }, + { + "kind": "Content", + "text": ", blockRotation?: " + }, + { + "kind": "Reference", + "text": "BlockRotation", + "canonicalReference": "server!BlockRotation:type" + }, { "kind": "Content", "text": "): " @@ -10394,285 +9949,517 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 + "startIndex": 7, + "endIndex": 8 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "halfExtents", + "parameterName": "globalCoordinate", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, "isOptional": false + }, + { + "parameterName": "blockTypeId", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": false + }, + { + "parameterName": "blockRotation", + "parameterTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "isOptional": true } ], "isOptional": false, "isAbstract": false, - "name": "setHalfExtents" - }, + "name": "setBlock" + } + ], + "extendsTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "implementsTokenRanges": [] + }, + { + "kind": "Enum", + "canonicalReference": "server!ChunkLatticeEvent:enum", + "docComment": "/**\n * Event types a ChunkLattice instance can emit.\n *\n * See `ChunkLatticeEventPayloads` for the payloads.\n *\n * **Category:** Events\n *\n * @public\n */\n", + "excerptTokens": [ { - "kind": "Method", - "canonicalReference": "server!Collider#setHalfHeight:member(1)", - "docComment": "/**\n * Sets the half height of a simulated capsule, cone, cylinder, or round cylinder collider.\n *\n * @param halfHeight - The half height of the capsule, cone, cylinder, or round cylinder collider.\n *\n * **Category:** Physics\n */\n", + "kind": "Content", + "text": "export declare enum ChunkLatticeEvent " + } + ], + "fileUrlPath": "src/worlds/blocks/ChunkLattice.ts", + "releaseTag": "Public", + "name": "ChunkLatticeEvent", + "preserveMemberOrder": false, + "members": [ + { + "kind": "EnumMember", + "canonicalReference": "server!ChunkLatticeEvent.ADD_CHUNK:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "setHalfHeight(halfHeight: " - }, - { - "kind": "Content", - "text": "number" + "text": "ADD_CHUNK = " }, { "kind": "Content", - "text": "): " - }, + "text": "\"CHUNK_LATTICE.ADD_CHUNK\"" + } + ], + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "ADD_CHUNK" + }, + { + "kind": "EnumMember", + "canonicalReference": "server!ChunkLatticeEvent.REMOVE_CHUNK:member", + "docComment": "", + "excerptTokens": [ { "kind": "Content", - "text": "void" + "text": "REMOVE_CHUNK = " }, { "kind": "Content", - "text": ";" + "text": "\"CHUNK_LATTICE.REMOVE_CHUNK\"" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 }, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ + "name": "REMOVE_CHUNK" + }, + { + "kind": "EnumMember", + "canonicalReference": "server!ChunkLatticeEvent.SET_BLOCK:member", + "docComment": "", + "excerptTokens": [ { - "parameterName": "halfHeight", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false + "kind": "Content", + "text": "SET_BLOCK = " + }, + { + "kind": "Content", + "text": "\"CHUNK_LATTICE.SET_BLOCK\"" } ], - "isOptional": false, - "isAbstract": false, - "name": "setHalfHeight" - }, + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "SET_BLOCK" + } + ] + }, + { + "kind": "Interface", + "canonicalReference": "server!ChunkLatticeEventPayloads:interface", + "docComment": "/**\n * Event payloads for ChunkLattice emitted events.\n *\n * **Category:** Events\n *\n * @public\n */\n", + "excerptTokens": [ { - "kind": "Method", - "canonicalReference": "server!Collider#setMass:member(1)", - "docComment": "/**\n * Sets the mass of the collider.\n *\n * @param mass - The mass of the collider.\n *\n * **Category:** Physics\n */\n", + "kind": "Content", + "text": "export interface ChunkLatticeEventPayloads " + } + ], + "fileUrlPath": "src/worlds/blocks/ChunkLattice.ts", + "releaseTag": "Public", + "name": "ChunkLatticeEventPayloads", + "preserveMemberOrder": false, + "members": [ + { + "kind": "PropertySignature", + "canonicalReference": "server!ChunkLatticeEventPayloads#\"CHUNK_LATTICE.ADD_CHUNK\":member", + "docComment": "/**\n * Emitted when a chunk is added to the lattice.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setMass(mass: " + "text": "[" + }, + { + "kind": "Reference", + "text": "ChunkLatticeEvent.ADD_CHUNK", + "canonicalReference": "server!ChunkLatticeEvent.ADD_CHUNK:member" }, { "kind": "Content", - "text": "number" + "text": "]: " }, { "kind": "Content", - "text": "): " + "text": "{\n chunkLattice: " + }, + { + "kind": "Reference", + "text": "ChunkLattice", + "canonicalReference": "server!ChunkLattice:class" }, { "kind": "Content", - "text": "void" + "text": ";\n chunk: " + }, + { + "kind": "Reference", + "text": "Chunk", + "canonicalReference": "server!Chunk:class" }, { "kind": "Content", - "text": ";" - } - ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ + "text": ";\n }" + }, { - "parameterName": "mass", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false + "kind": "Content", + "text": ";" } ], + "isReadonly": false, "isOptional": false, - "isAbstract": false, - "name": "setMass" + "releaseTag": "Public", + "name": "\"CHUNK_LATTICE.ADD_CHUNK\"", + "propertyTypeTokenRange": { + "startIndex": 3, + "endIndex": 8 + } }, { - "kind": "Method", - "canonicalReference": "server!Collider#setOnCollision:member(1)", - "docComment": "/**\n * Sets the on collision callback for the collider.\n *\n * @remarks\n *\n * **Auto-enables events:** Automatically enables/disables collision events based on whether callback is set.\n *\n * @param callback - The on collision callback for the collider.\n *\n * **Category:** Physics\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!ChunkLatticeEventPayloads#\"CHUNK_LATTICE.REMOVE_CHUNK\":member", + "docComment": "/**\n * Emitted when a chunk is removed from the lattice.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setOnCollision(callback: " + "text": "[" }, { "kind": "Reference", - "text": "CollisionCallback", - "canonicalReference": "server!CollisionCallback:type" + "text": "ChunkLatticeEvent.REMOVE_CHUNK", + "canonicalReference": "server!ChunkLatticeEvent.REMOVE_CHUNK:member" }, { "kind": "Content", - "text": " | undefined" + "text": "]: " }, { "kind": "Content", - "text": "): " + "text": "{\n chunkLattice: " + }, + { + "kind": "Reference", + "text": "ChunkLattice", + "canonicalReference": "server!ChunkLattice:class" }, { "kind": "Content", - "text": "void" + "text": ";\n chunk: " + }, + { + "kind": "Reference", + "text": "Chunk", + "canonicalReference": "server!Chunk:class" }, { "kind": "Content", - "text": ";" - } - ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 4, - "endIndex": 5 - }, - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ + "text": ";\n }" + }, { - "parameterName": "callback", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - }, - "isOptional": false + "kind": "Content", + "text": ";" } ], + "isReadonly": false, "isOptional": false, - "isAbstract": false, - "name": "setOnCollision" + "releaseTag": "Public", + "name": "\"CHUNK_LATTICE.REMOVE_CHUNK\"", + "propertyTypeTokenRange": { + "startIndex": 3, + "endIndex": 8 + } }, { - "kind": "Method", - "canonicalReference": "server!Collider#setRadius:member(1)", - "docComment": "/**\n * Sets the radius of a simulated ball, capsule, cylinder, or round cylinder collider.\n *\n * @param radius - The radius of the collider.\n *\n * **Category:** Physics\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!ChunkLatticeEventPayloads#\"CHUNK_LATTICE.SET_BLOCK\":member", + "docComment": "/**\n * Emitted when a block is set in the lattice.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setRadius(radius: " + "text": "[" + }, + { + "kind": "Reference", + "text": "ChunkLatticeEvent.SET_BLOCK", + "canonicalReference": "server!ChunkLatticeEvent.SET_BLOCK:member" }, { "kind": "Content", - "text": "number" + "text": "]: " }, { "kind": "Content", - "text": "): " + "text": "{\n chunkLattice: " + }, + { + "kind": "Reference", + "text": "ChunkLattice", + "canonicalReference": "server!ChunkLattice:class" }, { "kind": "Content", - "text": "void" + "text": ";\n chunk: " + }, + { + "kind": "Reference", + "text": "Chunk", + "canonicalReference": "server!Chunk:class" + }, + { + "kind": "Content", + "text": ";\n globalCoordinate: " + }, + { + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" + }, + { + "kind": "Content", + "text": ";\n localCoordinate: " + }, + { + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" + }, + { + "kind": "Content", + "text": ";\n blockTypeId: number;\n blockRotation?: " + }, + { + "kind": "Reference", + "text": "BlockRotation", + "canonicalReference": "server!BlockRotation:type" + }, + { + "kind": "Content", + "text": ";\n }" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "\"CHUNK_LATTICE.SET_BLOCK\"", + "propertyTypeTokenRange": { "startIndex": 3, - "endIndex": 4 + "endIndex": 14 + } + } + ], + "extendsTokenRanges": [] + }, + { + "kind": "Enum", + "canonicalReference": "server!CoefficientCombineRule:enum", + "docComment": "/**\n * The coefficient for friction or bounciness combine rule.\n *\n * **Category:** Physics\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare enum CoefficientCombineRule " + } + ], + "fileUrlPath": "src/worlds/physics/Collider.ts", + "releaseTag": "Public", + "name": "CoefficientCombineRule", + "preserveMemberOrder": false, + "members": [ + { + "kind": "EnumMember", + "canonicalReference": "server!CoefficientCombineRule.Average:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "Average = " + }, + { + "kind": "Content", + "text": "0" + } + ], + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 }, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ + "name": "Average" + }, + { + "kind": "EnumMember", + "canonicalReference": "server!CoefficientCombineRule.Max:member", + "docComment": "", + "excerptTokens": [ { - "parameterName": "radius", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false + "kind": "Content", + "text": "Max = " + }, + { + "kind": "Content", + "text": "3" } ], - "isOptional": false, - "isAbstract": false, - "name": "setRadius" + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "Max" }, { - "kind": "Method", - "canonicalReference": "server!Collider#setRelativePosition:member(1)", - "docComment": "/**\n * Sets the position of the collider relative to its parent rigid body or the world origin.\n *\n * @remarks\n *\n * Colliders can be added as a child of a rigid body, or to the world directly. This position is relative to the parent rigid body or the world origin.\n *\n * @param position - The relative position of the collider.\n *\n * **Category:** Physics\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!CoefficientCombineRule.Min:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "setRelativePosition(position: " + "text": "Min = " }, { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" - }, + "kind": "Content", + "text": "1" + } + ], + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "Min" + }, + { + "kind": "EnumMember", + "canonicalReference": "server!CoefficientCombineRule.Multiply:member", + "docComment": "", + "excerptTokens": [ { "kind": "Content", - "text": "): " + "text": "Multiply = " }, { "kind": "Content", - "text": "void" + "text": "2" + } + ], + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "Multiply" + } + ] + }, + { + "kind": "Class", + "canonicalReference": "server!Collider:class", + "docComment": "/**\n * Represents a collider in a world's physics simulation.\n *\n * When to use: defining collision shapes for rigid bodies or entities. Do NOT use for: gameplay queries; use `Simulation.raycast` or intersection APIs instead.\n *\n * @remarks\n *\n * Colliders are usually created via `RigidBody` or `Entity` options. You can also create and manage them directly for advanced use cases.\n *\n * **Category:** Physics\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export default class Collider extends " + }, + { + "kind": "Reference", + "text": "EventRouter", + "canonicalReference": "server!EventRouter:class" + }, + { + "kind": "Content", + "text": " " + } + ], + "fileUrlPath": "src/worlds/physics/Collider.ts", + "releaseTag": "Public", + "isAbstract": false, + "name": "Collider", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Constructor", + "canonicalReference": "server!Collider:constructor(1)", + "docComment": "/**\n * Creates a collider with the provided options.\n *\n * Use for: configuring a collider before adding it to a simulation or rigid body.\n *\n * @param colliderOptions - The options for the collider instance.\n *\n * **Category:** Physics\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "constructor(colliderOptions: " + }, + { + "kind": "Reference", + "text": "ColliderOptions", + "canonicalReference": "server!ColliderOptions:type" }, { "kind": "Content", - "text": ";" + "text": ");" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "position", + "parameterName": "colliderOptions", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, "isOptional": false } - ], - "isOptional": false, - "isAbstract": false, - "name": "setRelativePosition" + ] }, { "kind": "Method", - "canonicalReference": "server!Collider#setRelativeRotation:member(1)", - "docComment": "/**\n * Sets the relative rotation of the collider to its parent rigid body or the world origin.\n *\n * @remarks\n *\n * Colliders can be added as a child of a rigid body, or to the world directly. This rotation is relative to the parent rigid body or the world origin.\n *\n * @param rotation - The relative rotation of the collider.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!Collider#addToSimulation:member(1)", + "docComment": "/**\n * Adds the collider to the simulation.\n *\n * @remarks\n *\n * **Parent linking:** Links the collider to the parent rigid body if provided.\n *\n * **Collision callback:** Applies any configured `onCollision` callback.\n *\n * @param simulation - The simulation to add the collider to.\n *\n * @param parentRigidBody - The parent rigid body of the collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setRelativeRotation(rotation: " + "text": "addToSimulation(simulation: " }, { "kind": "Reference", - "text": "QuaternionLike", - "canonicalReference": "server!QuaternionLike:interface" + "text": "Simulation", + "canonicalReference": "server!Simulation:class" + }, + { + "kind": "Content", + "text": ", parentRigidBody?: " + }, + { + "kind": "Reference", + "text": "RigidBody", + "canonicalReference": "server!RigidBody:class" }, { "kind": "Content", @@ -10689,135 +10476,138 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 + "startIndex": 5, + "endIndex": 6 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "rotation", + "parameterName": "simulation", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, "isOptional": false - } - ], - "isOptional": false, + }, + { + "parameterName": "parentRigidBody", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": true + } + ], + "isOptional": false, "isAbstract": false, - "name": "setRelativeRotation" + "name": "addToSimulation" }, { - "kind": "Method", - "canonicalReference": "server!Collider#setScale:member(1)", - "docComment": "/**\n * Scales the collider by the given scalar. Only ball, block, capsule, cone, cylinder, round cylinder are supported.\n *\n * @remarks\n *\n * **Ratio-based:** Uses ratio-based scaling relative to current scale, not absolute dimensions. Also scales `relativePosition` proportionally.\n *\n * @param scalar - The scalar to scale the collider by.\n *\n * **Category:** Physics\n */\n", + "kind": "Property", + "canonicalReference": "server!Collider#bounciness:member", + "docComment": "/**\n * The bounciness of the collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setScale(scale: " - }, - { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" - }, - { - "kind": "Content", - "text": "): " + "text": "get bounciness(): " }, { "kind": "Content", - "text": "void" + "text": "number" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, + "isReadonly": true, + "isOptional": false, "releaseTag": "Public", + "name": "bounciness", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "scale", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "setScale" + "isAbstract": false }, { - "kind": "Method", - "canonicalReference": "server!Collider#setSensor:member(1)", - "docComment": "/**\n * Sets whether the collider is a sensor.\n *\n * @param sensor - Whether the collider is a sensor.\n *\n * **Category:** Physics\n */\n", + "kind": "Property", + "canonicalReference": "server!Collider#bouncinessCombineRule:member", + "docComment": "/**\n * The bounciness combine rule of the collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setSensor(sensor: " + "text": "get bouncinessCombineRule(): " }, { - "kind": "Content", - "text": "boolean" + "kind": "Reference", + "text": "CoefficientCombineRule", + "canonicalReference": "server!CoefficientCombineRule:enum" }, { "kind": "Content", - "text": "): " - }, + "text": ";" + } + ], + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "bouncinessCombineRule", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Property", + "canonicalReference": "server!Collider#collisionGroups:member", + "docComment": "/**\n * The collision groups the collider belongs to.\n *\n * **Category:** Physics\n */\n", + "excerptTokens": [ { "kind": "Content", - "text": "void" + "text": "get collisionGroups(): " + }, + { + "kind": "Reference", + "text": "CollisionGroups", + "canonicalReference": "server!CollisionGroups:type" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, + "isReadonly": true, + "isOptional": false, "releaseTag": "Public", + "name": "collisionGroups", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "sensor", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "setSensor" + "isAbstract": false }, { "kind": "Method", - "canonicalReference": "server!Collider#setTag:member(1)", - "docComment": "/**\n * Sets the tag of the collider.\n *\n * @param tag - The tag of the collider.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!Collider#enableCollisionEvents:member(1)", + "docComment": "/**\n * Enables or disables collision events for the collider. This is automatically enabled if an on collision callback is set.\n *\n * @param enabled - Whether collision events are enabled.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setTag(tag: " + "text": "enableCollisionEvents(enabled: " }, { "kind": "Content", - "text": "string" + "text": "boolean" }, { "kind": "Content", @@ -10842,7 +10632,7 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "tag", + "parameterName": "enabled", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -10852,25 +10642,16 @@ ], "isOptional": false, "isAbstract": false, - "name": "setTag" + "name": "enableCollisionEvents" }, { "kind": "Method", - "canonicalReference": "server!Collider#setVoxel:member(1)", - "docComment": "/**\n * Sets the voxel at the given coordinate as filled or not filled.\n *\n * @param coordinate - The coordinate of the voxel to set.\n *\n * @param filled - True if the voxel at the coordinate should be filled, false if it should be removed.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!Collider#enableContactForceEvents:member(1)", + "docComment": "/**\n * Enables or disables contact force events for the collider. This is automatically enabled if an on contact force callback is set.\n *\n * @param enabled - Whether contact force events are enabled.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setVoxel(coordinate: " - }, - { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" - }, - { - "kind": "Content", - "text": ", filled: " + "text": "enableContactForceEvents(enabled: " }, { "kind": "Content", @@ -10891,47 +10672,38 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 5, - "endIndex": 6 + "startIndex": 3, + "endIndex": 4 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "coordinate", + "parameterName": "enabled", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, "isOptional": false - }, - { - "parameterName": "filled", - "parameterTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, - "isOptional": false } ], "isOptional": false, "isAbstract": false, - "name": "setVoxel" + "name": "enableContactForceEvents" }, { "kind": "Property", - "canonicalReference": "server!Collider#shape:member", - "docComment": "/**\n * The shape of the collider.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!Collider#friction:member", + "docComment": "/**\n * The friction of the collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get shape(): " + "text": "get friction(): " }, { - "kind": "Reference", - "text": "ColliderShape", - "canonicalReference": "server!ColliderShape:enum" + "kind": "Content", + "text": "number" }, { "kind": "Content", @@ -10941,7 +10713,7 @@ "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "shape", + "name": "friction", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -10952,16 +10724,17 @@ }, { "kind": "Property", - "canonicalReference": "server!Collider#tag:member", - "docComment": "/**\n * An arbitrary identifier tag of the collider. Useful for your own logic.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!Collider#frictionCombineRule:member", + "docComment": "/**\n * The friction combine rule of the collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get tag(): " + "text": "get frictionCombineRule(): " }, { - "kind": "Content", - "text": "string | undefined" + "kind": "Reference", + "text": "CoefficientCombineRule", + "canonicalReference": "server!CoefficientCombineRule:enum" }, { "kind": "Content", @@ -10971,7 +10744,7 @@ "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "tag", + "name": "frictionCombineRule", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -10979,853 +10752,898 @@ "isStatic": false, "isProtected": false, "isAbstract": false - } - ], - "extendsTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "implementsTokenRanges": [] - }, - { - "kind": "TypeAlias", - "canonicalReference": "server!ColliderOptions:type", - "docComment": "/**\n * The options for a collider.\n *\n * Use for: providing collider definitions when creating rigid bodies or entities. Do NOT use for: runtime changes; use `Collider` APIs instead.\n *\n * **Category:** Physics\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export type ColliderOptions = " - }, - { - "kind": "Reference", - "text": "BallColliderOptions", - "canonicalReference": "server!BallColliderOptions:interface" - }, - { - "kind": "Content", - "text": " | " - }, - { - "kind": "Reference", - "text": "BlockColliderOptions", - "canonicalReference": "server!BlockColliderOptions:interface" - }, - { - "kind": "Content", - "text": " | " - }, - { - "kind": "Reference", - "text": "CapsuleColliderOptions", - "canonicalReference": "server!CapsuleColliderOptions:interface" - }, - { - "kind": "Content", - "text": " | " - }, - { - "kind": "Reference", - "text": "ConeColliderOptions", - "canonicalReference": "server!ConeColliderOptions:interface" - }, - { - "kind": "Content", - "text": " | " - }, - { - "kind": "Reference", - "text": "CylinderColliderOptions", - "canonicalReference": "server!CylinderColliderOptions:interface" }, { - "kind": "Content", - "text": " | " - }, - { - "kind": "Reference", - "text": "RoundCylinderColliderOptions", - "canonicalReference": "server!RoundCylinderColliderOptions:interface" - }, - { - "kind": "Content", - "text": " | " - }, - { - "kind": "Reference", - "text": "TrimeshColliderOptions", - "canonicalReference": "server!TrimeshColliderOptions:interface" - }, - { - "kind": "Content", - "text": " | " - }, - { - "kind": "Reference", - "text": "VoxelsColliderOptions", - "canonicalReference": "server!VoxelsColliderOptions:interface" - }, - { - "kind": "Content", - "text": " | " - }, - { - "kind": "Reference", - "text": "WedgeColliderOptions", - "canonicalReference": "server!WedgeColliderOptions:interface" - }, - { - "kind": "Content", - "text": " | " - }, - { - "kind": "Reference", - "text": "NoneColliderOptions", - "canonicalReference": "server!NoneColliderOptions:interface" - }, - { - "kind": "Content", - "text": ";" - } - ], - "fileUrlPath": "src/worlds/physics/Collider.ts", - "releaseTag": "Public", - "name": "ColliderOptions", - "typeTokenRange": { - "startIndex": 1, - "endIndex": 20 - } - }, - { - "kind": "Enum", - "canonicalReference": "server!ColliderShape:enum", - "docComment": "/**\n * The shapes a collider can be.\n *\n * **Category:** Physics\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export declare enum ColliderShape " - } - ], - "fileUrlPath": "src/worlds/physics/Collider.ts", - "releaseTag": "Public", - "name": "ColliderShape", - "preserveMemberOrder": false, - "members": [ - { - "kind": "EnumMember", - "canonicalReference": "server!ColliderShape.BALL:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Collider#isBall:member", + "docComment": "/**\n * Whether the collider is a ball collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "BALL = " + "text": "get isBall(): " }, { "kind": "Content", - "text": "\"ball\"" + "text": "boolean" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "isBall", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", - "name": "BALL" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!ColliderShape.BLOCK:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Collider#isBlock:member", + "docComment": "/**\n * Whether the collider is a block collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "BLOCK = " + "text": "get isBlock(): " }, { "kind": "Content", - "text": "\"block\"" + "text": "boolean" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "isBlock", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", - "name": "BLOCK" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!ColliderShape.CAPSULE:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Collider#isCapsule:member", + "docComment": "/**\n * Whether the collider is a capsule collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "CAPSULE = " + "text": "get isCapsule(): " }, { "kind": "Content", - "text": "\"capsule\"" + "text": "boolean" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "isCapsule", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", - "name": "CAPSULE" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!ColliderShape.CONE:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Collider#isCone:member", + "docComment": "/**\n * Whether the collider is a cone collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "CONE = " + "text": "get isCone(): " }, { "kind": "Content", - "text": "\"cone\"" + "text": "boolean" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "isCone", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", - "name": "CONE" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!ColliderShape.CYLINDER:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Collider#isCylinder:member", + "docComment": "/**\n * Whether the collider is a cylinder collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "CYLINDER = " + "text": "get isCylinder(): " }, { "kind": "Content", - "text": "\"cylinder\"" + "text": "boolean" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "isCylinder", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", - "name": "CYLINDER" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!ColliderShape.NONE:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Collider#isEnabled:member", + "docComment": "/**\n * Whether the collider is enabled.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "NONE = " + "text": "get isEnabled(): " }, { "kind": "Content", - "text": "\"none\"" + "text": "boolean" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "isEnabled", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", - "name": "NONE" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!ColliderShape.ROUND_CYLINDER:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Collider#isNone:member", + "docComment": "/**\n * Whether the collider is a none collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "ROUND_CYLINDER = " + "text": "get isNone(): " }, { "kind": "Content", - "text": "\"round-cylinder\"" + "text": "boolean" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "isNone", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", - "name": "ROUND_CYLINDER" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!ColliderShape.TRIMESH:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Collider#isRemoved:member", + "docComment": "/**\n * Whether the collider has been removed from the simulation.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "TRIMESH = " + "text": "get isRemoved(): " }, { "kind": "Content", - "text": "\"trimesh\"" + "text": "boolean" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "isRemoved", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", - "name": "TRIMESH" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!ColliderShape.VOXELS:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Collider#isRoundCylinder:member", + "docComment": "/**\n * Whether the collider is a round cylinder collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "VOXELS = " + "text": "get isRoundCylinder(): " }, { "kind": "Content", - "text": "\"voxels\"" + "text": "boolean" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "isRoundCylinder", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", - "name": "VOXELS" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!ColliderShape.WEDGE:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Collider#isSensor:member", + "docComment": "/**\n * Whether the collider is a sensor.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "WEDGE = " + "text": "get isSensor(): " }, { "kind": "Content", - "text": "\"wedge\"" + "text": "boolean" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "isSensor", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", - "name": "WEDGE" - } - ] - }, - { - "kind": "TypeAlias", - "canonicalReference": "server!CollisionCallback:type", - "docComment": "/**\n * A callback function that is called when a collision occurs.\n *\n * @param other - The other object involved in the collision, a block or entity.\n *\n * @param started - Whether the collision has started or ended.\n *\n * **Category:** Physics\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export type CollisionCallback = " - }, - { - "kind": "Content", - "text": "((other: " - }, - { - "kind": "Reference", - "text": "BlockType", - "canonicalReference": "server!BlockType:class" - }, - { - "kind": "Content", - "text": " | " - }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" - }, - { - "kind": "Content", - "text": ", started: boolean) => void) | ((other: " - }, - { - "kind": "Reference", - "text": "BlockType", - "canonicalReference": "server!BlockType:class" - }, - { - "kind": "Content", - "text": " | " - }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" - }, - { - "kind": "Content", - "text": ", started: boolean, colliderHandleA: number, colliderHandleB: number) => void)" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "Content", - "text": ";" - } - ], - "fileUrlPath": "src/worlds/physics/ColliderMap.ts", - "releaseTag": "Public", - "name": "CollisionCallback", - "typeTokenRange": { - "startIndex": 1, - "endIndex": 10 - } - }, - { - "kind": "Enum", - "canonicalReference": "server!CollisionGroup:enum", - "docComment": "/**\n * The default collision groups.\n *\n * @remarks\n *\n * Collision groups determine which objects collide and generate events. Up to 15 groups can be registered. Filtering uses pairwise bit masks:\n *\n * - The belongsTo groups (the 16 left-most bits of `self.0`) - The collidesWith mask (the 16 right-most bits of `self.0`)\n *\n * An interaction is allowed between two filters `a` and `b` if:\n * ```\n * ((a >> 16) & b) != 0 && ((b >> 16) & a) != 0\n * ```\n *\n * **Category:** Physics\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export declare enum CollisionGroup " - } - ], - "fileUrlPath": "src/worlds/physics/CollisionGroupsBuilder.ts", - "releaseTag": "Public", - "name": "CollisionGroup", - "preserveMemberOrder": false, - "members": [ - { - "kind": "EnumMember", - "canonicalReference": "server!CollisionGroup.ALL:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Collider#isSimulated:member", + "docComment": "/**\n * Whether the collider is simulated.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "ALL = " + "text": "get isSimulated(): " }, { "kind": "Content", - "text": "65535" + "text": "boolean" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "isSimulated", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", - "name": "ALL" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!CollisionGroup.BLOCK:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Collider#isTrimesh:member", + "docComment": "/**\n * Whether the collider is a trimesh collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "BLOCK = " + "text": "get isTrimesh(): " }, { "kind": "Content", - "text": "1" + "text": "boolean" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "isTrimesh", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", - "name": "BLOCK" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!CollisionGroup.ENTITY:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Collider#isVoxel:member", + "docComment": "/**\n * Whether the collider is a voxel collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "ENTITY = " + "text": "get isVoxel(): " }, { "kind": "Content", - "text": "2" + "text": "boolean" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "isVoxel", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", - "name": "ENTITY" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!CollisionGroup.ENTITY_SENSOR:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Collider#isWedge:member", + "docComment": "/**\n * Whether the collider is a wedge collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "ENTITY_SENSOR = " + "text": "get isWedge(): " }, { "kind": "Content", - "text": "4" + "text": "boolean" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "isWedge", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", - "name": "ENTITY_SENSOR" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!CollisionGroup.ENVIRONMENT_ENTITY:member", - "docComment": "", + "kind": "Method", + "canonicalReference": "server!Collider.optionsFromBlockHalfExtents:member(1)", + "docComment": "/**\n * Creates collider options from a block's half extents.\n *\n * @param halfExtents - The half extents of the block.\n *\n * @returns The collider options object.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "ENVIRONMENT_ENTITY = " + "text": "static optionsFromBlockHalfExtents(halfExtents: " + }, + { + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", - "text": "8" + "text": "): " + }, + { + "kind": "Reference", + "text": "ColliderOptions", + "canonicalReference": "server!ColliderOptions:type" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 + "isStatic": true, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 }, "releaseTag": "Public", - "name": "ENVIRONMENT_ENTITY" + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "halfExtents", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "optionsFromBlockHalfExtents" }, { - "kind": "EnumMember", - "canonicalReference": "server!CollisionGroup.GROUP_1:member", - "docComment": "", + "kind": "Method", + "canonicalReference": "server!Collider.optionsFromModelUri:member(1)", + "docComment": "/**\n * Creates collider options from a model URI using an approximate shape and size.\n *\n * @remarks\n *\n * Uses model bounds and heuristics unless `preferredShape` is specified.\n *\n * @param modelUri - The URI of the model.\n *\n * @param scale - The scale of the model.\n *\n * @param preferredShape - The preferred shape to use for the collider.\n *\n * @returns The collider options object.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "GROUP_1 = " + "text": "static optionsFromModelUri(modelUri: " }, { "kind": "Content", - "text": "32" + "text": "string" + }, + { + "kind": "Content", + "text": ", scale?: " + }, + { + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" + }, + { + "kind": "Content", + "text": " | number" + }, + { + "kind": "Content", + "text": ", preferredShape?: " + }, + { + "kind": "Reference", + "text": "ColliderShape", + "canonicalReference": "server!ColliderShape:enum" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "ColliderOptions", + "canonicalReference": "server!ColliderOptions:type" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 + "isStatic": true, + "returnTypeTokenRange": { + "startIndex": 8, + "endIndex": 9 }, "releaseTag": "Public", - "name": "GROUP_1" + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "modelUri", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "scale", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 5 + }, + "isOptional": true + }, + { + "parameterName": "preferredShape", + "parameterTypeTokenRange": { + "startIndex": 6, + "endIndex": 7 + }, + "isOptional": true + } + ], + "isOptional": false, + "isAbstract": false, + "name": "optionsFromModelUri" }, { - "kind": "EnumMember", - "canonicalReference": "server!CollisionGroup.GROUP_10:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Collider#parentRigidBody:member", + "docComment": "/**\n * The parent rigid body of the collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "GROUP_10 = " + "text": "get parentRigidBody(): " + }, + { + "kind": "Reference", + "text": "RigidBody", + "canonicalReference": "server!RigidBody:class" }, { "kind": "Content", - "text": "16384" + "text": " | undefined" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "parentRigidBody", + "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 2 + "endIndex": 3 }, - "releaseTag": "Public", - "name": "GROUP_10" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!CollisionGroup.GROUP_11:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Collider#rawCollider:member", + "docComment": "/**\n * The raw collider object from the Rapier physics engine.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "GROUP_11 = " + "text": "get rawCollider(): " + }, + { + "kind": "Reference", + "text": "RawCollider", + "canonicalReference": "server!RawCollider:type" }, { "kind": "Content", - "text": "32768" + "text": " | undefined" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "rawCollider", + "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 2 + "endIndex": 3 }, - "releaseTag": "Public", - "name": "GROUP_11" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!CollisionGroup.GROUP_2:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Collider#rawShape:member", + "docComment": "/**\n * The raw shape object from the Rapier physics engine.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "GROUP_2 = " + "text": "get rawShape(): " + }, + { + "kind": "Reference", + "text": "RawShape", + "canonicalReference": "server!RawShape:type" }, { "kind": "Content", - "text": "64" + "text": " | undefined" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "rawShape", + "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 2 + "endIndex": 3 }, - "releaseTag": "Public", - "name": "GROUP_2" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!CollisionGroup.GROUP_3:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Collider#relativePosition:member", + "docComment": "/**\n * The relative position of the collider to its parent rigid body.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "GROUP_3 = " + "text": "get relativePosition(): " + }, + { + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", - "text": "128" + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "relativePosition", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", - "name": "GROUP_3" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!CollisionGroup.GROUP_4:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Collider#relativeRotation:member", + "docComment": "/**\n * The relative rotation of the collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "GROUP_4 = " + "text": "get relativeRotation(): " + }, + { + "kind": "Reference", + "text": "QuaternionLike", + "canonicalReference": "server!QuaternionLike:interface" }, { "kind": "Content", - "text": "256" + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "relativeRotation", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", - "name": "GROUP_4" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!CollisionGroup.GROUP_5:member", - "docComment": "", + "kind": "Method", + "canonicalReference": "server!Collider#removeFromSimulation:member(1)", + "docComment": "/**\n * Removes the collider from the simulation.\n *\n * @remarks\n *\n * **Parent unlinking:** Unlinks from parent rigid body if attached.\n *\n * **Side effects:** Removes the collider from the simulation and unlinks it from any parent rigid body.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "GROUP_5 = " + "text": "removeFromSimulation(): " }, { "kind": "Content", - "text": "512" + "text": "void" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isStatic": false, + "returnTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, "releaseTag": "Public", - "name": "GROUP_5" + "isProtected": false, + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "removeFromSimulation" }, { - "kind": "EnumMember", - "canonicalReference": "server!CollisionGroup.GROUP_6:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Collider#scale:member", + "docComment": "/**\n * The scale of the collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "GROUP_6 = " + "text": "get scale(): " + }, + { + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", - "text": "1024" + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "scale", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", - "name": "GROUP_6" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!CollisionGroup.GROUP_7:member", - "docComment": "", + "kind": "Method", + "canonicalReference": "server!Collider#setBounciness:member(1)", + "docComment": "/**\n * Sets the bounciness of the collider.\n *\n * @param bounciness - The bounciness of the collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "GROUP_7 = " + "text": "setBounciness(bounciness: " }, { "kind": "Content", - "text": "2048" - } - ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "name": "GROUP_7" - }, - { - "kind": "EnumMember", - "canonicalReference": "server!CollisionGroup.GROUP_8:member", - "docComment": "", - "excerptTokens": [ + "text": "number" + }, { "kind": "Content", - "text": "GROUP_8 = " + "text": "): " }, { "kind": "Content", - "text": "4096" + "text": "void" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 }, "releaseTag": "Public", - "name": "GROUP_8" + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "bounciness", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "setBounciness" }, { - "kind": "EnumMember", - "canonicalReference": "server!CollisionGroup.GROUP_9:member", - "docComment": "", + "kind": "Method", + "canonicalReference": "server!Collider#setBouncinessCombineRule:member(1)", + "docComment": "/**\n * Sets the bounciness combine rule of the collider.\n *\n * @param bouncinessCombineRule - The bounciness combine rule of the collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "GROUP_9 = " + "text": "setBouncinessCombineRule(bouncinessCombineRule: " + }, + { + "kind": "Reference", + "text": "CoefficientCombineRule", + "canonicalReference": "server!CoefficientCombineRule:enum" }, { "kind": "Content", - "text": "8192" - } - ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "name": "GROUP_9" - }, - { - "kind": "EnumMember", - "canonicalReference": "server!CollisionGroup.PLAYER:member", - "docComment": "", - "excerptTokens": [ + "text": "): " + }, { "kind": "Content", - "text": "PLAYER = " + "text": "void" }, { "kind": "Content", - "text": "16" + "text": ";" } ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 }, "releaseTag": "Public", - "name": "PLAYER" - } - ] - }, - { - "kind": "TypeAlias", - "canonicalReference": "server!CollisionGroups:type", - "docComment": "/**\n * A set of collision groups.\n *\n * **Category:** Physics\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export type CollisionGroups = " - }, - { - "kind": "Content", - "text": "{\n belongsTo: " - }, - { - "kind": "Reference", - "text": "CollisionGroup", - "canonicalReference": "server!CollisionGroup:enum" - }, - { - "kind": "Content", - "text": "[];\n collidesWith: " - }, - { - "kind": "Reference", - "text": "CollisionGroup", - "canonicalReference": "server!CollisionGroup:enum" - }, - { - "kind": "Content", - "text": "[];\n}" + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "bouncinessCombineRule", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "setBouncinessCombineRule" }, - { - "kind": "Content", - "text": ";" - } - ], - "fileUrlPath": "src/worlds/physics/CollisionGroupsBuilder.ts", - "releaseTag": "Public", - "name": "CollisionGroups", - "typeTokenRange": { - "startIndex": 1, - "endIndex": 6 - } - }, - { - "kind": "Class", - "canonicalReference": "server!CollisionGroupsBuilder:class", - "docComment": "/**\n * A helper class for building and decoding collision groups.\n *\n * When to use: creating custom collision filters for colliders and rigid bodies. Do NOT use for: per-frame changes; collision group changes are usually infrequent.\n *\n * @remarks\n *\n * Use the static methods directly to encode or decode collision group masks.\n *\n * **Category:** Physics\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export default class CollisionGroupsBuilder " - } - ], - "fileUrlPath": "src/worlds/physics/CollisionGroupsBuilder.ts", - "releaseTag": "Public", - "isAbstract": false, - "name": "CollisionGroupsBuilder", - "preserveMemberOrder": false, - "members": [ { "kind": "Method", - "canonicalReference": "server!CollisionGroupsBuilder.buildRawCollisionGroups:member(1)", - "docComment": "/**\n * Builds a raw collision group mask from a set of collision groups.\n *\n * @param collisionGroups - The set of collision groups to build.\n *\n * @returns A raw set of collision groups represented as a 32-bit number.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!Collider#setCollisionGroups:member(1)", + "docComment": "/**\n * Sets the collision groups of the collider.\n *\n * @param collisionGroups - The collision groups of the collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "static buildRawCollisionGroups(collisionGroups: " + "text": "setCollisionGroups(collisionGroups: " }, { "kind": "Reference", @@ -11837,16 +11655,15 @@ "text": "): " }, { - "kind": "Reference", - "text": "RawCollisionGroups", - "canonicalReference": "server!RawCollisionGroups:type" + "kind": "Content", + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isStatic": true, + "isStatic": false, "returnTypeTokenRange": { "startIndex": 3, "endIndex": 4 @@ -11866,37 +11683,35 @@ ], "isOptional": false, "isAbstract": false, - "name": "buildRawCollisionGroups" + "name": "setCollisionGroups" }, { "kind": "Method", - "canonicalReference": "server!CollisionGroupsBuilder.decodeCollisionGroups:member(1)", - "docComment": "/**\n * Decodes collision groups into their string equivalents.\n *\n * @param collisionGroups - The set of collision groups to decode.\n *\n * @returns A set of collision groups represented as their string equivalents.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!Collider#setEnabled:member(1)", + "docComment": "/**\n * Sets whether the collider is enabled.\n *\n * @param enabled - Whether the collider is enabled.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "static decodeCollisionGroups(collisionGroups: " + "text": "setEnabled(enabled: " }, { - "kind": "Reference", - "text": "CollisionGroups", - "canonicalReference": "server!CollisionGroups:type" + "kind": "Content", + "text": "boolean" }, { "kind": "Content", "text": "): " }, { - "kind": "Reference", - "text": "DecodedCollisionGroups", - "canonicalReference": "server!DecodedCollisionGroups:type" + "kind": "Content", + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isStatic": true, + "isStatic": false, "returnTypeTokenRange": { "startIndex": 3, "endIndex": 4 @@ -11906,7 +11721,7 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "collisionGroups", + "parameterName": "enabled", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -11916,37 +11731,35 @@ ], "isOptional": false, "isAbstract": false, - "name": "decodeCollisionGroups" + "name": "setEnabled" }, { "kind": "Method", - "canonicalReference": "server!CollisionGroupsBuilder.decodeRawCollisionGroups:member(1)", - "docComment": "/**\n * Decodes a raw collision group mask into a set of collision groups.\n *\n * @param groups - The raw set of collision groups to decode.\n *\n * @returns A set of collision groups.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!Collider#setFriction:member(1)", + "docComment": "/**\n * Sets the friction of the collider.\n *\n * @param friction - The friction of the collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "static decodeRawCollisionGroups(groups: " + "text": "setFriction(friction: " }, { - "kind": "Reference", - "text": "RawCollisionGroups", - "canonicalReference": "server!RawCollisionGroups:type" + "kind": "Content", + "text": "number" }, { "kind": "Content", "text": "): " }, { - "kind": "Reference", - "text": "CollisionGroups", - "canonicalReference": "server!CollisionGroups:type" + "kind": "Content", + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isStatic": true, + "isStatic": false, "returnTypeTokenRange": { "startIndex": 3, "endIndex": 4 @@ -11956,7 +11769,7 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "groups", + "parameterName": "friction", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -11966,21 +11779,21 @@ ], "isOptional": false, "isAbstract": false, - "name": "decodeRawCollisionGroups" + "name": "setFriction" }, { "kind": "Method", - "canonicalReference": "server!CollisionGroupsBuilder.isDefaultCollisionGroups:member(1)", - "docComment": "/**\n * Checks if the collision groups are the default collision groups.\n *\n * @param collisionGroups - The set of collision groups to check.\n *\n * @returns Whether the collision groups are the default collision groups.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!Collider#setFrictionCombineRule:member(1)", + "docComment": "/**\n * Sets the friction combine rule of the collider.\n *\n * @param frictionCombineRule - The friction combine rule of the collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "static isDefaultCollisionGroups(collisionGroups: " + "text": "setFrictionCombineRule(frictionCombineRule: " }, { "kind": "Reference", - "text": "CollisionGroups", - "canonicalReference": "server!CollisionGroups:type" + "text": "CoefficientCombineRule", + "canonicalReference": "server!CoefficientCombineRule:enum" }, { "kind": "Content", @@ -11988,14 +11801,14 @@ }, { "kind": "Content", - "text": "boolean" + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isStatic": true, + "isStatic": false, "returnTypeTokenRange": { "startIndex": 3, "endIndex": 4 @@ -12005,7 +11818,7 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "collisionGroups", + "parameterName": "frictionCombineRule", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -12015,631 +11828,365 @@ ], "isOptional": false, "isAbstract": false, - "name": "isDefaultCollisionGroups" - } - ], - "implementsTokenRanges": [] - }, - { - "kind": "TypeAlias", - "canonicalReference": "server!CommandCallback:type", - "docComment": "/**\n * A callback function for a chat command.\n *\n * @param player - The player that sent the command.\n *\n * @param args - An array of arguments, comprised of all space separated text after the command.\n *\n * @param message - The full message of the command. **Category:** Chat\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export type CommandCallback = " - }, - { - "kind": "Content", - "text": "(player: " - }, - { - "kind": "Reference", - "text": "Player", - "canonicalReference": "server!Player:class" - }, - { - "kind": "Content", - "text": ", args: string[], message: string) => void" - }, - { - "kind": "Content", - "text": ";" - } - ], - "fileUrlPath": "src/worlds/chat/ChatManager.ts", - "releaseTag": "Public", - "name": "CommandCallback", - "typeTokenRange": { - "startIndex": 1, - "endIndex": 4 - } - }, - { - "kind": "Interface", - "canonicalReference": "server!ConeColliderOptions:interface", - "docComment": "/**\n * The options for a cone collider.\n *\n * Use for: cone-shaped colliders. Do NOT use for: other shapes; use the matching collider option type.\n *\n * **Category:** Physics\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export interface ConeColliderOptions extends " - }, - { - "kind": "Reference", - "text": "BaseColliderOptions", - "canonicalReference": "server!BaseColliderOptions:interface" + "name": "setFrictionCombineRule" }, { - "kind": "Content", - "text": " " - } - ], - "fileUrlPath": "src/worlds/physics/Collider.ts", - "releaseTag": "Public", - "name": "ConeColliderOptions", - "preserveMemberOrder": false, - "members": [ - { - "kind": "PropertySignature", - "canonicalReference": "server!ConeColliderOptions#halfHeight:member", - "docComment": "/**\n * The half height of the cone collider.\n *\n * **Category:** Physics\n */\n", + "kind": "Method", + "canonicalReference": "server!Collider#setHalfExtents:member(1)", + "docComment": "/**\n * Sets the half extents of a simulated block collider.\n *\n * @param halfExtents - The half extents of the block collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "halfHeight?: " + "text": "setHalfExtents(halfExtents: " + }, + { + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", - "text": "number" + "text": "): " + }, + { + "kind": "Content", + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": true, + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, "releaseTag": "Public", - "name": "halfHeight", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "halfExtents", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "setHalfExtents" }, { - "kind": "PropertySignature", - "canonicalReference": "server!ConeColliderOptions#radius:member", - "docComment": "/**\n * The radius of the cone collider.\n *\n * **Category:** Physics\n */\n", + "kind": "Method", + "canonicalReference": "server!Collider#setHalfHeight:member(1)", + "docComment": "/**\n * Sets the half height of a simulated capsule, cone, cylinder, or round cylinder collider.\n *\n * @param halfHeight - The half height of the capsule, cone, cylinder, or round cylinder collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "radius?: " + "text": "setHalfHeight(halfHeight: " }, { "kind": "Content", "text": "number" }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "void" + }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": true, + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, "releaseTag": "Public", - "name": "radius", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "halfHeight", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "setHalfHeight" }, { - "kind": "PropertySignature", - "canonicalReference": "server!ConeColliderOptions#shape:member", - "docComment": "", + "kind": "Method", + "canonicalReference": "server!Collider#setMass:member(1)", + "docComment": "/**\n * Sets the mass of the collider.\n *\n * @param mass - The mass of the collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "shape: " + "text": "setMass(mass: " }, { - "kind": "Reference", - "text": "ColliderShape.CONE", - "canonicalReference": "server!ColliderShape.CONE:member" + "kind": "Content", + "text": "number" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": false, + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, "releaseTag": "Public", - "name": "shape", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } - } - ], - "extendsTokenRanges": [ - { - "startIndex": 1, - "endIndex": 2 - } - ] - }, - { - "kind": "TypeAlias", - "canonicalReference": "server!ContactForceData:type", - "docComment": "/**\n * Data for contact forces.\n *\n * **Category:** Physics\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export type ContactForceData = " - }, - { - "kind": "Content", - "text": "{\n totalForce: " - }, - { - "kind": "Reference", - "text": "RAPIER.Vector", - "canonicalReference": "@dimforge/rapier3d-simd-compat!Vector:interface" - }, - { - "kind": "Content", - "text": ";\n totalForceMagnitude: number;\n maxForceDirection: " - }, - { - "kind": "Reference", - "text": "RAPIER.Vector", - "canonicalReference": "@dimforge/rapier3d-simd-compat!Vector:interface" - }, - { - "kind": "Content", - "text": ";\n maxForceMagnitude: number;\n}" - }, - { - "kind": "Content", - "text": ";" - } - ], - "fileUrlPath": "src/worlds/physics/Simulation.ts", - "releaseTag": "Public", - "name": "ContactForceData", - "typeTokenRange": { - "startIndex": 1, - "endIndex": 6 - } - }, - { - "kind": "TypeAlias", - "canonicalReference": "server!ContactManifold:type", - "docComment": "/**\n * A contact manifold.\n *\n * **Category:** Physics\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export type ContactManifold = " - }, - { - "kind": "Content", - "text": "{\n contactPoints: " - }, - { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" - }, - { - "kind": "Content", - "text": "[];\n localNormalA: " - }, - { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" - }, - { - "kind": "Content", - "text": ";\n localNormalB: " - }, - { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" - }, - { - "kind": "Content", - "text": ";\n normal: " - }, - { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" - }, - { - "kind": "Content", - "text": ";\n}" - }, - { - "kind": "Content", - "text": ";" - } - ], - "fileUrlPath": "src/worlds/physics/Simulation.ts", - "releaseTag": "Public", - "name": "ContactManifold", - "typeTokenRange": { - "startIndex": 1, - "endIndex": 10 - } - }, - { - "kind": "Interface", - "canonicalReference": "server!CylinderColliderOptions:interface", - "docComment": "/**\n * The options for a cylinder collider.\n *\n * Use for: cylinder-shaped colliders. Do NOT use for: other shapes; use the matching collider option type.\n *\n * **Category:** Physics\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export interface CylinderColliderOptions extends " - }, - { - "kind": "Reference", - "text": "BaseColliderOptions", - "canonicalReference": "server!BaseColliderOptions:interface" + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "mass", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "setMass" }, { - "kind": "Content", - "text": " " - } - ], - "fileUrlPath": "src/worlds/physics/Collider.ts", - "releaseTag": "Public", - "name": "CylinderColliderOptions", - "preserveMemberOrder": false, - "members": [ - { - "kind": "PropertySignature", - "canonicalReference": "server!CylinderColliderOptions#halfHeight:member", - "docComment": "/**\n * The half height of the cylinder collider.\n *\n * **Category:** Physics\n */\n", + "kind": "Method", + "canonicalReference": "server!Collider#setOnCollision:member(1)", + "docComment": "/**\n * Sets the on collision callback for the collider.\n *\n * @remarks\n *\n * **Auto-enables events:** Automatically enables/disables collision events based on whether callback is set.\n *\n * @param callback - The on collision callback for the collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "halfHeight?: " + "text": "setOnCollision(callback: " }, { - "kind": "Content", - "text": "number" + "kind": "Reference", + "text": "CollisionCallback", + "canonicalReference": "server!CollisionCallback:type" }, { "kind": "Content", - "text": ";" - } - ], - "isReadonly": false, - "isOptional": true, - "releaseTag": "Public", - "name": "halfHeight", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } - }, - { - "kind": "PropertySignature", - "canonicalReference": "server!CylinderColliderOptions#radius:member", - "docComment": "/**\n * The radius of the cylinder collider.\n *\n * **Category:** Physics\n */\n", - "excerptTokens": [ + "text": " | undefined" + }, { "kind": "Content", - "text": "radius?: " + "text": "): " }, { "kind": "Content", - "text": "number" + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": true, + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 4, + "endIndex": 5 + }, "releaseTag": "Public", - "name": "radius", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "callback", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "setOnCollision" }, { - "kind": "PropertySignature", - "canonicalReference": "server!CylinderColliderOptions#shape:member", - "docComment": "", + "kind": "Method", + "canonicalReference": "server!Collider#setRadius:member(1)", + "docComment": "/**\n * Sets the radius of a simulated ball, capsule, cylinder, or round cylinder collider.\n *\n * @param radius - The radius of the collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "shape: " - }, - { - "kind": "Reference", - "text": "ColliderShape.CYLINDER", - "canonicalReference": "server!ColliderShape.CYLINDER:member" + "text": "setRadius(radius: " }, { "kind": "Content", - "text": ";" - } - ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "shape", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } - } - ], - "extendsTokenRanges": [ - { - "startIndex": 1, - "endIndex": 2 - } - ] - }, - { - "kind": "TypeAlias", - "canonicalReference": "server!DecodedCollisionGroups:type", - "docComment": "/**\n * A decoded set of collision groups represented as their string equivalents.\n *\n * **Category:** Physics\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export type DecodedCollisionGroups = " - }, - { - "kind": "Content", - "text": "{\n belongsTo: string[];\n collidesWith: string[];\n}" - }, - { - "kind": "Content", - "text": ";" - } - ], - "fileUrlPath": "src/worlds/physics/CollisionGroupsBuilder.ts", - "releaseTag": "Public", - "name": "DecodedCollisionGroups", - "typeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } - }, - { - "kind": "Variable", - "canonicalReference": "server!DEFAULT_ENTITY_RIGID_BODY_OPTIONS:var", - "docComment": "/**\n * The default rigid body options for a model entity when `EntityOptions.rigidBodyOptions` is not provided.\n *\n * **Category:** Entities\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "DEFAULT_ENTITY_RIGID_BODY_OPTIONS: " - }, - { - "kind": "Reference", - "text": "RigidBodyOptions", - "canonicalReference": "server!RigidBodyOptions:type" - } - ], - "fileUrlPath": "src/worlds/entities/Entity.ts", - "isReadonly": true, - "releaseTag": "Public", - "name": "DEFAULT_ENTITY_RIGID_BODY_OPTIONS", - "variableTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } - }, - { - "kind": "Class", - "canonicalReference": "server!DefaultPlayerEntity:class", - "docComment": "/**\n * Represents the default player model entity.\n *\n * When to use: standard player avatars with built-in cosmetics and default controls. Do NOT use for: fully custom player rigs that don't match the default model's anchors/animations.\n *\n * @remarks\n *\n * Extends `PlayerEntity`, uses the default player model, and assigns `DefaultPlayerEntityController`. You can override defaults, but if you change `modelUri`, ensure the model has the same animation names and anchor points.\n *\n * @example\n * ```typescript\n * const playerEntity = new DefaultPlayerEntity({ player });\n *\n * playerEntity.spawn(world, { x: 0, y: 10, z: 0 });\n * ```\n *\n * **Category:** Entities\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export default class DefaultPlayerEntity extends " - }, - { - "kind": "Reference", - "text": "PlayerEntity", - "canonicalReference": "server!PlayerEntity:class" - }, - { - "kind": "Content", - "text": " " - } - ], - "fileUrlPath": "src/worlds/entities/DefaultPlayerEntity.ts", - "releaseTag": "Public", - "isAbstract": false, - "name": "DefaultPlayerEntity", - "preserveMemberOrder": false, - "members": [ - { - "kind": "Constructor", - "canonicalReference": "server!DefaultPlayerEntity:constructor(1)", - "docComment": "/**\n * Creates a new DefaultPlayerEntity instance.\n *\n * @remarks\n *\n * **Auto-assigned defaults:** A `DefaultPlayerEntityController` is automatically created and assigned. Default idle animations are initialized as looped and playing.\n *\n * **Cosmetics on spawn:** When spawned, player cosmetics (hair, skin, equipped items) are fetched asynchronously and applied. Child entities are created for hair and equipped cosmetic items.\n *\n * @param options - The options for the default player entity.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ + "text": "number" + }, { "kind": "Content", - "text": "constructor(options: " + "text": "): " }, { - "kind": "Reference", - "text": "DefaultPlayerEntityOptions", - "canonicalReference": "server!DefaultPlayerEntityOptions:type" + "kind": "Content", + "text": "void" }, { "kind": "Content", - "text": ");" + "text": ";" } ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "options", + "parameterName": "radius", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, "isOptional": false } - ] + ], + "isOptional": false, + "isAbstract": false, + "name": "setRadius" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntity#cosmeticHiddenSlots:member", - "docComment": "/**\n * The cosmetic slots that are hidden.\n *\n * **Category:** Entities\n *\n * @public\n */\n", + "kind": "Method", + "canonicalReference": "server!Collider#setRelativePosition:member(1)", + "docComment": "/**\n * Sets the position of the collider relative to its parent rigid body or the world origin.\n *\n * @remarks\n *\n * Colliders can be added as a child of a rigid body, or to the world directly. This position is relative to the parent rigid body or the world origin.\n *\n * @param position - The relative position of the collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get cosmeticHiddenSlots(): " + "text": "setRelativePosition(position: " }, { "kind": "Reference", - "text": "PlayerCosmeticSlot", - "canonicalReference": "server!PlayerCosmeticSlot:type" + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", - "text": "[]" + "text": "): " }, { "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "cosmeticHiddenSlots", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - } - ], - "extendsTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "implementsTokenRanges": [] - }, - { - "kind": "Class", - "canonicalReference": "server!DefaultPlayerEntityController:class", - "docComment": "/**\n * The default player entity controller implementation.\n *\n * When to use: player-controlled avatars using `DefaultPlayerEntity`. Do NOT use for: NPCs or non-player entities; use `SimpleEntityController` or `PathfindingEntityController` instead.\n *\n * @remarks\n *\n * Extends `BaseEntityController` and implements default movement, platforming, jumping, and swimming. You can extend this class to add custom logic.\n *\n *

Coordinate System & Model Orientation

\n *\n * HYTOPIA uses **-Z as forward**. Models must be authored with their front facing -Z. A yaw of 0 means facing -Z. The controller rotates the entity based on camera yaw and movement direction, always orienting the entity's -Z axis in the intended facing direction.\n *\n * @example\n * ```typescript\n * // Create a custom entity controller for myEntity, prior to spawning it.\n * myEntity.setController(new DefaultPlayerEntityController({\n * jumpVelocity: 10,\n * runVelocity: 8,\n * walkVelocity: 4,\n * }));\n *\n * // Spawn the entity in the world.\n * myEntity.spawn(world, { x: 53, y: 10, z: 23 });\n * ```\n *\n * **Category:** Controllers\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export default class DefaultPlayerEntityController extends " - }, - { - "kind": "Reference", - "text": "BaseEntityController", - "canonicalReference": "server!BaseEntityController:class" - }, - { - "kind": "Content", - "text": " " - } - ], - "fileUrlPath": "src/worlds/entities/controllers/DefaultPlayerEntityController.ts", - "releaseTag": "Public", - "isAbstract": false, - "name": "DefaultPlayerEntityController", - "preserveMemberOrder": false, - "members": [ - { - "kind": "Constructor", - "canonicalReference": "server!DefaultPlayerEntityController:constructor(1)", - "docComment": "/**\n * Constructs a new instance of the `DefaultPlayerEntityController` class\n *\n * @param options - Options for the controller.\n *\n * **Category:** Controllers\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "constructor(options?: " - }, - { - "kind": "Reference", - "text": "DefaultPlayerEntityControllerOptions", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions:interface" + "text": "void" }, { "kind": "Content", - "text": ");" + "text": ";" } ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "options", + "parameterName": "position", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isOptional": true + "isOptional": false } - ] + ], + "isOptional": false, + "isAbstract": false, + "name": "setRelativePosition" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#applyDirectionalMovementRotations:member", - "docComment": "/**\n * Whether to apply directional rotations to the entity while moving, defaults to true.\n */\n", + "kind": "Method", + "canonicalReference": "server!Collider#setRelativeRotation:member(1)", + "docComment": "/**\n * Sets the relative rotation of the collider to its parent rigid body or the world origin.\n *\n * @remarks\n *\n * Colliders can be added as a child of a rigid body, or to the world directly. This rotation is relative to the parent rigid body or the world origin.\n *\n * @param rotation - The relative rotation of the collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "applyDirectionalMovementRotations: " + "text": "setRelativeRotation(rotation: " + }, + { + "kind": "Reference", + "text": "QuaternionLike", + "canonicalReference": "server!QuaternionLike:interface" }, { "kind": "Content", - "text": "boolean" + "text": "): " + }, + { + "kind": "Content", + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "applyDirectionalMovementRotations", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "releaseTag": "Public", "isProtected": false, - "isAbstract": false + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "rotation", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "setRelativeRotation" }, { "kind": "Method", - "canonicalReference": "server!DefaultPlayerEntityController#attach:member(1)", - "docComment": "/**\n * Called when the controller is attached to an entity.\n *\n * @remarks\n *\n * **Wraps `applyImpulse`:** The entity's `applyImpulse` method is wrapped to track external velocities separately from internal movement. External impulses decay over time when grounded.\n *\n * **Locks rotations:** Calls `entity.lockAllRotations()` to prevent physics from rotating the entity. Rotation is set explicitly by the controller based on camera orientation.\n *\n * **Enables CCD:** Enables continuous collision detection on the entity.\n *\n * **Swimming detection:** Registers a `BLOCK_COLLISION` listener to detect liquid blocks and manage swimming state, gravity scale, and animations.\n *\n * @param entity - The entity to attach the controller to.\n *\n * **Category:** Controllers\n */\n", + "canonicalReference": "server!Collider#setScale:member(1)", + "docComment": "/**\n * Scales the collider by the given scalar. Only ball, block, capsule, cone, cylinder, round cylinder are supported.\n *\n * @remarks\n *\n * **Ratio-based:** Uses ratio-based scaling relative to current scale, not absolute dimensions. Also scales `relativePosition` proportionally.\n *\n * @param scalar - The scalar to scale the collider by.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "attach(entity: " + "text": "setScale(scale: " }, { "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", @@ -12664,7 +12211,7 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "entity", + "parameterName": "scale", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -12674,189 +12221,195 @@ ], "isOptional": false, "isAbstract": false, - "name": "attach" + "name": "setScale" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#autoCancelMouseLeftClick:member", - "docComment": "/**\n * Whether to automatically cancel left click input after first processed tick, defaults to true.\n */\n", + "kind": "Method", + "canonicalReference": "server!Collider#setSensor:member(1)", + "docComment": "/**\n * Sets whether the collider is a sensor.\n *\n * @param sensor - Whether the collider is a sensor.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "autoCancelMouseLeftClick: " + "text": "setSensor(sensor: " }, { "kind": "Content", "text": "boolean" }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "void" + }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "autoCancelMouseLeftClick", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "releaseTag": "Public", "isProtected": false, - "isAbstract": false + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "sensor", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "setSensor" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#canJump:member", - "docComment": "/**\n * A function allowing custom logic to determine if the entity can jump.\n *\n * @param controller - The default player entity controller instance.\n *\n * @returns Whether the entity of the entity controller can jump.\n */\n", + "kind": "Method", + "canonicalReference": "server!Collider#setTag:member(1)", + "docComment": "/**\n * Sets the tag of the collider.\n *\n * @param tag - The tag of the collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "canJump: " + "text": "setTag(tag: " }, { "kind": "Content", - "text": "(controller: " + "text": "string" }, { - "kind": "Reference", - "text": "DefaultPlayerEntityController", - "canonicalReference": "server!DefaultPlayerEntityController:class" + "kind": "Content", + "text": "): " }, { "kind": "Content", - "text": ") => boolean" + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "canJump", - "propertyTypeTokenRange": { - "startIndex": 1, + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 3, "endIndex": 4 }, - "isStatic": false, + "releaseTag": "Public", "isProtected": false, - "isAbstract": false + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "tag", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "setTag" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#canRun:member", - "docComment": "/**\n * A function allowing custom logic to determine if the entity can run.\n *\n * @param controller - The default player entity controller instance.\n *\n * @returns Whether the entity of the entity controller can run.\n */\n", + "kind": "Method", + "canonicalReference": "server!Collider#setVoxel:member(1)", + "docComment": "/**\n * Sets the voxel at the given coordinate as filled or not filled.\n *\n * @param coordinate - The coordinate of the voxel to set.\n *\n * @param filled - True if the voxel at the coordinate should be filled, false if it should be removed.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "canRun: " - }, - { - "kind": "Content", - "text": "(controller: " + "text": "setVoxel(coordinate: " }, { "kind": "Reference", - "text": "DefaultPlayerEntityController", - "canonicalReference": "server!DefaultPlayerEntityController:class" + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", - "text": ") => boolean" + "text": ", filled: " }, { "kind": "Content", - "text": ";" - } - ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "canRun", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 4 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#canSwim:member", - "docComment": "/**\n * A function allowing custom logic to determine if the entity can swim.\n *\n * @param controller - The default player entity controller instance.\n *\n * @returns Whether the entity of the entity controller can swim.\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "canSwim: " + "text": "boolean" }, { "kind": "Content", - "text": "(controller: " - }, - { - "kind": "Reference", - "text": "DefaultPlayerEntityController", - "canonicalReference": "server!DefaultPlayerEntityController:class" + "text": "): " }, { "kind": "Content", - "text": ") => boolean" + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "canSwim", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 4 - }, "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "releaseTag": "Public", "isProtected": false, - "isAbstract": false + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "coordinate", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "filled", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "setVoxel" }, { "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#canWalk:member", - "docComment": "/**\n * A function allowing custom logic to determine if the entity can walk.\n *\n * @param controller - The default player entity controller instance.\n *\n * @returns Whether the entity of the entity controller can walk.\n */\n", + "canonicalReference": "server!Collider#shape:member", + "docComment": "/**\n * The shape of the collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "canWalk: " - }, - { - "kind": "Content", - "text": "(controller: " + "text": "get shape(): " }, { "kind": "Reference", - "text": "DefaultPlayerEntityController", - "canonicalReference": "server!DefaultPlayerEntityController:class" - }, - { - "kind": "Content", - "text": ") => boolean" + "text": "ColliderShape", + "canonicalReference": "server!ColliderShape:enum" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, + "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "canWalk", + "name": "shape", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 4 + "endIndex": 2 }, "isStatic": false, "isProtected": false, @@ -12864,26 +12417,26 @@ }, { "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#facesCameraWhenIdle:member", - "docComment": "/**\n * Whether the entity rotates to face the camera direction when idle. When `true`, the entity always faces the camera direction. When `false`, the entity only rotates while actively moving.\n */\n", + "canonicalReference": "server!Collider#tag:member", + "docComment": "/**\n * An arbitrary identifier tag of the collider. Useful for your own logic.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "facesCameraWhenIdle: " + "text": "get tag(): " }, { "kind": "Content", - "text": "boolean" + "text": "string | undefined" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, + "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "facesCameraWhenIdle", + "name": "tag", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -12891,884 +12444,1110 @@ "isStatic": false, "isProtected": false, "isAbstract": false + } + ], + "extendsTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "implementsTokenRanges": [] + }, + { + "kind": "TypeAlias", + "canonicalReference": "server!ColliderOptions:type", + "docComment": "/**\n * The options for a collider.\n *\n * Use for: providing collider definitions when creating rigid bodies or entities. Do NOT use for: runtime changes; use `Collider` APIs instead.\n *\n * **Category:** Physics\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export type ColliderOptions = " }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#idleLoopedAnimations:member", - "docComment": "/**\n * The looped animation(s) that will play when the entity is idle.\n */\n", + "kind": "Reference", + "text": "BallColliderOptions", + "canonicalReference": "server!BallColliderOptions:interface" + }, + { + "kind": "Content", + "text": " | " + }, + { + "kind": "Reference", + "text": "BlockColliderOptions", + "canonicalReference": "server!BlockColliderOptions:interface" + }, + { + "kind": "Content", + "text": " | " + }, + { + "kind": "Reference", + "text": "CapsuleColliderOptions", + "canonicalReference": "server!CapsuleColliderOptions:interface" + }, + { + "kind": "Content", + "text": " | " + }, + { + "kind": "Reference", + "text": "ConeColliderOptions", + "canonicalReference": "server!ConeColliderOptions:interface" + }, + { + "kind": "Content", + "text": " | " + }, + { + "kind": "Reference", + "text": "CylinderColliderOptions", + "canonicalReference": "server!CylinderColliderOptions:interface" + }, + { + "kind": "Content", + "text": " | " + }, + { + "kind": "Reference", + "text": "RoundCylinderColliderOptions", + "canonicalReference": "server!RoundCylinderColliderOptions:interface" + }, + { + "kind": "Content", + "text": " | " + }, + { + "kind": "Reference", + "text": "TrimeshColliderOptions", + "canonicalReference": "server!TrimeshColliderOptions:interface" + }, + { + "kind": "Content", + "text": " | " + }, + { + "kind": "Reference", + "text": "VoxelsColliderOptions", + "canonicalReference": "server!VoxelsColliderOptions:interface" + }, + { + "kind": "Content", + "text": " | " + }, + { + "kind": "Reference", + "text": "WedgeColliderOptions", + "canonicalReference": "server!WedgeColliderOptions:interface" + }, + { + "kind": "Content", + "text": " | " + }, + { + "kind": "Reference", + "text": "NoneColliderOptions", + "canonicalReference": "server!NoneColliderOptions:interface" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "src/worlds/physics/Collider.ts", + "releaseTag": "Public", + "name": "ColliderOptions", + "typeTokenRange": { + "startIndex": 1, + "endIndex": 20 + } + }, + { + "kind": "Enum", + "canonicalReference": "server!ColliderShape:enum", + "docComment": "/**\n * The shapes a collider can be.\n *\n * **Category:** Physics\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare enum ColliderShape " + } + ], + "fileUrlPath": "src/worlds/physics/Collider.ts", + "releaseTag": "Public", + "name": "ColliderShape", + "preserveMemberOrder": false, + "members": [ + { + "kind": "EnumMember", + "canonicalReference": "server!ColliderShape.BALL:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "idleLoopedAnimations: " - }, - { - "kind": "Content", - "text": "string[]" + "text": "BALL = " }, { "kind": "Content", - "text": ";" + "text": "\"ball\"" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "idleLoopedAnimations", - "propertyTypeTokenRange": { + "initializerTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "releaseTag": "Public", + "name": "BALL" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#interactOneshotAnimations:member", - "docComment": "/**\n * The oneshot animation(s) that will play when the entity interacts (left click)\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!ColliderShape.BLOCK:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "interactOneshotAnimations: " - }, - { - "kind": "Content", - "text": "string[]" + "text": "BLOCK = " }, { "kind": "Content", - "text": ";" + "text": "\"block\"" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "interactOneshotAnimations", - "propertyTypeTokenRange": { + "initializerTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "releaseTag": "Public", + "name": "BLOCK" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#isActivelyMoving:member", - "docComment": "/**\n * Whether the entity is moving from player inputs.\n *\n * **Category:** Controllers\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!ColliderShape.CAPSULE:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "get isActivelyMoving(): " - }, - { - "kind": "Content", - "text": "boolean" + "text": "CAPSULE = " }, { "kind": "Content", - "text": ";" + "text": "\"capsule\"" } ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "isActivelyMoving", - "propertyTypeTokenRange": { + "initializerTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "releaseTag": "Public", + "name": "CAPSULE" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#isGrounded:member", - "docComment": "/**\n * Whether the entity is grounded.\n *\n * **Category:** Controllers\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!ColliderShape.CONE:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "get isGrounded(): " - }, - { - "kind": "Content", - "text": "boolean" + "text": "CONE = " }, { "kind": "Content", - "text": ";" + "text": "\"cone\"" } ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "isGrounded", - "propertyTypeTokenRange": { + "initializerTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "releaseTag": "Public", + "name": "CONE" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#isOnPlatform:member", - "docComment": "/**\n * Whether the entity is on a platform.\n *\n * @remarks\n *\n * A platform is any entity with a kinematic rigid body.\n *\n * **Category:** Controllers\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!ColliderShape.CYLINDER:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "get isOnPlatform(): " - }, - { - "kind": "Content", - "text": "boolean" + "text": "CYLINDER = " }, { "kind": "Content", - "text": ";" + "text": "\"cylinder\"" } ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "isOnPlatform", - "propertyTypeTokenRange": { + "initializerTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "releaseTag": "Public", + "name": "CYLINDER" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#isSwimming:member", - "docComment": "/**\n * Whether the entity is swimming.\n *\n * @remarks\n *\n * Determined by whether the entity is in contact with a liquid block.\n *\n * **Category:** Controllers\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!ColliderShape.NONE:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "get isSwimming(): " - }, - { - "kind": "Content", - "text": "boolean" + "text": "NONE = " }, { "kind": "Content", - "text": ";" + "text": "\"none\"" } ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "isSwimming", - "propertyTypeTokenRange": { + "initializerTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "releaseTag": "Public", + "name": "NONE" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#jumpLandHeavyOneshotAnimations:member", - "docComment": "/**\n * The oneshot animation(s) that will play when the entity lands with a high velocity.\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!ColliderShape.ROUND_CYLINDER:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "jumpLandHeavyOneshotAnimations: " - }, - { - "kind": "Content", - "text": "string[]" + "text": "ROUND_CYLINDER = " }, { "kind": "Content", - "text": ";" + "text": "\"round-cylinder\"" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "jumpLandHeavyOneshotAnimations", - "propertyTypeTokenRange": { + "initializerTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "releaseTag": "Public", + "name": "ROUND_CYLINDER" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#jumpLandLightOneshotAnimations:member", - "docComment": "/**\n * The oneshot animation(s) that will play when the entity lands after jumping or being airborne.\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!ColliderShape.TRIMESH:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "jumpLandLightOneshotAnimations: " - }, - { - "kind": "Content", - "text": "string[]" + "text": "TRIMESH = " }, { "kind": "Content", - "text": ";" + "text": "\"trimesh\"" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "jumpLandLightOneshotAnimations", - "propertyTypeTokenRange": { + "initializerTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "releaseTag": "Public", + "name": "TRIMESH" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#jumpOneshotAnimations:member", - "docComment": "/**\n * The oneshot animation(s) that will play when the entity is jumping.\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!ColliderShape.VOXELS:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "jumpOneshotAnimations: " - }, - { - "kind": "Content", - "text": "string[]" + "text": "VOXELS = " }, { "kind": "Content", - "text": ";" + "text": "\"voxels\"" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "jumpOneshotAnimations", - "propertyTypeTokenRange": { + "initializerTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "releaseTag": "Public", + "name": "VOXELS" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#jumpVelocity:member", - "docComment": "/**\n * The upward velocity applied to the entity when it jumps.\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!ColliderShape.WEDGE:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "jumpVelocity: " - }, - { - "kind": "Content", - "text": "number" + "text": "WEDGE = " }, { "kind": "Content", - "text": ";" + "text": "\"wedge\"" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "jumpVelocity", - "propertyTypeTokenRange": { + "initializerTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "releaseTag": "Public", + "name": "WEDGE" + } + ] + }, + { + "kind": "TypeAlias", + "canonicalReference": "server!CollisionCallback:type", + "docComment": "/**\n * A callback function that is called when a collision occurs.\n *\n * @param other - The other object involved in the collision, a block or entity.\n *\n * @param started - Whether the collision has started or ended.\n *\n * **Category:** Physics\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export type CollisionCallback = " }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#platform:member", - "docComment": "/**\n * The platform the entity is on, if any.\n *\n * **Category:** Controllers\n */\n", + "kind": "Content", + "text": "((other: " + }, + { + "kind": "Reference", + "text": "BlockType", + "canonicalReference": "server!BlockType:class" + }, + { + "kind": "Content", + "text": " | " + }, + { + "kind": "Reference", + "text": "Entity", + "canonicalReference": "server!Entity:class" + }, + { + "kind": "Content", + "text": ", started: boolean) => void) | ((other: " + }, + { + "kind": "Reference", + "text": "BlockType", + "canonicalReference": "server!BlockType:class" + }, + { + "kind": "Content", + "text": " | " + }, + { + "kind": "Reference", + "text": "Entity", + "canonicalReference": "server!Entity:class" + }, + { + "kind": "Content", + "text": ", started: boolean, colliderHandleA: number, colliderHandleB: number) => void)" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "src/worlds/physics/ColliderMap.ts", + "releaseTag": "Public", + "name": "CollisionCallback", + "typeTokenRange": { + "startIndex": 1, + "endIndex": 10 + } + }, + { + "kind": "Enum", + "canonicalReference": "server!CollisionGroup:enum", + "docComment": "/**\n * The default collision groups.\n *\n * @remarks\n *\n * Collision groups determine which objects collide and generate events. Up to 15 groups can be registered. Filtering uses pairwise bit masks:\n *\n * - The belongsTo groups (the 16 left-most bits of `self.0`) - The collidesWith mask (the 16 right-most bits of `self.0`)\n *\n * An interaction is allowed between two filters `a` and `b` if:\n * ```\n * ((a >> 16) & b) != 0 && ((b >> 16) & a) != 0\n * ```\n *\n * **Category:** Physics\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare enum CollisionGroup " + } + ], + "fileUrlPath": "src/worlds/physics/CollisionGroupsBuilder.ts", + "releaseTag": "Public", + "name": "CollisionGroup", + "preserveMemberOrder": false, + "members": [ + { + "kind": "EnumMember", + "canonicalReference": "server!CollisionGroup.ALL:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "get platform(): " + "text": "ALL = " }, { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" - }, + "kind": "Content", + "text": "65535" + } + ], + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "ALL" + }, + { + "kind": "EnumMember", + "canonicalReference": "server!CollisionGroup.BLOCK:member", + "docComment": "", + "excerptTokens": [ { "kind": "Content", - "text": " | undefined" + "text": "BLOCK = " }, { "kind": "Content", - "text": ";" + "text": "1" } ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "platform", - "propertyTypeTokenRange": { + "initializerTokenRange": { "startIndex": 1, - "endIndex": 3 + "endIndex": 2 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "releaseTag": "Public", + "name": "BLOCK" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#runLoopedAnimations:member", - "docComment": "/**\n * The looped animation(s) that will play when the entity is running.\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!CollisionGroup.ENTITY:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "runLoopedAnimations: " - }, - { - "kind": "Content", - "text": "string[]" + "text": "ENTITY = " }, { "kind": "Content", - "text": ";" + "text": "2" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "runLoopedAnimations", - "propertyTypeTokenRange": { + "initializerTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "releaseTag": "Public", + "name": "ENTITY" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#runVelocity:member", - "docComment": "/**\n * The normalized horizontal velocity applied to the entity when it runs.\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!CollisionGroup.ENTITY_SENSOR:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "runVelocity: " - }, - { - "kind": "Content", - "text": "number" + "text": "ENTITY_SENSOR = " }, { "kind": "Content", - "text": ";" + "text": "4" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "runVelocity", - "propertyTypeTokenRange": { + "initializerTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "releaseTag": "Public", + "name": "ENTITY_SENSOR" }, { - "kind": "Method", - "canonicalReference": "server!DefaultPlayerEntityController#spawn:member(1)", - "docComment": "/**\n * Called when the controlled entity is spawned. In DefaultPlayerEntityController, this function is used to create the colliders for the entity for wall and ground detection.\n *\n * @remarks\n *\n * **Creates colliders:** Adds two child colliders to the entity: - `groundSensor`: Cylinder sensor below entity for ground/platform detection and landing animations - `wallCollider`: Capsule collider for wall collision with zero friction\n *\n * **Collider sizes scale:** Collider dimensions scale proportionally with `entity.height`.\n *\n * @param entity - The entity that is spawned.\n *\n * **Category:** Controllers\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!CollisionGroup.ENVIRONMENT_ENTITY:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "spawn(entity: " - }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": "ENVIRONMENT_ENTITY = " }, { "kind": "Content", - "text": "): " - }, + "text": "8" + } + ], + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "ENVIRONMENT_ENTITY" + }, + { + "kind": "EnumMember", + "canonicalReference": "server!CollisionGroup.GROUP_1:member", + "docComment": "", + "excerptTokens": [ { "kind": "Content", - "text": "void" + "text": "GROUP_1 = " }, { "kind": "Content", - "text": ";" + "text": "32" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 }, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "entity", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "spawn" + "name": "GROUP_1" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#sticksToPlatforms:member", - "docComment": "/**\n * Whether the entity sticks to platforms.\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!CollisionGroup.GROUP_10:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "sticksToPlatforms: " - }, - { - "kind": "Content", - "text": "boolean" + "text": "GROUP_10 = " }, { "kind": "Content", - "text": ";" + "text": "16384" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "sticksToPlatforms", - "propertyTypeTokenRange": { + "initializerTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "releaseTag": "Public", + "name": "GROUP_10" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#swimFastVelocity:member", - "docComment": "/**\n * The normalized horizontal velocity applied to the entity when it swims fast (equivalent to running).\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!CollisionGroup.GROUP_11:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "swimFastVelocity: " - }, - { - "kind": "Content", - "text": "number" + "text": "GROUP_11 = " }, { "kind": "Content", - "text": ";" + "text": "32768" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "swimFastVelocity", - "propertyTypeTokenRange": { + "initializerTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "releaseTag": "Public", + "name": "GROUP_11" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#swimGravity:member", - "docComment": "/**\n * The gravity modifier applied to the entity when swimming.\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!CollisionGroup.GROUP_2:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "swimGravity: " - }, - { - "kind": "Content", - "text": "number" + "text": "GROUP_2 = " }, { "kind": "Content", - "text": ";" + "text": "64" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "swimGravity", - "propertyTypeTokenRange": { + "initializerTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "releaseTag": "Public", + "name": "GROUP_2" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#swimIdleLoopedAnimations:member", - "docComment": "/**\n * The looped animation(s) that will play when the entity is not moving while swimming.\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!CollisionGroup.GROUP_3:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "swimIdleLoopedAnimations: " - }, - { - "kind": "Content", - "text": "string[]" + "text": "GROUP_3 = " }, { "kind": "Content", - "text": ";" + "text": "128" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "swimIdleLoopedAnimations", - "propertyTypeTokenRange": { + "initializerTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "releaseTag": "Public", + "name": "GROUP_3" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#swimLoopedAnimations:member", - "docComment": "/**\n * The looped animation(s) that will play when the entity is swimming in any direction.\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!CollisionGroup.GROUP_4:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "swimLoopedAnimations: " - }, - { - "kind": "Content", - "text": "string[]" + "text": "GROUP_4 = " }, { "kind": "Content", - "text": ";" + "text": "256" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "swimLoopedAnimations", - "propertyTypeTokenRange": { + "initializerTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "releaseTag": "Public", + "name": "GROUP_4" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#swimMaxGravityVelocity:member", - "docComment": "/**\n * The maximum downward velocity that the entity can reach when affected by gravity while swimming.\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!CollisionGroup.GROUP_5:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "swimMaxGravityVelocity: " - }, - { - "kind": "Content", - "text": "number" + "text": "GROUP_5 = " }, { "kind": "Content", - "text": ";" + "text": "512" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "swimMaxGravityVelocity", - "propertyTypeTokenRange": { + "initializerTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "releaseTag": "Public", + "name": "GROUP_5" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#swimSlowVelocity:member", - "docComment": "/**\n * The normalized horizontal velocity applied to the entity when it swims slowly (equivalent to walking).\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!CollisionGroup.GROUP_6:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "swimSlowVelocity: " - }, - { - "kind": "Content", - "text": "number" + "text": "GROUP_6 = " }, { "kind": "Content", - "text": ";" + "text": "1024" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "swimSlowVelocity", - "propertyTypeTokenRange": { + "initializerTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "releaseTag": "Public", + "name": "GROUP_6" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#swimUpwardVelocity:member", - "docComment": "/**\n * The upward velocity applied to the entity when swimming.\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!CollisionGroup.GROUP_7:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "swimUpwardVelocity: " + "text": "GROUP_7 = " }, { "kind": "Content", - "text": "number" + "text": "2048" + } + ], + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "GROUP_7" + }, + { + "kind": "EnumMember", + "canonicalReference": "server!CollisionGroup.GROUP_8:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "GROUP_8 = " }, { "kind": "Content", - "text": ";" + "text": "4096" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "swimUpwardVelocity", - "propertyTypeTokenRange": { + "initializerTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "releaseTag": "Public", + "name": "GROUP_8" }, { - "kind": "Method", - "canonicalReference": "server!DefaultPlayerEntityController#tickWithPlayerInput:member(1)", - "docComment": "/**\n * Ticks the player movement for the entity controller, overriding the default implementation. If the entity to tick is a child entity, only the event will be emitted but the default movement logic will not be applied.\n *\n * @remarks\n *\n * **Rotation (-Z forward):** Sets entity rotation based on camera yaw. A yaw of 0 faces -Z. Movement direction offsets (WASD/joystick) are added to camera yaw to determine facing. Models must be authored with their front facing -Z.\n *\n * **Child entities:** If `entity.parent` is set, only emits the event and returns early. Movement logic is skipped for child entities.\n *\n * **Input cancellation:** If `autoCancelMouseLeftClick` is true (default), `input.ml` is set to `false` after processing to prevent repeated triggers.\n *\n * **Animations:** Automatically manages idle, walk, run, jump, swim, and interact animations based on movement state and input.\n *\n * @param entity - The entity to tick.\n *\n * @param input - The current input state of the player.\n *\n * @param cameraOrientation - The current camera orientation state of the player.\n *\n * @param deltaTimeMs - The delta time in milliseconds since the last tick.\n *\n * **Category:** Controllers\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!CollisionGroup.GROUP_9:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "tickWithPlayerInput(entity: " - }, - { - "kind": "Reference", - "text": "PlayerEntity", - "canonicalReference": "server!PlayerEntity:class" + "text": "GROUP_9 = " }, { "kind": "Content", - "text": ", input: " - }, - { - "kind": "Reference", - "text": "PlayerInput", - "canonicalReference": "server!PlayerInput:type" - }, + "text": "8192" + } + ], + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "GROUP_9" + }, + { + "kind": "EnumMember", + "canonicalReference": "server!CollisionGroup.PLAYER:member", + "docComment": "", + "excerptTokens": [ { "kind": "Content", - "text": ", cameraOrientation: " + "text": "PLAYER = " }, { - "kind": "Reference", - "text": "PlayerCameraOrientation", - "canonicalReference": "server!PlayerCameraOrientation:type" - }, + "kind": "Content", + "text": "16" + } + ], + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "PLAYER" + } + ] + }, + { + "kind": "TypeAlias", + "canonicalReference": "server!CollisionGroups:type", + "docComment": "/**\n * A set of collision groups.\n *\n * **Category:** Physics\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export type CollisionGroups = " + }, + { + "kind": "Content", + "text": "{\n belongsTo: " + }, + { + "kind": "Reference", + "text": "CollisionGroup", + "canonicalReference": "server!CollisionGroup:enum" + }, + { + "kind": "Content", + "text": "[];\n collidesWith: " + }, + { + "kind": "Reference", + "text": "CollisionGroup", + "canonicalReference": "server!CollisionGroup:enum" + }, + { + "kind": "Content", + "text": "[];\n}" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "src/worlds/physics/CollisionGroupsBuilder.ts", + "releaseTag": "Public", + "name": "CollisionGroups", + "typeTokenRange": { + "startIndex": 1, + "endIndex": 6 + } + }, + { + "kind": "Class", + "canonicalReference": "server!CollisionGroupsBuilder:class", + "docComment": "/**\n * A helper class for building and decoding collision groups.\n *\n * When to use: creating custom collision filters for colliders and rigid bodies. Do NOT use for: per-frame changes; collision group changes are usually infrequent.\n *\n * @remarks\n *\n * Use the static methods directly to encode or decode collision group masks.\n *\n * **Category:** Physics\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export default class CollisionGroupsBuilder " + } + ], + "fileUrlPath": "src/worlds/physics/CollisionGroupsBuilder.ts", + "releaseTag": "Public", + "isAbstract": false, + "name": "CollisionGroupsBuilder", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Method", + "canonicalReference": "server!CollisionGroupsBuilder.buildRawCollisionGroups:member(1)", + "docComment": "/**\n * Builds a raw collision group mask from a set of collision groups.\n *\n * @param collisionGroups - The set of collision groups to build.\n *\n * @returns A raw set of collision groups represented as a 32-bit number.\n *\n * **Category:** Physics\n */\n", + "excerptTokens": [ { "kind": "Content", - "text": ", deltaTimeMs: " + "text": "static buildRawCollisionGroups(collisionGroups: " }, { - "kind": "Content", - "text": "number" + "kind": "Reference", + "text": "CollisionGroups", + "canonicalReference": "server!CollisionGroups:type" }, { "kind": "Content", "text": "): " }, { - "kind": "Content", - "text": "void" + "kind": "Reference", + "text": "RawCollisionGroups", + "canonicalReference": "server!RawCollisionGroups:type" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, + "isStatic": true, "returnTypeTokenRange": { - "startIndex": 9, - "endIndex": 10 + "startIndex": 3, + "endIndex": 4 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "entity", + "parameterName": "collisionGroups", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "buildRawCollisionGroups" + }, + { + "kind": "Method", + "canonicalReference": "server!CollisionGroupsBuilder.decodeCollisionGroups:member(1)", + "docComment": "/**\n * Decodes collision groups into their string equivalents.\n *\n * @param collisionGroups - The set of collision groups to decode.\n *\n * @returns A set of collision groups represented as their string equivalents.\n *\n * **Category:** Physics\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "static decodeCollisionGroups(collisionGroups: " }, { - "parameterName": "input", - "parameterTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, - "isOptional": false + "kind": "Reference", + "text": "CollisionGroups", + "canonicalReference": "server!CollisionGroups:type" }, { - "parameterName": "cameraOrientation", - "parameterTypeTokenRange": { - "startIndex": 5, - "endIndex": 6 - }, - "isOptional": false + "kind": "Content", + "text": "): " }, { - "parameterName": "deltaTimeMs", + "kind": "Reference", + "text": "DecodedCollisionGroups", + "canonicalReference": "server!DecodedCollisionGroups:type" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": true, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "collisionGroups", "parameterTypeTokenRange": { - "startIndex": 7, - "endIndex": 8 + "startIndex": 1, + "endIndex": 2 }, "isOptional": false } ], "isOptional": false, "isAbstract": false, - "name": "tickWithPlayerInput" + "name": "decodeCollisionGroups" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#walkLoopedAnimations:member", - "docComment": "/**\n * The looped animation(s) that will play when the entity is walking.\n */\n", + "kind": "Method", + "canonicalReference": "server!CollisionGroupsBuilder.decodeRawCollisionGroups:member(1)", + "docComment": "/**\n * Decodes a raw collision group mask into a set of collision groups.\n *\n * @param groups - The raw set of collision groups to decode.\n *\n * @returns A set of collision groups.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "walkLoopedAnimations: " + "text": "static decodeRawCollisionGroups(groups: " + }, + { + "kind": "Reference", + "text": "RawCollisionGroups", + "canonicalReference": "server!RawCollisionGroups:type" }, { "kind": "Content", - "text": "string[]" + "text": "): " + }, + { + "kind": "Reference", + "text": "CollisionGroups", + "canonicalReference": "server!CollisionGroups:type" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "walkLoopedAnimations", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 + "isStatic": true, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 }, - "isStatic": false, + "releaseTag": "Public", "isProtected": false, - "isAbstract": false + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "groups", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "decodeRawCollisionGroups" }, { - "kind": "Property", - "canonicalReference": "server!DefaultPlayerEntityController#walkVelocity:member", - "docComment": "/**\n * The normalized horizontal velocity applied to the entity when it walks.\n */\n", + "kind": "Method", + "canonicalReference": "server!CollisionGroupsBuilder.isDefaultCollisionGroups:member(1)", + "docComment": "/**\n * Checks if the collision groups are the default collision groups.\n *\n * @param collisionGroups - The set of collision groups to check.\n *\n * @returns Whether the collision groups are the default collision groups.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "walkVelocity: " + "text": "static isDefaultCollisionGroups(collisionGroups: " + }, + { + "kind": "Reference", + "text": "CollisionGroups", + "canonicalReference": "server!CollisionGroups:type" }, { "kind": "Content", - "text": "number" + "text": "): " + }, + { + "kind": "Content", + "text": "boolean" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "walkVelocity", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 + "isStatic": true, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 }, - "isStatic": false, + "releaseTag": "Public", "isProtected": false, - "isAbstract": false + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "collisionGroups", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "isDefaultCollisionGroups" } ], - "extendsTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, "implementsTokenRanges": [] }, + { + "kind": "TypeAlias", + "canonicalReference": "server!CommandCallback:type", + "docComment": "/**\n * A callback function for a chat command.\n *\n * @param player - The player that sent the command.\n *\n * @param args - An array of arguments, comprised of all space separated text after the command.\n *\n * @param message - The full message of the command. **Category:** Chat\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export type CommandCallback = " + }, + { + "kind": "Content", + "text": "(player: " + }, + { + "kind": "Reference", + "text": "Player", + "canonicalReference": "server!Player:class" + }, + { + "kind": "Content", + "text": ", args: string[], message: string) => void" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "src/worlds/chat/ChatManager.ts", + "releaseTag": "Public", + "name": "CommandCallback", + "typeTokenRange": { + "startIndex": 1, + "endIndex": 4 + } + }, { "kind": "Interface", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions:interface", - "docComment": "/**\n * Options for creating a DefaultPlayerEntityController instance.\n *\n * Use for: configuring default player movement and animation behavior at construction time. Do NOT use for: per-frame changes; override methods or adjust controller state instead.\n *\n * **Category:** Controllers\n *\n * @public\n */\n", + "canonicalReference": "server!CompressedWorldMap:interface", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "export interface DefaultPlayerEntityControllerOptions " + "text": "export interface CompressedWorldMap " } ], - "fileUrlPath": "src/worlds/entities/controllers/DefaultPlayerEntityController.ts", + "fileUrlPath": "src/worlds/maps/WorldMapCodec.ts", "releaseTag": "Public", - "name": "DefaultPlayerEntityControllerOptions", + "name": "CompressedWorldMap", "preserveMemberOrder": false, "members": [ { "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#applyDirectionalMovementRotations:member", - "docComment": "/**\n * Whether to apply directional rotations to the entity while moving, defaults to true.\n */\n", + "canonicalReference": "server!CompressedWorldMap#algorithm:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "applyDirectionalMovementRotations?: " + "text": "algorithm?: " }, { - "kind": "Content", - "text": "boolean" + "kind": "Reference", + "text": "CompressedWorldMapAlgorithm", + "canonicalReference": "server!CompressedWorldMapAlgorithm:type" }, { "kind": "Content", @@ -13778,7 +13557,7 @@ "isReadonly": false, "isOptional": true, "releaseTag": "Public", - "name": "applyDirectionalMovementRotations", + "name": "algorithm", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -13786,43 +13565,39 @@ }, { "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#autoCancelMouseLeftClick:member", - "docComment": "/**\n * Whether to automatically cancel left click input after first processed tick, defaults to true.\n */\n", + "canonicalReference": "server!CompressedWorldMap#blockTypes:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "autoCancelMouseLeftClick?: " + "text": "blockTypes?: " }, { - "kind": "Content", - "text": "boolean" + "kind": "Reference", + "text": "BlockTypeOptions", + "canonicalReference": "server!BlockTypeOptions:interface" }, { "kind": "Content", - "text": ";" - } - ], - "isReadonly": false, - "isOptional": true, - "releaseTag": "Public", - "name": "autoCancelMouseLeftClick", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } - }, - { - "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#canJump:member", - "docComment": "/**\n * A function allowing custom logic to determine if the entity can jump.\n */\n", - "excerptTokens": [ + "text": "[] | " + }, + { + "kind": "Reference", + "text": "Record", + "canonicalReference": "!Record:type" + }, { "kind": "Content", - "text": "canJump?: " + "text": " boolean" + "text": ">" }, { "kind": "Content", @@ -13832,24 +13607,25 @@ "isReadonly": false, "isOptional": true, "releaseTag": "Public", - "name": "canJump", + "name": "blockTypes", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 2 + "endIndex": 7 } }, { "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#canRun:member", - "docComment": "/**\n * A function allowing custom logic to determine if the entity can run.\n */\n", + "canonicalReference": "server!CompressedWorldMap#bounds:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "canRun?: " + "text": "bounds: " }, { - "kind": "Content", - "text": "() => boolean" + "kind": "Reference", + "text": "CompressedWorldMapBounds", + "canonicalReference": "server!~CompressedWorldMapBounds:interface" }, { "kind": "Content", @@ -13857,9 +13633,9 @@ } ], "isReadonly": false, - "isOptional": true, + "isOptional": false, "releaseTag": "Public", - "name": "canRun", + "name": "bounds", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -13867,16 +13643,16 @@ }, { "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#canSwim:member", - "docComment": "/**\n * A function allowing custom logic to determine if the entity can swim.\n */\n", + "canonicalReference": "server!CompressedWorldMap#codecVersion:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "canSwim?: " + "text": "codecVersion?: " }, { "kind": "Content", - "text": "() => boolean" + "text": "number" }, { "kind": "Content", @@ -13886,7 +13662,7 @@ "isReadonly": false, "isOptional": true, "releaseTag": "Public", - "name": "canSwim", + "name": "codecVersion", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -13894,16 +13670,16 @@ }, { "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#canWalk:member", - "docComment": "/**\n * A function allowing custom logic to determine if the entity can walk.\n */\n", + "canonicalReference": "server!CompressedWorldMap#data:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "canWalk?: " + "text": "data: " }, { "kind": "Content", - "text": "() => boolean" + "text": "string" }, { "kind": "Content", @@ -13911,9 +13687,9 @@ } ], "isReadonly": false, - "isOptional": true, + "isOptional": false, "releaseTag": "Public", - "name": "canWalk", + "name": "data", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -13921,16 +13697,21 @@ }, { "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#facesCameraWhenIdle:member", - "docComment": "/**\n * Whether the entity rotates to face the camera direction when idle.\n */\n", + "canonicalReference": "server!CompressedWorldMap#entities:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "facesCameraWhenIdle?: " + "text": "entities?: " + }, + { + "kind": "Reference", + "text": "WorldMap", + "canonicalReference": "server!WorldMap:interface" }, { "kind": "Content", - "text": "boolean" + "text": "['entities']" }, { "kind": "Content", @@ -13940,24 +13721,24 @@ "isReadonly": false, "isOptional": true, "releaseTag": "Public", - "name": "facesCameraWhenIdle", + "name": "entities", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 2 + "endIndex": 3 } }, { "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#idleLoopedAnimations:member", - "docComment": "/**\n * Overrides the animation(s) that will play when the entity is idle.\n */\n", + "canonicalReference": "server!CompressedWorldMap#format:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "idleLoopedAnimations?: " + "text": "format?: " }, { "kind": "Content", - "text": "string[]" + "text": "'hytopia.worldmap.compressed'" }, { "kind": "Content", @@ -13967,7 +13748,7 @@ "isReadonly": false, "isOptional": true, "releaseTag": "Public", - "name": "idleLoopedAnimations", + "name": "format", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -13975,16 +13756,16 @@ }, { "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#interactOneshotAnimations:member", - "docComment": "/**\n * Overrides the animation(s) that will play when the entity interacts (left click)\n */\n", + "canonicalReference": "server!CompressedWorldMap#mapVersion:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "interactOneshotAnimations?: " + "text": "mapVersion?: " }, { "kind": "Content", - "text": "string[]" + "text": "unknown" }, { "kind": "Content", @@ -13994,7 +13775,7 @@ "isReadonly": false, "isOptional": true, "releaseTag": "Public", - "name": "interactOneshotAnimations", + "name": "mapVersion", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -14002,16 +13783,16 @@ }, { "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#jumpLandHeavyOneshotAnimations:member", - "docComment": "/**\n * Overrides the animation(s) that will play when the entity lands with a high velocity.\n */\n", + "canonicalReference": "server!CompressedWorldMap#metadata:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "jumpLandHeavyOneshotAnimations?: " + "text": "metadata?: " }, { "kind": "Content", - "text": "string[]" + "text": "unknown" }, { "kind": "Content", @@ -14021,7 +13802,7 @@ "isReadonly": false, "isOptional": true, "releaseTag": "Public", - "name": "jumpLandHeavyOneshotAnimations", + "name": "metadata", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -14029,16 +13810,17 @@ }, { "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#jumpLandLightOneshotAnimations:member", - "docComment": "/**\n * Overrides the animation(s) that will play when the entity lands after jumping or being airborne.\n */\n", + "canonicalReference": "server!CompressedWorldMap#options:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "jumpLandLightOneshotAnimations?: " + "text": "options?: " }, { - "kind": "Content", - "text": "string[]" + "kind": "Reference", + "text": "CompressedWorldMapOptions", + "canonicalReference": "server!~CompressedWorldMapOptions:interface" }, { "kind": "Content", @@ -14048,7 +13830,7 @@ "isReadonly": false, "isOptional": true, "releaseTag": "Public", - "name": "jumpLandLightOneshotAnimations", + "name": "options", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -14056,16 +13838,16 @@ }, { "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#jumpOneshotAnimations:member", - "docComment": "/**\n * Overrides the animation(s) that will play when the entity is jumping.\n */\n", + "canonicalReference": "server!CompressedWorldMap#version:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "jumpOneshotAnimations?: " + "text": "version?: " }, { "kind": "Content", - "text": "string[]" + "text": "string" }, { "kind": "Content", @@ -14075,24 +13857,69 @@ "isReadonly": false, "isOptional": true, "releaseTag": "Public", - "name": "jumpOneshotAnimations", + "name": "version", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 } + } + ], + "extendsTokenRanges": [] + }, + { + "kind": "TypeAlias", + "canonicalReference": "server!CompressedWorldMapAlgorithm:type", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "export type CompressedWorldMapAlgorithm = " + }, + { + "kind": "Content", + "text": "'brotli' | 'gzip' | 'none'" }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "src/worlds/maps/WorldMapCodec.ts", + "releaseTag": "Public", + "name": "CompressedWorldMapAlgorithm", + "typeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, + { + "kind": "Interface", + "canonicalReference": "server!CompressWorldMapOptions:interface", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "export interface CompressWorldMapOptions " + } + ], + "fileUrlPath": "src/worlds/maps/WorldMapCodec.ts", + "releaseTag": "Public", + "name": "CompressWorldMapOptions", + "preserveMemberOrder": false, + "members": [ { "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#jumpVelocity:member", - "docComment": "/**\n * The upward velocity applied to the entity when it jumps.\n */\n", + "canonicalReference": "server!CompressWorldMapOptions#algorithm:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "jumpVelocity?: " + "text": "algorithm?: " }, { - "kind": "Content", - "text": "number" + "kind": "Reference", + "text": "CompressedWorldMapAlgorithm", + "canonicalReference": "server!CompressedWorldMapAlgorithm:type" }, { "kind": "Content", @@ -14102,7 +13929,7 @@ "isReadonly": false, "isOptional": true, "releaseTag": "Public", - "name": "jumpVelocity", + "name": "algorithm", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -14110,16 +13937,16 @@ }, { "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#runLoopedAnimations:member", - "docComment": "/**\n * Overrides the animation(s) that will play when the entity is running.\n */\n", + "canonicalReference": "server!CompressWorldMapOptions#includeRotations:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "runLoopedAnimations?: " + "text": "includeRotations?: " }, { "kind": "Content", - "text": "string[]" + "text": "boolean" }, { "kind": "Content", @@ -14129,7 +13956,7 @@ "isReadonly": false, "isOptional": true, "releaseTag": "Public", - "name": "runLoopedAnimations", + "name": "includeRotations", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -14137,12 +13964,12 @@ }, { "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#runVelocity:member", - "docComment": "/**\n * The normalized horizontal velocity applied to the entity when it runs.\n */\n", + "canonicalReference": "server!CompressWorldMapOptions#level:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "runVelocity?: " + "text": "level?: " }, { "kind": "Content", @@ -14156,24 +13983,51 @@ "isReadonly": false, "isOptional": true, "releaseTag": "Public", - "name": "runVelocity", + "name": "level", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 } - }, - { - "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#sticksToPlatforms:member", - "docComment": "/**\n * Whether the entity sticks to platforms, defaults to true.\n */\n", + } + ], + "extendsTokenRanges": [] + }, + { + "kind": "Interface", + "canonicalReference": "server!ConeColliderOptions:interface", + "docComment": "/**\n * The options for a cone collider.\n *\n * Use for: cone-shaped colliders. Do NOT use for: other shapes; use the matching collider option type.\n *\n * **Category:** Physics\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export interface ConeColliderOptions extends " + }, + { + "kind": "Reference", + "text": "BaseColliderOptions", + "canonicalReference": "server!BaseColliderOptions:interface" + }, + { + "kind": "Content", + "text": " " + } + ], + "fileUrlPath": "src/worlds/physics/Collider.ts", + "releaseTag": "Public", + "name": "ConeColliderOptions", + "preserveMemberOrder": false, + "members": [ + { + "kind": "PropertySignature", + "canonicalReference": "server!ConeColliderOptions#halfHeight:member", + "docComment": "/**\n * The half height of the cone collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "sticksToPlatforms?: " + "text": "halfHeight?: " }, { "kind": "Content", - "text": "boolean" + "text": "number" }, { "kind": "Content", @@ -14183,7 +14037,7 @@ "isReadonly": false, "isOptional": true, "releaseTag": "Public", - "name": "sticksToPlatforms", + "name": "halfHeight", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -14191,12 +14045,12 @@ }, { "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#swimFastVelocity:member", - "docComment": "/**\n * The normalized horizontal velocity applied to the entity when it swims fast (equivalent to running).\n */\n", + "canonicalReference": "server!ConeColliderOptions#radius:member", + "docComment": "/**\n * The radius of the cone collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "swimFastVelocity?: " + "text": "radius?: " }, { "kind": "Content", @@ -14210,7 +14064,7 @@ "isReadonly": false, "isOptional": true, "releaseTag": "Public", - "name": "swimFastVelocity", + "name": "radius", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -14218,16 +14072,17 @@ }, { "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#swimGravity:member", - "docComment": "/**\n * The gravity modifier applied to the entity when swimming.\n */\n", + "canonicalReference": "server!ConeColliderOptions#shape:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "swimGravity?: " + "text": "shape: " }, { - "kind": "Content", - "text": "number" + "kind": "Reference", + "text": "ColliderShape.CONE", + "canonicalReference": "server!ColliderShape.CONE:member" }, { "kind": "Content", @@ -14235,107 +14090,297 @@ } ], "isReadonly": false, - "isOptional": true, + "isOptional": false, "releaseTag": "Public", - "name": "swimGravity", + "name": "shape", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 } + } + ], + "extendsTokenRanges": [ + { + "startIndex": 1, + "endIndex": 2 + } + ] + }, + { + "kind": "TypeAlias", + "canonicalReference": "server!ContactForceData:type", + "docComment": "/**\n * Data for contact forces.\n *\n * **Category:** Physics\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export type ContactForceData = " }, { - "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#swimIdleLoopedAnimations:member", - "docComment": "/**\n * The looped animation(s) that will play when the entity is not moving while swimming.\n */\n", + "kind": "Content", + "text": "{\n totalForce: " + }, + { + "kind": "Reference", + "text": "RAPIER.Vector", + "canonicalReference": "@dimforge/rapier3d-simd-compat!Vector:interface" + }, + { + "kind": "Content", + "text": ";\n totalForceMagnitude: number;\n maxForceDirection: " + }, + { + "kind": "Reference", + "text": "RAPIER.Vector", + "canonicalReference": "@dimforge/rapier3d-simd-compat!Vector:interface" + }, + { + "kind": "Content", + "text": ";\n maxForceMagnitude: number;\n}" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "src/worlds/physics/Simulation.ts", + "releaseTag": "Public", + "name": "ContactForceData", + "typeTokenRange": { + "startIndex": 1, + "endIndex": 6 + } + }, + { + "kind": "TypeAlias", + "canonicalReference": "server!ContactManifold:type", + "docComment": "/**\n * A contact manifold.\n *\n * **Category:** Physics\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export type ContactManifold = " + }, + { + "kind": "Content", + "text": "{\n contactPoints: " + }, + { + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" + }, + { + "kind": "Content", + "text": "[];\n localNormalA: " + }, + { + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" + }, + { + "kind": "Content", + "text": ";\n localNormalB: " + }, + { + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" + }, + { + "kind": "Content", + "text": ";\n normal: " + }, + { + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" + }, + { + "kind": "Content", + "text": ";\n}" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "src/worlds/physics/Simulation.ts", + "releaseTag": "Public", + "name": "ContactManifold", + "typeTokenRange": { + "startIndex": 1, + "endIndex": 10 + } + }, + { + "kind": "Class", + "canonicalReference": "server!CpuProfiler:class", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "export default class CpuProfiler " + } + ], + "fileUrlPath": "src/metrics/CpuProfiler.ts", + "releaseTag": "Public", + "isAbstract": false, + "name": "CpuProfiler", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Method", + "canonicalReference": "server!CpuProfiler.captureHeapSnapshot:member(1)", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "swimIdleLoopedAnimations?: " + "text": "static captureHeapSnapshot(outputPath?: " }, { "kind": "Content", - "text": "string[]" + "text": "string" }, { "kind": "Content", - "text": ";" - } - ], - "isReadonly": false, - "isOptional": true, - "releaseTag": "Public", - "name": "swimIdleLoopedAnimations", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } - }, - { - "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#swimLoopedAnimations:member", - "docComment": "/**\n * The looped animation(s) that will play when the entity is swimming in any direction.\n */\n", - "excerptTokens": [ + "text": "): " + }, { - "kind": "Content", - "text": "swimLoopedAnimations?: " + "kind": "Reference", + "text": "Promise", + "canonicalReference": "!Promise:interface" }, { "kind": "Content", - "text": "string[]" + "text": "" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": true, + "isStatic": true, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 5 + }, "releaseTag": "Public", - "name": "swimLoopedAnimations", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "outputPath", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": true + } + ], + "isOptional": false, + "isAbstract": false, + "name": "captureHeapSnapshot" }, { - "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#swimMaxGravityVelocity:member", - "docComment": "/**\n * The maximum downward velocity that the entity can reach when affected by gravity while swimming.\n */\n", + "kind": "Method", + "canonicalReference": "server!CpuProfiler.captureProfile:member(1)", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "swimMaxGravityVelocity?: " + "text": "static captureProfile(durationMs: " }, { "kind": "Content", "text": "number" }, + { + "kind": "Content", + "text": ", outputPath?: " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "Promise", + "canonicalReference": "!Promise:interface" + }, + { + "kind": "Content", + "text": "" + }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": true, + "isStatic": true, + "returnTypeTokenRange": { + "startIndex": 5, + "endIndex": 7 + }, "releaseTag": "Public", - "name": "swimMaxGravityVelocity", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } - }, + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "durationMs", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "outputPath", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": true + } + ], + "isOptional": false, + "isAbstract": false, + "name": "captureProfile" + } + ], + "implementsTokenRanges": [] + }, + { + "kind": "Interface", + "canonicalReference": "server!CreateWorldMapChunkCacheOptions:interface", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "export interface CreateWorldMapChunkCacheOptions " + } + ], + "fileUrlPath": "src/worlds/maps/WorldMapChunkCacheCodec.ts", + "releaseTag": "Public", + "name": "CreateWorldMapChunkCacheOptions", + "preserveMemberOrder": false, + "members": [ { "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#swimSlowVelocity:member", - "docComment": "/**\n * The normalized horizontal velocity applied to the entity when it swims slowly (equivalent to walking).\n */\n", + "canonicalReference": "server!CreateWorldMapChunkCacheOptions#algorithm:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "swimSlowVelocity?: " + "text": "algorithm?: " }, { - "kind": "Content", - "text": "number" + "kind": "Reference", + "text": "WorldMapChunkCacheAlgorithm", + "canonicalReference": "server!WorldMapChunkCacheAlgorithm:type" }, { "kind": "Content", @@ -14345,7 +14390,7 @@ "isReadonly": false, "isOptional": true, "releaseTag": "Public", - "name": "swimSlowVelocity", + "name": "algorithm", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -14353,16 +14398,16 @@ }, { "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#swimUpwardVelocity:member", - "docComment": "/**\n * The upward velocity applied to the entity when swimming.\n */\n", + "canonicalReference": "server!CreateWorldMapChunkCacheOptions#includeRotations:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "swimUpwardVelocity?: " + "text": "includeRotations?: " }, { "kind": "Content", - "text": "number" + "text": "boolean" }, { "kind": "Content", @@ -14372,7 +14417,7 @@ "isReadonly": false, "isOptional": true, "releaseTag": "Public", - "name": "swimUpwardVelocity", + "name": "includeRotations", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -14380,16 +14425,16 @@ }, { "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#walkLoopedAnimations:member", - "docComment": "/**\n * Overrides the animation(s) that will play when the entity is walking.\n */\n", + "canonicalReference": "server!CreateWorldMapChunkCacheOptions#level:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "walkLoopedAnimations?: " + "text": "level?: " }, { "kind": "Content", - "text": "string[]" + "text": "number" }, { "kind": "Content", @@ -14399,7 +14444,7 @@ "isReadonly": false, "isOptional": true, "releaseTag": "Public", - "name": "walkLoopedAnimations", + "name": "level", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -14407,16 +14452,16 @@ }, { "kind": "PropertySignature", - "canonicalReference": "server!DefaultPlayerEntityControllerOptions#walkVelocity:member", - "docComment": "/**\n * The normalized horizontal velocity applied to the entity when it walks.\n */\n", + "canonicalReference": "server!CreateWorldMapChunkCacheOptions#sourceSha256:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "walkVelocity?: " + "text": "sourceSha256?: " }, { "kind": "Content", - "text": "number" + "text": "string" }, { "kind": "Content", @@ -14426,7 +14471,7 @@ "isReadonly": false, "isOptional": true, "releaseTag": "Public", - "name": "walkVelocity", + "name": "sourceSha256", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -14435,78 +14480,38 @@ ], "extendsTokenRanges": [] }, - { - "kind": "TypeAlias", - "canonicalReference": "server!DefaultPlayerEntityOptions:type", - "docComment": "/**\n * Options for creating a DefaultPlayerEntity instance.\n *\n * Use for: customizing the default player avatar (for example cosmetic visibility). Do NOT use for: changing movement behavior; use `DefaultPlayerEntityControllerOptions`.\n *\n * **Category:** Entities\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export type DefaultPlayerEntityOptions = " - }, - { - "kind": "Content", - "text": "{\n cosmeticHiddenSlots?: " - }, - { - "kind": "Reference", - "text": "PlayerCosmeticSlot", - "canonicalReference": "server!PlayerCosmeticSlot:type" - }, - { - "kind": "Content", - "text": "[];\n} & " - }, - { - "kind": "Reference", - "text": "PlayerEntityOptions", - "canonicalReference": "server!PlayerEntityOptions:type" - }, - { - "kind": "Content", - "text": ";" - } - ], - "fileUrlPath": "src/worlds/entities/DefaultPlayerEntity.ts", - "releaseTag": "Public", - "name": "DefaultPlayerEntityOptions", - "typeTokenRange": { - "startIndex": 1, - "endIndex": 5 - } - }, { "kind": "Interface", - "canonicalReference": "server!DynamicRigidBodyOptions:interface", - "docComment": "/**\n * The options for a dynamic rigid body, also the default type.\n *\n * Use for: physics-driven bodies affected by forces and collisions. Do NOT use for: kinematic bodies; use the kinematic option types instead.\n *\n * **Category:** Physics\n *\n * @public\n */\n", + "canonicalReference": "server!CylinderColliderOptions:interface", + "docComment": "/**\n * The options for a cylinder collider.\n *\n * Use for: cylinder-shaped colliders. Do NOT use for: other shapes; use the matching collider option type.\n *\n * **Category:** Physics\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "export interface DynamicRigidBodyOptions extends " + "text": "export interface CylinderColliderOptions extends " }, { "kind": "Reference", - "text": "BaseRigidBodyOptions", - "canonicalReference": "server!BaseRigidBodyOptions:interface" + "text": "BaseColliderOptions", + "canonicalReference": "server!BaseColliderOptions:interface" }, { "kind": "Content", "text": " " } ], - "fileUrlPath": "src/worlds/physics/RigidBody.ts", + "fileUrlPath": "src/worlds/physics/Collider.ts", "releaseTag": "Public", - "name": "DynamicRigidBodyOptions", + "name": "CylinderColliderOptions", "preserveMemberOrder": false, "members": [ { "kind": "PropertySignature", - "canonicalReference": "server!DynamicRigidBodyOptions#additionalMass:member", - "docComment": "/**\n * The additional mass of the rigid body.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!CylinderColliderOptions#halfHeight:member", + "docComment": "/**\n * The half height of the cylinder collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "additionalMass?: " + "text": "halfHeight?: " }, { "kind": "Content", @@ -14520,35 +14525,7 @@ "isReadonly": false, "isOptional": true, "releaseTag": "Public", - "name": "additionalMass", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } - }, - { - "kind": "PropertySignature", - "canonicalReference": "server!DynamicRigidBodyOptions#additionalMassProperties:member", - "docComment": "/**\n * The additional mass properties of the rigid body.\n *\n * **Category:** Physics\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "additionalMassProperties?: " - }, - { - "kind": "Reference", - "text": "RigidBodyAdditionalMassProperties", - "canonicalReference": "server!RigidBodyAdditionalMassProperties:type" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": false, - "isOptional": true, - "releaseTag": "Public", - "name": "additionalMassProperties", + "name": "halfHeight", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -14556,12 +14533,12 @@ }, { "kind": "PropertySignature", - "canonicalReference": "server!DynamicRigidBodyOptions#additionalSolverIterations:member", - "docComment": "/**\n * The additional solver iterations of the rigid body.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!CylinderColliderOptions#radius:member", + "docComment": "/**\n * The radius of the cylinder collider.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "additionalSolverIterations?: " + "text": "radius?: " }, { "kind": "Content", @@ -14575,7 +14552,7 @@ "isReadonly": false, "isOptional": true, "releaseTag": "Public", - "name": "additionalSolverIterations", + "name": "radius", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -14583,16 +14560,17 @@ }, { "kind": "PropertySignature", - "canonicalReference": "server!DynamicRigidBodyOptions#angularDamping:member", - "docComment": "/**\n * The angular damping of the rigid body.\n *\n * **Category:** Physics\n */\n", + "canonicalReference": "server!CylinderColliderOptions#shape:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "angularDamping?: " + "text": "shape: " }, { - "kind": "Content", - "text": "number" + "kind": "Reference", + "text": "ColliderShape.CYLINDER", + "canonicalReference": "server!ColliderShape.CYLINDER:member" }, { "kind": "Content", @@ -14600,164 +14578,242 @@ } ], "isReadonly": false, - "isOptional": true, + "isOptional": false, "releaseTag": "Public", - "name": "angularDamping", + "name": "shape", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 } + } + ], + "extendsTokenRanges": [ + { + "startIndex": 1, + "endIndex": 2 + } + ] + }, + { + "kind": "TypeAlias", + "canonicalReference": "server!DecodedCollisionGroups:type", + "docComment": "/**\n * A decoded set of collision groups represented as their string equivalents.\n *\n * **Category:** Physics\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export type DecodedCollisionGroups = " }, { - "kind": "PropertySignature", - "canonicalReference": "server!DynamicRigidBodyOptions#angularVelocity:member", - "docComment": "/**\n * The angular velocity of the rigid body.\n *\n * **Category:** Physics\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "angularVelocity?: " - }, - { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": false, - "isOptional": true, - "releaseTag": "Public", - "name": "angularVelocity", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } + "kind": "Content", + "text": "{\n belongsTo: string[];\n collidesWith: string[];\n}" }, { - "kind": "PropertySignature", - "canonicalReference": "server!DynamicRigidBodyOptions#ccdEnabled:member", - "docComment": "/**\n * Whether the rigid body has continuous collision detection enabled.\n *\n * **Category:** Physics\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "ccdEnabled?: " - }, - { - "kind": "Content", - "text": "boolean" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": false, - "isOptional": true, - "releaseTag": "Public", - "name": "ccdEnabled", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "src/worlds/physics/CollisionGroupsBuilder.ts", + "releaseTag": "Public", + "name": "DecodedCollisionGroups", + "typeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, + { + "kind": "Variable", + "canonicalReference": "server!DEFAULT_ENTITY_RIGID_BODY_OPTIONS:var", + "docComment": "/**\n * The default rigid body options for a model entity when `EntityOptions.rigidBodyOptions` is not provided.\n *\n * **Category:** Entities\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "DEFAULT_ENTITY_RIGID_BODY_OPTIONS: " }, { - "kind": "PropertySignature", - "canonicalReference": "server!DynamicRigidBodyOptions#dominanceGroup:member", - "docComment": "/**\n * The dominance group of the rigid body.\n *\n * **Category:** Physics\n */\n", + "kind": "Reference", + "text": "RigidBodyOptions", + "canonicalReference": "server!RigidBodyOptions:type" + } + ], + "fileUrlPath": "src/worlds/entities/Entity.ts", + "isReadonly": true, + "releaseTag": "Public", + "name": "DEFAULT_ENTITY_RIGID_BODY_OPTIONS", + "variableTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, + { + "kind": "Class", + "canonicalReference": "server!DefaultPlayerEntity:class", + "docComment": "/**\n * Represents the default player model entity.\n *\n * When to use: standard player avatars with built-in cosmetics and default controls. Do NOT use for: fully custom player rigs that don't match the default model's anchors/animations.\n *\n * @remarks\n *\n * Extends `PlayerEntity`, uses the default player model, and assigns `DefaultPlayerEntityController`. You can override defaults, but if you change `modelUri`, ensure the model has the same animation names and anchor points.\n *\n * @example\n * ```typescript\n * const playerEntity = new DefaultPlayerEntity({ player });\n *\n * playerEntity.spawn(world, { x: 0, y: 10, z: 0 });\n * ```\n *\n * **Category:** Entities\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export default class DefaultPlayerEntity extends " + }, + { + "kind": "Reference", + "text": "PlayerEntity", + "canonicalReference": "server!PlayerEntity:class" + }, + { + "kind": "Content", + "text": " " + } + ], + "fileUrlPath": "src/worlds/entities/DefaultPlayerEntity.ts", + "releaseTag": "Public", + "isAbstract": false, + "name": "DefaultPlayerEntity", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Constructor", + "canonicalReference": "server!DefaultPlayerEntity:constructor(1)", + "docComment": "/**\n * Creates a new DefaultPlayerEntity instance.\n *\n * @remarks\n *\n * **Auto-assigned defaults:** A `DefaultPlayerEntityController` is automatically created and assigned. Default idle animations are initialized as looped and playing.\n *\n * **Cosmetics on spawn:** When spawned, player cosmetics (hair, skin, equipped items) are fetched asynchronously and applied. Child entities are created for hair and equipped cosmetic items.\n *\n * @param options - The options for the default player entity.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "dominanceGroup?: " + "text": "constructor(options: " }, { - "kind": "Content", - "text": "number" + "kind": "Reference", + "text": "DefaultPlayerEntityOptions", + "canonicalReference": "server!DefaultPlayerEntityOptions:type" }, { "kind": "Content", - "text": ";" + "text": ");" } ], - "isReadonly": false, - "isOptional": true, "releaseTag": "Public", - "name": "dominanceGroup", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "options", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ] }, { - "kind": "PropertySignature", - "canonicalReference": "server!DynamicRigidBodyOptions#enabledPositions:member", - "docComment": "/**\n * The enabled axes of positional movement of the rigid body.\n *\n * **Category:** Physics\n */\n", + "kind": "Property", + "canonicalReference": "server!DefaultPlayerEntity#cosmeticHiddenSlots:member", + "docComment": "/**\n * The cosmetic slots that are hidden.\n *\n * **Category:** Entities\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "enabledPositions?: " + "text": "get cosmeticHiddenSlots(): " }, { "kind": "Reference", - "text": "Vector3Boolean", - "canonicalReference": "server!Vector3Boolean:interface" + "text": "PlayerCosmeticSlot", + "canonicalReference": "server!PlayerCosmeticSlot:type" + }, + { + "kind": "Content", + "text": "[]" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": true, + "isReadonly": true, + "isOptional": false, "releaseTag": "Public", - "name": "enabledPositions", + "name": "cosmeticHiddenSlots", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 2 - } + "endIndex": 3 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + } + ], + "extendsTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "implementsTokenRanges": [] + }, + { + "kind": "Class", + "canonicalReference": "server!DefaultPlayerEntityController:class", + "docComment": "/**\n * The default player entity controller implementation.\n *\n * When to use: player-controlled avatars using `DefaultPlayerEntity`. Do NOT use for: NPCs or non-player entities; use `SimpleEntityController` or `PathfindingEntityController` instead.\n *\n * @remarks\n *\n * Extends `BaseEntityController` and implements default movement, platforming, jumping, and swimming. You can extend this class to add custom logic.\n *\n *

Coordinate System & Model Orientation

\n *\n * HYTOPIA uses **-Z as forward**. Models must be authored with their front facing -Z. A yaw of 0 means facing -Z. The controller rotates the entity based on camera yaw and movement direction, always orienting the entity's -Z axis in the intended facing direction.\n *\n * @example\n * ```typescript\n * // Create a custom entity controller for myEntity, prior to spawning it.\n * myEntity.setController(new DefaultPlayerEntityController({\n * jumpVelocity: 10,\n * runVelocity: 8,\n * walkVelocity: 4,\n * }));\n *\n * // Spawn the entity in the world.\n * myEntity.spawn(world, { x: 53, y: 10, z: 23 });\n * ```\n *\n * **Category:** Controllers\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export default class DefaultPlayerEntityController extends " }, { - "kind": "PropertySignature", - "canonicalReference": "server!DynamicRigidBodyOptions#enabledRotations:member", - "docComment": "/**\n * The enabled rotations of the rigid body.\n *\n * **Category:** Physics\n */\n", + "kind": "Reference", + "text": "BaseEntityController", + "canonicalReference": "server!BaseEntityController:class" + }, + { + "kind": "Content", + "text": " " + } + ], + "fileUrlPath": "src/worlds/entities/controllers/DefaultPlayerEntityController.ts", + "releaseTag": "Public", + "isAbstract": false, + "name": "DefaultPlayerEntityController", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Constructor", + "canonicalReference": "server!DefaultPlayerEntityController:constructor(1)", + "docComment": "/**\n * Constructs a new instance of the `DefaultPlayerEntityController` class\n *\n * @param options - Options for the controller.\n *\n * **Category:** Controllers\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "enabledRotations?: " + "text": "constructor(options?: " }, { "kind": "Reference", - "text": "Vector3Boolean", - "canonicalReference": "server!Vector3Boolean:interface" + "text": "DefaultPlayerEntityControllerOptions", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions:interface" }, { "kind": "Content", - "text": ";" + "text": ");" } ], - "isReadonly": false, - "isOptional": true, "releaseTag": "Public", - "name": "enabledRotations", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "options", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": true + } + ] }, { - "kind": "PropertySignature", - "canonicalReference": "server!DynamicRigidBodyOptions#gravityScale:member", - "docComment": "/**\n * The gravity scale of the rigid body.\n *\n * **Category:** Physics\n */\n", + "kind": "Property", + "canonicalReference": "server!DefaultPlayerEntityController#applyDirectionalMovementRotations:member", + "docComment": "/**\n * Whether to apply directional rotations to the entity while moving, defaults to true.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "gravityScale?: " + "text": "applyDirectionalMovementRotations: " }, { "kind": "Content", - "text": "number" + "text": "boolean" }, { "kind": "Content", @@ -14765,77 +14821,74 @@ } ], "isReadonly": false, - "isOptional": true, + "isOptional": false, "releaseTag": "Public", - "name": "gravityScale", + "name": "applyDirectionalMovementRotations", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 - } + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "PropertySignature", - "canonicalReference": "server!DynamicRigidBodyOptions#linearDamping:member", - "docComment": "/**\n * The linear damping of the rigid body.\n *\n * **Category:** Physics\n */\n", + "kind": "Method", + "canonicalReference": "server!DefaultPlayerEntityController#attach:member(1)", + "docComment": "/**\n * Called when the controller is attached to an entity.\n *\n * @remarks\n *\n * **Wraps `applyImpulse`:** The entity's `applyImpulse` method is wrapped to track external velocities separately from internal movement. External impulses decay over time when grounded.\n *\n * **Locks rotations:** Calls `entity.lockAllRotations()` to prevent physics from rotating the entity. Rotation is set explicitly by the controller based on camera orientation.\n *\n * **Enables CCD:** Enables continuous collision detection on the entity.\n *\n * **Swimming detection:** Registers a `BLOCK_COLLISION` listener to detect liquid blocks and manage swimming state, gravity scale, and animations.\n *\n * @param entity - The entity to attach the controller to.\n *\n * **Category:** Controllers\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "linearDamping?: " + "text": "attach(entity: " }, { - "kind": "Content", - "text": "number" + "kind": "Reference", + "text": "Entity", + "canonicalReference": "server!Entity:class" }, { "kind": "Content", - "text": ";" - } - ], - "isReadonly": false, - "isOptional": true, - "releaseTag": "Public", - "name": "linearDamping", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } - }, - { - "kind": "PropertySignature", - "canonicalReference": "server!DynamicRigidBodyOptions#linearVelocity:member", - "docComment": "/**\n * The linear velocity of the rigid body.\n *\n * **Category:** Physics\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "linearVelocity?: " + "text": "): " }, { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" + "kind": "Content", + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": true, + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, "releaseTag": "Public", - "name": "linearVelocity", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "entity", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "attach" }, { - "kind": "PropertySignature", - "canonicalReference": "server!DynamicRigidBodyOptions#sleeping:member", - "docComment": "/**\n * Whether the rigid body is sleeping.\n *\n * **Category:** Physics\n */\n", + "kind": "Property", + "canonicalReference": "server!DefaultPlayerEntityController#autoCancelMouseLeftClick:member", + "docComment": "/**\n * Whether to automatically cancel left click input after first processed tick, defaults to true.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "sleeping?: " + "text": "autoCancelMouseLeftClick: " }, { "kind": "Content", @@ -14847,26 +14900,38 @@ } ], "isReadonly": false, - "isOptional": true, + "isOptional": false, "releaseTag": "Public", - "name": "sleeping", + "name": "autoCancelMouseLeftClick", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 - } + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "PropertySignature", - "canonicalReference": "server!DynamicRigidBodyOptions#softCcdPrediction:member", - "docComment": "/**\n * The soft continuous collision detection prediction of the rigid body.\n *\n * **Category:** Physics\n */\n", + "kind": "Property", + "canonicalReference": "server!DefaultPlayerEntityController#canJump:member", + "docComment": "/**\n * A function allowing custom logic to determine if the entity can jump.\n *\n * @param controller - The default player entity controller instance.\n *\n * @returns Whether the entity of the entity controller can jump.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "softCcdPrediction?: " + "text": "canJump: " }, { "kind": "Content", - "text": "number" + "text": "(controller: " + }, + { + "kind": "Reference", + "text": "DefaultPlayerEntityController", + "canonicalReference": "server!DefaultPlayerEntityController:class" + }, + { + "kind": "Content", + "text": ") => boolean" }, { "kind": "Content", @@ -14874,27 +14939,38 @@ } ], "isReadonly": false, - "isOptional": true, + "isOptional": false, "releaseTag": "Public", - "name": "softCcdPrediction", + "name": "canJump", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 2 - } + "endIndex": 4 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "PropertySignature", - "canonicalReference": "server!DynamicRigidBodyOptions#type:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!DefaultPlayerEntityController#canRun:member", + "docComment": "/**\n * A function allowing custom logic to determine if the entity can run.\n *\n * @param controller - The default player entity controller instance.\n *\n * @returns Whether the entity of the entity controller can run.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "type: " + "text": "canRun: " + }, + { + "kind": "Content", + "text": "(controller: " }, { "kind": "Reference", - "text": "RigidBodyType.DYNAMIC", - "canonicalReference": "server!RigidBodyType.DYNAMIC:member" + "text": "DefaultPlayerEntityController", + "canonicalReference": "server!DefaultPlayerEntityController:class" + }, + { + "kind": "Content", + "text": ") => boolean" }, { "kind": "Content", @@ -14904,117 +14980,49 @@ "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "type", + "name": "canRun", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 2 - } - } - ], - "extendsTokenRanges": [ - { - "startIndex": 1, - "endIndex": 2 - } - ] - }, - { - "kind": "Class", - "canonicalReference": "server!Entity:class", - "docComment": "/**\n * Represents a dynamic or static object in a world.\n *\n * When to use: any non-player object that needs physics, visuals, or interactions. Do NOT use for: player-controlled avatars (use `PlayerEntity` / `DefaultPlayerEntity`). Do NOT use for: voxel blocks (use block APIs on `ChunkLattice`).\n *\n * @remarks\n *\n * Entities are created from a block texture or a `.gltf` model and can have rigid bodies, colliders, animations, and controllers.\n *\n *

Coordinate System

\n *\n * HYTOPIA uses a right-handed coordinate system where: - **+X** is right - **+Y** is up - **-Z** is forward (identity orientation)\n *\n * Models should be authored with their front/forward facing the **-Z axis**. When an entity has identity rotation (0,0,0,1 quaternion or yaw=0), it faces -Z.\n *\n *

Events

\n *\n * This class is an EventRouter, and instances of it emit events with payloads listed under `EntityEventPayloads`.\n *\n * @example\n * ```typescript\n * const spider = new Entity({\n * name: 'Spider',\n * modelUri: 'models/spider.gltf',\n * rigidBodyOptions: {\n * type: RigidBodyType.DYNAMIC,\n * enabledRotations: { x: false, y: true, z: false },\n * colliders: [\n * {\n * shape: ColliderShape.ROUND_CYLINDER,\n * borderRadius: 0.1,\n * halfHeight: 0.225,\n * radius: 0.5,\n * tag: 'body',\n * }\n * ],\n * },\n * });\n *\n * spider.spawn(world, { x: 20, y: 6, z: 10 });\n * ```\n *\n * **Category:** Entities\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export default class Entity extends " - }, - { - "kind": "Reference", - "text": "RigidBody", - "canonicalReference": "server!RigidBody:class" - }, - { - "kind": "Content", - "text": " implements " - }, - { - "kind": "Reference", - "text": "protocol.Serializable", - "canonicalReference": "@hytopia.com/server-protocol!Serializable:interface" + "endIndex": 4 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "Content", - "text": " " - } - ], - "fileUrlPath": "src/worlds/entities/Entity.ts", - "releaseTag": "Public", - "isAbstract": false, - "name": "Entity", - "preserveMemberOrder": false, - "members": [ - { - "kind": "Constructor", - "canonicalReference": "server!Entity:constructor(1)", - "docComment": "/**\n * Creates a new Entity instance.\n *\n * Use for: defining a new entity before spawning it into a world. Do NOT use for: player-controlled avatars (use `PlayerEntity` or `DefaultPlayerEntity`).\n *\n * @remarks\n *\n * Exactly one of `blockTextureUri` or `modelUri` must be provided. If `controller` is provided, `controller.attach(this)` is called during construction (before spawn).\n *\n * @param options - The options for the entity.\n *\n * **Requires:** If `parent` is provided, it must already be spawned.\n *\n * **Side effects:** May attach the provided controller.\n *\n * **Category:** Entities\n */\n", + "kind": "Property", + "canonicalReference": "server!DefaultPlayerEntityController#canSwim:member", + "docComment": "/**\n * A function allowing custom logic to determine if the entity can swim.\n *\n * @param controller - The default player entity controller instance.\n *\n * @returns Whether the entity of the entity controller can swim.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "constructor(options: " - }, - { - "kind": "Reference", - "text": "EntityOptions", - "canonicalReference": "server!EntityOptions:type" + "text": "canSwim: " }, { "kind": "Content", - "text": ");" - } - ], - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "options", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ] - }, - { - "kind": "Property", - "canonicalReference": "server!Entity#availableModelAnimationNames:member", - "docComment": "/**\n * The names of the animations available in the entity's model.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "get availableModelAnimationNames(): " + "text": "(controller: " }, { "kind": "Reference", - "text": "Readonly", - "canonicalReference": "!Readonly:type" + "text": "DefaultPlayerEntityController", + "canonicalReference": "server!DefaultPlayerEntityController:class" }, { "kind": "Content", - "text": "" + "text": ") => boolean" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, + "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "availableModelAnimationNames", + "name": "canSwim", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 3 + "endIndex": 4 }, "isStatic": false, "isProtected": false, @@ -15022,34 +15030,38 @@ }, { "kind": "Property", - "canonicalReference": "server!Entity#availableModelNodeNames:member", - "docComment": "/**\n * The names of the nodes available in the entity's model.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!DefaultPlayerEntityController#canWalk:member", + "docComment": "/**\n * A function allowing custom logic to determine if the entity can walk.\n *\n * @param controller - The default player entity controller instance.\n *\n * @returns Whether the entity of the entity controller can walk.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get availableModelNodeNames(): " + "text": "canWalk: " + }, + { + "kind": "Content", + "text": "(controller: " }, { "kind": "Reference", - "text": "Readonly", - "canonicalReference": "!Readonly:type" + "text": "DefaultPlayerEntityController", + "canonicalReference": "server!DefaultPlayerEntityController:class" }, { "kind": "Content", - "text": "" + "text": ") => boolean" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, + "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "availableModelNodeNames", + "name": "canWalk", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 3 + "endIndex": 4 }, "isStatic": false, "isProtected": false, @@ -15057,34 +15069,29 @@ }, { "kind": "Property", - "canonicalReference": "server!Entity#blockHalfExtents:member", - "docComment": "/**\n * The half extents of the block entity's visual size.\n *\n * @remarks\n *\n * Only set for block entities.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!DefaultPlayerEntityController#facesCameraWhenIdle:member", + "docComment": "/**\n * Whether the entity rotates to face the camera direction when idle. When `true`, the entity always faces the camera direction. When `false`, the entity only rotates while actively moving.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get blockHalfExtents(): " - }, - { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" + "text": "facesCameraWhenIdle: " }, { "kind": "Content", - "text": " | undefined" + "text": "boolean" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, + "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "blockHalfExtents", + "name": "facesCameraWhenIdle", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 3 + "endIndex": 2 }, "isStatic": false, "isProtected": false, @@ -15092,26 +15099,26 @@ }, { "kind": "Property", - "canonicalReference": "server!Entity#blockTextureUri:member", - "docComment": "/**\n * The texture URI for block entities.\n *\n * @remarks\n *\n * When set, this entity is treated as a block entity.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!DefaultPlayerEntityController#idleLoopedAnimations:member", + "docComment": "/**\n * The looped animation(s) that will play when the entity is idle.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get blockTextureUri(): " + "text": "idleLoopedAnimations: " }, { "kind": "Content", - "text": "string | undefined" + "text": "string[]" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, + "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "blockTextureUri", + "name": "idleLoopedAnimations", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -15121,53 +15128,47 @@ "isAbstract": false }, { - "kind": "Method", - "canonicalReference": "server!Entity#clearModelNodeOverrides:member(1)", - "docComment": "/**\n * Clears all model node overrides from the entity's model.\n *\n * **Category:** Entities\n */\n", + "kind": "Property", + "canonicalReference": "server!DefaultPlayerEntityController#interactOneshotAnimations:member", + "docComment": "/**\n * The oneshot animation(s) that will play when the entity interacts (left click)\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "clearModelNodeOverrides(): " + "text": "interactOneshotAnimations: " }, { "kind": "Content", - "text": "void" + "text": "string[]" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "interactOneshotAnimations", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", + "isStatic": false, "isProtected": false, - "overloadIndex": 1, - "parameters": [], - "isOptional": false, - "isAbstract": false, - "name": "clearModelNodeOverrides" + "isAbstract": false }, { "kind": "Property", - "canonicalReference": "server!Entity#controller:member", - "docComment": "/**\n * The controller for the entity.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!DefaultPlayerEntityController#isActivelyMoving:member", + "docComment": "/**\n * Whether the entity is moving from player inputs.\n *\n * **Category:** Controllers\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get controller(): " - }, - { - "kind": "Reference", - "text": "BaseEntityController", - "canonicalReference": "server!BaseEntityController:class" + "text": "get isActivelyMoving(): " }, { "kind": "Content", - "text": " | undefined" + "text": "boolean" }, { "kind": "Content", @@ -15177,10 +15178,10 @@ "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "controller", + "name": "isActivelyMoving", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 3 + "endIndex": 2 }, "isStatic": false, "isProtected": false, @@ -15188,16 +15189,16 @@ }, { "kind": "Property", - "canonicalReference": "server!Entity#depth:member", - "docComment": "/**\n * The depth (Z-axis) of the entity's model or block size.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!DefaultPlayerEntityController#isGrounded:member", + "docComment": "/**\n * Whether the entity is grounded.\n *\n * **Category:** Controllers\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get depth(): " + "text": "get isGrounded(): " }, { "kind": "Content", - "text": "number" + "text": "boolean" }, { "kind": "Content", @@ -15207,7 +15208,7 @@ "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "depth", + "name": "isGrounded", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -15217,53 +15218,47 @@ "isAbstract": false }, { - "kind": "Method", - "canonicalReference": "server!Entity#despawn:member(1)", - "docComment": "/**\n * Despawns the entity and all children from the world.\n *\n * Use for: removing entities from the world. Do NOT use for: temporary hiding; consider visibility or animations instead.\n *\n * @remarks\n *\n * **Cascading:** Recursively despawns all child entities first (depth-first).\n *\n * **Controller:** Calls `controller.detach()` then `controller.despawn()` if attached.\n *\n * **Cleanup:** Automatically unregisters attached audios, despawns attached particle emitters, and unloads attached scene UIs from their respective managers.\n *\n * **Simulation:** Removes from physics simulation.\n *\n * **Side effects:** Emits `EntityEvent.DESPAWN` and unregisters from world managers.\n *\n * **Category:** Entities\n */\n", + "kind": "Property", + "canonicalReference": "server!DefaultPlayerEntityController#isOnPlatform:member", + "docComment": "/**\n * Whether the entity is on a platform.\n *\n * @remarks\n *\n * A platform is any entity with a kinematic rigid body.\n *\n * **Category:** Controllers\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "despawn(): " + "text": "get isOnPlatform(): " }, { "kind": "Content", - "text": "void" + "text": "boolean" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "isOnPlatform", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", + "isStatic": false, "isProtected": false, - "overloadIndex": 1, - "parameters": [], - "isOptional": false, - "isAbstract": false, - "name": "despawn" + "isAbstract": false }, { "kind": "Property", - "canonicalReference": "server!Entity#emissiveColor:member", - "docComment": "/**\n * The emissive color of the entity.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!DefaultPlayerEntityController#isSwimming:member", + "docComment": "/**\n * Whether the entity is swimming.\n *\n * @remarks\n *\n * Determined by whether the entity is in contact with a liquid block.\n *\n * **Category:** Controllers\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get emissiveColor(): " - }, - { - "kind": "Reference", - "text": "RgbColor", - "canonicalReference": "server!RgbColor:interface" + "text": "get isSwimming(): " }, { "kind": "Content", - "text": " | undefined" + "text": "boolean" }, { "kind": "Content", @@ -15273,10 +15268,10 @@ "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "emissiveColor", + "name": "isSwimming", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 3 + "endIndex": 2 }, "isStatic": false, "isProtected": false, @@ -15284,26 +15279,26 @@ }, { "kind": "Property", - "canonicalReference": "server!Entity#emissiveIntensity:member", - "docComment": "/**\n * The emissive intensity of the entity.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!DefaultPlayerEntityController#jumpLandHeavyOneshotAnimations:member", + "docComment": "/**\n * The oneshot animation(s) that will play when the entity lands with a high velocity.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get emissiveIntensity(): " + "text": "jumpLandHeavyOneshotAnimations: " }, { "kind": "Content", - "text": "number | undefined" + "text": "string[]" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, + "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "emissiveIntensity", + "name": "jumpLandHeavyOneshotAnimations", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -15313,79 +15308,108 @@ "isAbstract": false }, { - "kind": "Method", - "canonicalReference": "server!Entity#getModelAnimation:member(1)", - "docComment": "/**\n * Gets or lazily creates a model animation for the entity's model by name.\n *\n * @remarks\n *\n * Model entities only; returns `undefined` for block entities. If the animation does not yet exist, a new instance with default settings is created and added to `modelAnimations`. Use `availableModelAnimationNames` to discover which animation names exist in the model.\n *\n * @param name - The name of the animation to get or create.\n *\n * @returns The model animation instance, or `undefined` for block entities.\n *\n * **Category:** Entities\n */\n", + "kind": "Property", + "canonicalReference": "server!DefaultPlayerEntityController#jumpLandLightOneshotAnimations:member", + "docComment": "/**\n * The oneshot animation(s) that will play when the entity lands after jumping or being airborne.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "getModelAnimation(name: " + "text": "jumpLandLightOneshotAnimations: " }, { "kind": "Content", - "text": "string" + "text": "string[]" }, { "kind": "Content", - "text": "): " - }, + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "jumpLandLightOneshotAnimations", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Property", + "canonicalReference": "server!DefaultPlayerEntityController#jumpOneshotAnimations:member", + "docComment": "/**\n * The oneshot animation(s) that will play when the entity is jumping.\n */\n", + "excerptTokens": [ { - "kind": "Reference", - "text": "EntityModelAnimation", - "canonicalReference": "server!EntityModelAnimation:class" + "kind": "Content", + "text": "jumpOneshotAnimations: " }, { "kind": "Content", - "text": " | undefined" + "text": "string[]" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 5 - }, + "isReadonly": false, + "isOptional": false, "releaseTag": "Public", + "name": "jumpOneshotAnimations", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "name", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "getModelAnimation" + "isAbstract": false }, { - "kind": "Method", - "canonicalReference": "server!Entity#getModelNodeOverride:member(1)", - "docComment": "/**\n * Gets or lazily creates a model node override for the entity's model.\n *\n * @remarks\n *\n * Model entities only; returns `undefined` for block entities. If the override does not yet exist, a new instance with default settings is created and added to `modelNodeOverrides`. Use `availableModelNodeNames` to discover which node names exist in the model.\n *\n * @param nameMatch - The node selector for the model node override to get or create. Case-insensitive exact match by default, with optional edge wildcard (`head*`, `*head`, `*head*`).\n *\n * @returns The model node override instance, or `undefined` for block entities.\n *\n * **Category:** Entities\n */\n", + "kind": "Property", + "canonicalReference": "server!DefaultPlayerEntityController#jumpVelocity:member", + "docComment": "/**\n * The upward velocity applied to the entity when it jumps.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "getModelNodeOverride(nameMatch: " + "text": "jumpVelocity: " }, { "kind": "Content", - "text": "string" + "text": "number" }, { "kind": "Content", - "text": "): " + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "jumpVelocity", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Property", + "canonicalReference": "server!DefaultPlayerEntityController#platform:member", + "docComment": "/**\n * The platform the entity is on, if any.\n *\n * **Category:** Controllers\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "get platform(): " }, { "kind": "Reference", - "text": "EntityModelNodeOverride", - "canonicalReference": "server!EntityModelNodeOverride:class" + "text": "Entity", + "canonicalReference": "server!Entity:class" }, { "kind": "Content", @@ -15396,50 +15420,40 @@ "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 5 - }, + "isReadonly": true, + "isOptional": false, "releaseTag": "Public", + "name": "platform", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + }, + "isStatic": false, "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "nameMatch", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "getModelNodeOverride" + "isAbstract": false }, { "kind": "Property", - "canonicalReference": "server!Entity#height:member", - "docComment": "/**\n * The height (Y-axis) of the entity's model or block size.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!DefaultPlayerEntityController#runLoopedAnimations:member", + "docComment": "/**\n * The looped animation(s) that will play when the entity is running.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get height(): " + "text": "runLoopedAnimations: " }, { "kind": "Content", - "text": "number" + "text": "string[]" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, + "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "height", + "name": "runLoopedAnimations", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -15450,26 +15464,26 @@ }, { "kind": "Property", - "canonicalReference": "server!Entity#id:member", - "docComment": "/**\n * The unique identifier for the entity.\n *\n * @remarks\n *\n * Assigned when the entity is spawned.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!DefaultPlayerEntityController#runVelocity:member", + "docComment": "/**\n * The normalized horizontal velocity applied to the entity when it runs.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get id(): " + "text": "runVelocity: " }, { "kind": "Content", - "text": "number | undefined" + "text": "number" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, + "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "id", + "name": "runVelocity", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -15480,26 +15494,17 @@ }, { "kind": "Method", - "canonicalReference": "server!Entity#interact:member(1)", - "docComment": "/**\n * Triggers an interaction on the entity from a player.\n *\n * Use for: programmatic interactions that should mimic a player click/tap. Do NOT use for: server-only effects without player context.\n *\n * @remarks\n *\n * This is automatically called when a player clicks or taps the entity, but can also be called directly for programmatic interactions. Emits `EntityEvent.INTERACT`.\n *\n * @param player - The player interacting with the entity.\n *\n * @param raycastHit - The raycast hit result, if the interaction was triggered by a client-side click/tap.\n *\n * **Requires:** Entity must be spawned.\n *\n * **Side effects:** Emits `EntityEvent.INTERACT`.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!DefaultPlayerEntityController#spawn:member(1)", + "docComment": "/**\n * Called when the controlled entity is spawned. In DefaultPlayerEntityController, this function is used to create the colliders for the entity for wall and ground detection.\n *\n * @remarks\n *\n * **Creates colliders:** Adds two child colliders to the entity: - `groundSensor`: Cylinder sensor below entity for ground/platform detection and landing animations - `wallCollider`: Capsule collider for wall collision with zero friction\n *\n * **Collider sizes scale:** Collider dimensions scale proportionally with `entity.height`.\n *\n * @param entity - The entity that is spawned.\n *\n * **Category:** Controllers\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "interact(player: " - }, - { - "kind": "Reference", - "text": "Player", - "canonicalReference": "server!Player:class" - }, - { - "kind": "Content", - "text": ", raycastHit?: " + "text": "spawn(entity: " }, { "kind": "Reference", - "text": "RaycastHit", - "canonicalReference": "server!RaycastHit:type" + "text": "Entity", + "canonicalReference": "server!Entity:class" }, { "kind": "Content", @@ -15516,42 +15521,34 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 5, - "endIndex": 6 + "startIndex": 3, + "endIndex": 4 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "player", + "parameterName": "entity", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, "isOptional": false - }, - { - "parameterName": "raycastHit", - "parameterTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, - "isOptional": true } ], "isOptional": false, "isAbstract": false, - "name": "interact" + "name": "spawn" }, { "kind": "Property", - "canonicalReference": "server!Entity#isBlockEntity:member", - "docComment": "/**\n * Whether this entity is a block entity.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!DefaultPlayerEntityController#sticksToPlatforms:member", + "docComment": "/**\n * Whether the entity sticks to platforms.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get isBlockEntity(): " + "text": "sticksToPlatforms: " }, { "kind": "Content", @@ -15562,10 +15559,10 @@ "text": ";" } ], - "isReadonly": true, + "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "isBlockEntity", + "name": "sticksToPlatforms", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -15576,26 +15573,26 @@ }, { "kind": "Property", - "canonicalReference": "server!Entity#isEnvironmental:member", - "docComment": "/**\n * Whether the entity is environmental.\n *\n * @remarks\n *\n * Environmental entities are excluded from per-tick controller updates and update emission.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!DefaultPlayerEntityController#swimFastVelocity:member", + "docComment": "/**\n * The normalized horizontal velocity applied to the entity when it swims fast (equivalent to running).\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get isEnvironmental(): " + "text": "swimFastVelocity: " }, { "kind": "Content", - "text": "boolean" + "text": "number" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, + "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "isEnvironmental", + "name": "swimFastVelocity", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -15606,26 +15603,26 @@ }, { "kind": "Property", - "canonicalReference": "server!Entity#isModelEntity:member", - "docComment": "/**\n * Whether this entity is a model entity.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!DefaultPlayerEntityController#swimGravity:member", + "docComment": "/**\n * The gravity modifier applied to the entity when swimming.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get isModelEntity(): " + "text": "swimGravity: " }, { "kind": "Content", - "text": "boolean" + "text": "number" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, + "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "isModelEntity", + "name": "swimGravity", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -15636,26 +15633,26 @@ }, { "kind": "Property", - "canonicalReference": "server!Entity#isSpawned:member", - "docComment": "/**\n * Whether the entity is spawned in a world.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!DefaultPlayerEntityController#swimIdleLoopedAnimations:member", + "docComment": "/**\n * The looped animation(s) that will play when the entity is not moving while swimming.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get isSpawned(): " + "text": "swimIdleLoopedAnimations: " }, { "kind": "Content", - "text": "boolean" + "text": "string[]" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, + "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "isSpawned", + "name": "swimIdleLoopedAnimations", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -15666,43 +15663,29 @@ }, { "kind": "Property", - "canonicalReference": "server!Entity#modelAnimations:member", - "docComment": "/**\n * The animations of the entity's model that have been accessed or configured.\n *\n * @remarks\n *\n * Animations are lazily created on first access via `getModelAnimation()`. This array only contains animations that have been explicitly used, not every clip in the model.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!DefaultPlayerEntityController#swimLoopedAnimations:member", + "docComment": "/**\n * The looped animation(s) that will play when the entity is swimming in any direction.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get modelAnimations(): " - }, - { - "kind": "Reference", - "text": "Readonly", - "canonicalReference": "!Readonly:type" - }, - { - "kind": "Content", - "text": "<" - }, - { - "kind": "Reference", - "text": "EntityModelAnimation", - "canonicalReference": "server!EntityModelAnimation:class" + "text": "swimLoopedAnimations: " }, { "kind": "Content", - "text": "[]>" + "text": "string[]" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, + "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "modelAnimations", + "name": "swimLoopedAnimations", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 5 + "endIndex": 2 }, "isStatic": false, "isProtected": false, @@ -15710,43 +15693,29 @@ }, { "kind": "Property", - "canonicalReference": "server!Entity#modelNodeOverrides:member", - "docComment": "/**\n * The node overrides of the entity's model that have been accessed or configured.\n *\n * @remarks\n *\n * Node overrides are lazily created on first access via `getModelNodeOverride()`. This array only contains overrides that have been explicitly used.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!DefaultPlayerEntityController#swimMaxGravityVelocity:member", + "docComment": "/**\n * The maximum downward velocity that the entity can reach when affected by gravity while swimming.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get modelNodeOverrides(): " - }, - { - "kind": "Reference", - "text": "Readonly", - "canonicalReference": "!Readonly:type" - }, - { - "kind": "Content", - "text": "<" - }, - { - "kind": "Reference", - "text": "EntityModelNodeOverride", - "canonicalReference": "server!EntityModelNodeOverride:class" + "text": "swimMaxGravityVelocity: " }, { "kind": "Content", - "text": "[]>" + "text": "number" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, + "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "modelNodeOverrides", + "name": "swimMaxGravityVelocity", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 5 + "endIndex": 2 }, "isStatic": false, "isProtected": false, @@ -15754,34 +15723,29 @@ }, { "kind": "Property", - "canonicalReference": "server!Entity#modelPreferredShape:member", - "docComment": "/**\n * The preferred collider shape when auto-generating colliders from the model.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!DefaultPlayerEntityController#swimSlowVelocity:member", + "docComment": "/**\n * The normalized horizontal velocity applied to the entity when it swims slowly (equivalent to walking).\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get modelPreferredShape(): " - }, - { - "kind": "Reference", - "text": "ColliderShape", - "canonicalReference": "server!ColliderShape:enum" + "text": "swimSlowVelocity: " }, { "kind": "Content", - "text": " | undefined" + "text": "number" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, + "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "modelPreferredShape", + "name": "swimSlowVelocity", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 3 + "endIndex": 2 }, "isStatic": false, "isProtected": false, @@ -15789,27 +15753,26 @@ }, { "kind": "Property", - "canonicalReference": "server!Entity#modelScale:member", - "docComment": "/**\n * The scale of the entity's model.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!DefaultPlayerEntityController#swimUpwardVelocity:member", + "docComment": "/**\n * The upward velocity applied to the entity when swimming.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get modelScale(): " + "text": "swimUpwardVelocity: " }, { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" + "kind": "Content", + "text": "number" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, + "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "modelScale", + "name": "swimUpwardVelocity", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -15819,117 +15782,126 @@ "isAbstract": false }, { - "kind": "Property", - "canonicalReference": "server!Entity#modelScaleInterpolationMs:member", - "docComment": "/**\n * The interpolation time in milliseconds applied to model scale changes.\n *\n * **Category:** Entities\n */\n", + "kind": "Method", + "canonicalReference": "server!DefaultPlayerEntityController#tickWithPlayerInput:member(1)", + "docComment": "/**\n * Ticks the player movement for the entity controller, overriding the default implementation. If the entity to tick is a child entity, only the event will be emitted but the default movement logic will not be applied.\n *\n * @remarks\n *\n * **Rotation (-Z forward):** Sets entity rotation based on camera yaw. A yaw of 0 faces -Z. Movement direction offsets (WASD/joystick) are added to camera yaw to determine facing. Models must be authored with their front facing -Z.\n *\n * **Child entities:** If `entity.parent` is set, only emits the event and returns early. Movement logic is skipped for child entities.\n *\n * **Input cancellation:** If `autoCancelMouseLeftClick` is true (default), `input.ml` is set to `false` after processing to prevent repeated triggers.\n *\n * **Animations:** Automatically manages idle, walk, run, jump, swim, and interact animations based on movement state and input.\n *\n * @param entity - The entity to tick.\n *\n * @param input - The current input state of the player.\n *\n * @param cameraOrientation - The current camera orientation state of the player.\n *\n * @param deltaTimeMs - The delta time in milliseconds since the last tick.\n *\n * **Category:** Controllers\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get modelScaleInterpolationMs(): " + "text": "tickWithPlayerInput(entity: " }, { - "kind": "Content", - "text": "number | undefined" + "kind": "Reference", + "text": "PlayerEntity", + "canonicalReference": "server!PlayerEntity:class" }, { "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "modelScaleInterpolationMs", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "server!Entity#modelTextureUri:member", - "docComment": "/**\n * The texture URI that overrides the model entity's default texture.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ + "text": ", input: " + }, + { + "kind": "Reference", + "text": "PlayerInput", + "canonicalReference": "server!PlayerInput:type" + }, { "kind": "Content", - "text": "get modelTextureUri(): " + "text": ", cameraOrientation: " + }, + { + "kind": "Reference", + "text": "PlayerCameraOrientation", + "canonicalReference": "server!PlayerCameraOrientation:type" }, { "kind": "Content", - "text": "string | undefined" + "text": ", deltaTimeMs: " }, { "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "modelTextureUri", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "server!Entity#modelUri:member", - "docComment": "/**\n * The URI or path to the `.gltf` model asset.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ + "text": "number" + }, { "kind": "Content", - "text": "get modelUri(): " + "text": "): " }, { "kind": "Content", - "text": "string | undefined" + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "modelUri", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 9, + "endIndex": 10 + }, + "releaseTag": "Public", "isProtected": false, - "isAbstract": false + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "entity", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "input", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": false + }, + { + "parameterName": "cameraOrientation", + "parameterTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "isOptional": false + }, + { + "parameterName": "deltaTimeMs", + "parameterTypeTokenRange": { + "startIndex": 7, + "endIndex": 8 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "tickWithPlayerInput" }, { "kind": "Property", - "canonicalReference": "server!Entity#name:member", - "docComment": "/**\n * The name of the entity.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!DefaultPlayerEntityController#walkLoopedAnimations:member", + "docComment": "/**\n * The looped animation(s) that will play when the entity is walking.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get name(): " + "text": "walkLoopedAnimations: " }, { "kind": "Content", - "text": "string" + "text": "string[]" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, + "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "name", + "name": "walkLoopedAnimations", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -15940,12 +15912,12 @@ }, { "kind": "Property", - "canonicalReference": "server!Entity#opacity:member", - "docComment": "/**\n * The opacity of the entity between 0 and 1.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!DefaultPlayerEntityController#walkVelocity:member", + "docComment": "/**\n * The normalized horizontal velocity applied to the entity when it walks.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get opacity(): " + "text": "walkVelocity: " }, { "kind": "Content", @@ -15956,10 +15928,10 @@ "text": ";" } ], - "isReadonly": true, + "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "opacity", + "name": "walkVelocity", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -15967,1201 +15939,943 @@ "isStatic": false, "isProtected": false, "isAbstract": false - }, + } + ], + "extendsTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "implementsTokenRanges": [] + }, + { + "kind": "Interface", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions:interface", + "docComment": "/**\n * Options for creating a DefaultPlayerEntityController instance.\n *\n * Use for: configuring default player movement and animation behavior at construction time. Do NOT use for: per-frame changes; override methods or adjust controller state instead.\n *\n * **Category:** Controllers\n *\n * @public\n */\n", + "excerptTokens": [ { - "kind": "Property", - "canonicalReference": "server!Entity#outline:member", - "docComment": "/**\n * The outline rendering options for the entity.\n *\n * **Category:** Entities\n */\n", + "kind": "Content", + "text": "export interface DefaultPlayerEntityControllerOptions " + } + ], + "fileUrlPath": "src/worlds/entities/controllers/DefaultPlayerEntityController.ts", + "releaseTag": "Public", + "name": "DefaultPlayerEntityControllerOptions", + "preserveMemberOrder": false, + "members": [ + { + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#applyDirectionalMovementRotations:member", + "docComment": "/**\n * Whether to apply directional rotations to the entity while moving, defaults to true.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get outline(): " - }, - { - "kind": "Reference", - "text": "Outline", - "canonicalReference": "server!Outline:interface" + "text": "applyDirectionalMovementRotations?: " }, { "kind": "Content", - "text": " | undefined" + "text": "boolean" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, - "isOptional": false, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "name": "outline", + "name": "applyDirectionalMovementRotations", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 3 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "endIndex": 2 + } }, { - "kind": "Property", - "canonicalReference": "server!Entity#parent:member", - "docComment": "/**\n * The parent entity, if attached.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#autoCancelMouseLeftClick:member", + "docComment": "/**\n * Whether to automatically cancel left click input after first processed tick, defaults to true.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get parent(): " - }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": "autoCancelMouseLeftClick?: " }, { "kind": "Content", - "text": " | undefined" + "text": "boolean" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, - "isOptional": false, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "name": "parent", + "name": "autoCancelMouseLeftClick", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 3 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "endIndex": 2 + } }, { - "kind": "Property", - "canonicalReference": "server!Entity#parentNodeName:member", - "docComment": "/**\n * The parent model node name, if attached.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#canJump:member", + "docComment": "/**\n * A function allowing custom logic to determine if the entity can jump.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get parentNodeName(): " + "text": "canJump?: " }, { "kind": "Content", - "text": "string | undefined" + "text": "() => boolean" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, - "isOptional": false, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "name": "parentNodeName", + "name": "canJump", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + } }, { - "kind": "Property", - "canonicalReference": "server!Entity#positionInterpolationMs:member", - "docComment": "/**\n * The interpolation time in milliseconds applied to position changes.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#canRun:member", + "docComment": "/**\n * A function allowing custom logic to determine if the entity can run.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get positionInterpolationMs(): " + "text": "canRun?: " }, { "kind": "Content", - "text": "number | undefined" + "text": "() => boolean" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, - "isOptional": false, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "name": "positionInterpolationMs", + "name": "canRun", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + } }, { - "kind": "Method", - "canonicalReference": "server!Entity#removeModelNodeOverride:member(1)", - "docComment": "/**\n * Removes a model node override from the entity's model.\n *\n * @param nameMatch - The name match of the model node override to remove.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#canSwim:member", + "docComment": "/**\n * A function allowing custom logic to determine if the entity can swim.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "removeModelNodeOverride(nameMatch: " - }, - { - "kind": "Content", - "text": "string" - }, - { - "kind": "Content", - "text": "): " + "text": "canSwim?: " }, { "kind": "Content", - "text": "void" + "text": "() => boolean" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "nameMatch", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "removeModelNodeOverride" + "name": "canSwim", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, { - "kind": "Method", - "canonicalReference": "server!Entity#removeModelNodeOverrides:member(1)", - "docComment": "/**\n * Removes multiple model node overrides from the entity's model.\n *\n * @param nameMatches - The name matches of the model node overrides to remove.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#canWalk:member", + "docComment": "/**\n * A function allowing custom logic to determine if the entity can walk.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "removeModelNodeOverrides(nameMatches: " - }, - { - "kind": "Content", - "text": "string[]" - }, - { - "kind": "Content", - "text": "): " + "text": "canWalk?: " }, { "kind": "Content", - "text": "void" + "text": "() => boolean" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "nameMatches", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "removeModelNodeOverrides" + "name": "canWalk", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, { - "kind": "Property", - "canonicalReference": "server!Entity#rotationInterpolationMs:member", - "docComment": "/**\n * The interpolation time in milliseconds applied to rotation changes.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#facesCameraWhenIdle:member", + "docComment": "/**\n * Whether the entity rotates to face the camera direction when idle.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get rotationInterpolationMs(): " + "text": "facesCameraWhenIdle?: " }, { "kind": "Content", - "text": "number | undefined" + "text": "boolean" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, - "isOptional": false, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "name": "rotationInterpolationMs", + "name": "facesCameraWhenIdle", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + } }, { - "kind": "Method", - "canonicalReference": "server!Entity#setBlockTextureUri:member(1)", - "docComment": "", + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#idleLoopedAnimations:member", + "docComment": "/**\n * Overrides the animation(s) that will play when the entity is idle.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setBlockTextureUri(blockTextureUri: " - }, - { - "kind": "Content", - "text": "string | undefined" - }, - { - "kind": "Content", - "text": "): " + "text": "idleLoopedAnimations?: " }, { "kind": "Content", - "text": "void" + "text": "string[]" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "blockTextureUri", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "setBlockTextureUri" + "name": "idleLoopedAnimations", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, { - "kind": "Method", - "canonicalReference": "server!Entity#setEmissiveColor:member(1)", - "docComment": "/**\n * Sets the emissive color of the entity.\n *\n * Use for: glow effects or highlighted states.\n *\n * @param emissiveColor - The emissive color of the entity.\n *\n * **Side effects:** Emits `EntityEvent.SET_EMISSIVE_COLOR` when spawned.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#interactOneshotAnimations:member", + "docComment": "/**\n * Overrides the animation(s) that will play when the entity interacts (left click)\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setEmissiveColor(emissiveColor: " + "text": "interactOneshotAnimations?: " }, { - "kind": "Reference", - "text": "RgbColor", - "canonicalReference": "server!RgbColor:interface" + "kind": "Content", + "text": "string[]" }, { "kind": "Content", - "text": " | undefined" - }, + "text": ";" + } + ], + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "interactOneshotAnimations", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, + { + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#jumpLandHeavyOneshotAnimations:member", + "docComment": "/**\n * Overrides the animation(s) that will play when the entity lands with a high velocity.\n */\n", + "excerptTokens": [ { "kind": "Content", - "text": "): " + "text": "jumpLandHeavyOneshotAnimations?: " }, { "kind": "Content", - "text": "void" + "text": "string[]" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 4, - "endIndex": 5 - }, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "emissiveColor", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "setEmissiveColor" + "name": "jumpLandHeavyOneshotAnimations", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, { - "kind": "Method", - "canonicalReference": "server!Entity#setEmissiveIntensity:member(1)", - "docComment": "/**\n * Sets the emissive intensity of the entity.\n *\n * @param emissiveIntensity - The emissive intensity of the entity. Use a value over 1 for brighter emissive effects.\n *\n * **Side effects:** Emits `EntityEvent.SET_EMISSIVE_INTENSITY` when spawned.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#jumpLandLightOneshotAnimations:member", + "docComment": "/**\n * Overrides the animation(s) that will play when the entity lands after jumping or being airborne.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setEmissiveIntensity(emissiveIntensity: " + "text": "jumpLandLightOneshotAnimations?: " }, { "kind": "Content", - "text": "number | undefined" + "text": "string[]" }, { "kind": "Content", - "text": "): " + "text": ";" + } + ], + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "jumpLandLightOneshotAnimations", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, + { + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#jumpOneshotAnimations:member", + "docComment": "/**\n * Overrides the animation(s) that will play when the entity is jumping.\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "jumpOneshotAnimations?: " }, { "kind": "Content", - "text": "void" + "text": "string[]" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "emissiveIntensity", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "setEmissiveIntensity" + "name": "jumpOneshotAnimations", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, { - "kind": "Method", - "canonicalReference": "server!Entity#setModelScale:member(1)", - "docComment": "/**\n * Sets the scale of the entity's model and proportionally scales its colliders.\n *\n * @remarks\n *\n * Model entities only; no effect for block entities.\n *\n * **Collider scaling is relative:** Colliders are scaled by the ratio of new/old scale, not set to absolute values. Example: scaling from 1 to 2 doubles collider size; scaling from 2 to 4 also doubles it.\n *\n * **Reference equality check:** Uses `===` to compare with current scale, so passing the same object reference will early return even if values changed. Always pass a new object.\n *\n * @param modelScale - The scale of the entity's model. Can be a vector or a number for uniform scaling.\n *\n * **Side effects:** Scales existing colliders and emits `EntityEvent.SET_MODEL_SCALE` when spawned.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#jumpVelocity:member", + "docComment": "/**\n * The upward velocity applied to the entity when it jumps.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setModelScale(modelScale: " + "text": "jumpVelocity?: " }, { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" + "kind": "Content", + "text": "number" }, { "kind": "Content", - "text": " | number" - }, + "text": ";" + } + ], + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "jumpVelocity", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, + { + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#runLoopedAnimations:member", + "docComment": "/**\n * Overrides the animation(s) that will play when the entity is running.\n */\n", + "excerptTokens": [ { "kind": "Content", - "text": "): " + "text": "runLoopedAnimations?: " }, { "kind": "Content", - "text": "void" + "text": "string[]" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 4, - "endIndex": 5 - }, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "modelScale", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "setModelScale" + "name": "runLoopedAnimations", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, { - "kind": "Method", - "canonicalReference": "server!Entity#setModelScaleInterpolationMs:member(1)", - "docComment": "/**\n * Sets the interpolation time in milliseconds applied to model scale changes.\n *\n * @param interpolationMs - The interpolation time in milliseconds to set.\n *\n * **Side effects:** Emits `EntityEvent.SET_MODEL_SCALE_INTERPOLATION_MS` when spawned.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#runVelocity:member", + "docComment": "/**\n * The normalized horizontal velocity applied to the entity when it runs.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setModelScaleInterpolationMs(interpolationMs: " - }, - { - "kind": "Content", - "text": "number | undefined" - }, - { - "kind": "Content", - "text": "): " + "text": "runVelocity?: " }, { "kind": "Content", - "text": "void" + "text": "number" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "interpolationMs", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "setModelScaleInterpolationMs" + "name": "runVelocity", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, { - "kind": "Method", - "canonicalReference": "server!Entity#setModelTextureUri:member(1)", - "docComment": "/**\n * Sets the texture uri of the entity's model. Setting this overrides the model's default texture.\n *\n * @remarks\n *\n * Model entities only; no effect for block entities.\n *\n * @param modelTextureUri - The texture uri of the entity's model.\n *\n * **Side effects:** Emits `EntityEvent.SET_MODEL_TEXTURE_URI` when spawned.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#sticksToPlatforms:member", + "docComment": "/**\n * Whether the entity sticks to platforms, defaults to true.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setModelTextureUri(modelTextureUri: " - }, - { - "kind": "Content", - "text": "string | undefined" - }, - { - "kind": "Content", - "text": "): " + "text": "sticksToPlatforms?: " }, { "kind": "Content", - "text": "void" + "text": "boolean" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "modelTextureUri", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "setModelTextureUri" + "name": "sticksToPlatforms", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, { - "kind": "Method", - "canonicalReference": "server!Entity#setOpacity:member(1)", - "docComment": "/**\n * Sets the opacity of the entity.\n *\n * @param opacity - The opacity of the entity between 0 and 1. 0 is fully transparent, 1 is fully opaque.\n *\n * **Side effects:** Emits `EntityEvent.SET_OPACITY` when spawned.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#swimFastVelocity:member", + "docComment": "/**\n * The normalized horizontal velocity applied to the entity when it swims fast (equivalent to running).\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setOpacity(opacity: " + "text": "swimFastVelocity?: " }, { "kind": "Content", "text": "number" }, - { - "kind": "Content", - "text": "): " - }, - { - "kind": "Content", - "text": "void" - }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "opacity", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "setOpacity" + "name": "swimFastVelocity", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, { - "kind": "Method", - "canonicalReference": "server!Entity#setOutline:member(1)", - "docComment": "/**\n * Sets the outline rendering options for the entity.\n *\n * @param outline - The outline options, or undefined to remove the outline.\n *\n * @param forPlayer - The player to set the outline for, if undefined the outline will be set for all players.\n *\n * **Side effects:** Emits `EntityEvent.SET_OUTLINE` when spawned.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#swimGravity:member", + "docComment": "/**\n * The gravity modifier applied to the entity when swimming.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setOutline(outline: " - }, - { - "kind": "Reference", - "text": "Outline", - "canonicalReference": "server!Outline:interface" - }, - { - "kind": "Content", - "text": " | undefined" - }, - { - "kind": "Content", - "text": ", forPlayer?: " - }, - { - "kind": "Reference", - "text": "Player", - "canonicalReference": "server!Player:class" - }, - { - "kind": "Content", - "text": "): " + "text": "swimGravity?: " }, { "kind": "Content", - "text": "void" + "text": "number" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 6, - "endIndex": 7 - }, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "outline", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - }, - "isOptional": false - }, - { - "parameterName": "forPlayer", - "parameterTypeTokenRange": { - "startIndex": 4, - "endIndex": 5 - }, - "isOptional": true - } - ], - "isOptional": false, - "isAbstract": false, - "name": "setOutline" + "name": "swimGravity", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, { - "kind": "Method", - "canonicalReference": "server!Entity#setParent:member(1)", - "docComment": "/**\n * Sets the parent of the entity and resets this entity's position and rotation.\n *\n * @remarks\n *\n * When setting the parent, all forces, torques and velocities of this entity are reset. Additionally, this entity's type will be set to `KINEMATIC_VELOCITY` if it is not already. All colliders of this entity will be disabled when parent is not undefined. If the provided parent is undefined, this entity will be removed from its parent and all colliders will be re-enabled. When setting an undefined parent to remove this entity from its parent, this entity's type will be set to the last type it was set to before being a child.\n *\n * @param parent - The parent entity to set, or undefined to remove from an existing parent.\n *\n * @param parentNodeName - The name of the parent's node (if parent is a model entity) this entity will attach to.\n *\n * @param position - The position to set for the entity. If parent is provided, this is relative to the parent's attachment point.\n *\n * @param rotation - The rotation to set for the entity. If parent is provided, this is relative to the parent's rotation.\n *\n * **Requires:** If `parent` is provided, it must be spawned.\n *\n * **Side effects:** Disables/enables colliders, changes rigid body type, and emits `EntityEvent.SET_PARENT`.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#swimIdleLoopedAnimations:member", + "docComment": "/**\n * The looped animation(s) that will play when the entity is not moving while swimming.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setParent(parent: " - }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": "swimIdleLoopedAnimations?: " }, { "kind": "Content", - "text": " | undefined" + "text": "string[]" }, { "kind": "Content", - "text": ", parentNodeName?: " - }, + "text": ";" + } + ], + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "swimIdleLoopedAnimations", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, + { + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#swimLoopedAnimations:member", + "docComment": "/**\n * The looped animation(s) that will play when the entity is swimming in any direction.\n */\n", + "excerptTokens": [ { "kind": "Content", - "text": "string" + "text": "swimLoopedAnimations?: " }, { "kind": "Content", - "text": ", position?: " - }, - { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" + "text": "string[]" }, { "kind": "Content", - "text": ", rotation?: " - }, - { - "kind": "Reference", - "text": "QuaternionLike", - "canonicalReference": "server!QuaternionLike:interface" - }, + "text": ";" + } + ], + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "swimLoopedAnimations", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, + { + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#swimMaxGravityVelocity:member", + "docComment": "/**\n * The maximum downward velocity that the entity can reach when affected by gravity while swimming.\n */\n", + "excerptTokens": [ { "kind": "Content", - "text": "): " + "text": "swimMaxGravityVelocity?: " }, { "kind": "Content", - "text": "void" + "text": "number" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 10, - "endIndex": 11 - }, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "parent", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - }, - "isOptional": false - }, - { - "parameterName": "parentNodeName", - "parameterTypeTokenRange": { - "startIndex": 4, - "endIndex": 5 - }, - "isOptional": true - }, - { - "parameterName": "position", - "parameterTypeTokenRange": { - "startIndex": 6, - "endIndex": 7 - }, - "isOptional": true - }, - { - "parameterName": "rotation", - "parameterTypeTokenRange": { - "startIndex": 8, - "endIndex": 9 - }, - "isOptional": true - } - ], - "isOptional": false, - "isAbstract": false, - "name": "setParent" + "name": "swimMaxGravityVelocity", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, { - "kind": "Method", - "canonicalReference": "server!Entity#setPositionInterpolationMs:member(1)", - "docComment": "/**\n * Sets the interpolation time in milliseconds applied to position changes.\n *\n * @param interpolationMs - The interpolation time in milliseconds to set.\n *\n * **Side effects:** Emits `EntityEvent.SET_POSITION_INTERPOLATION_MS` when spawned.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#swimSlowVelocity:member", + "docComment": "/**\n * The normalized horizontal velocity applied to the entity when it swims slowly (equivalent to walking).\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setPositionInterpolationMs(interpolationMs: " - }, - { - "kind": "Content", - "text": "number | undefined" - }, - { - "kind": "Content", - "text": "): " + "text": "swimSlowVelocity?: " }, { "kind": "Content", - "text": "void" + "text": "number" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "interpolationMs", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "setPositionInterpolationMs" + "name": "swimSlowVelocity", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, { - "kind": "Method", - "canonicalReference": "server!Entity#setRotationInterpolationMs:member(1)", - "docComment": "/**\n * Sets the interpolation time in milliseconds applied to rotation changes.\n *\n * @param interpolationMs - The interpolation time in milliseconds to set.\n *\n * **Side effects:** Emits `EntityEvent.SET_ROTATION_INTERPOLATION_MS` when spawned.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#swimUpwardVelocity:member", + "docComment": "/**\n * The upward velocity applied to the entity when swimming.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setRotationInterpolationMs(interpolationMs: " - }, - { - "kind": "Content", - "text": "number | undefined" - }, - { - "kind": "Content", - "text": "): " + "text": "swimUpwardVelocity?: " }, { "kind": "Content", - "text": "void" + "text": "number" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "interpolationMs", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "setRotationInterpolationMs" + "name": "swimUpwardVelocity", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, { - "kind": "Method", - "canonicalReference": "server!Entity#setTintColor:member(1)", - "docComment": "/**\n * Sets the tint color of the entity.\n *\n * @param tintColor - The tint color of the entity.\n *\n * **Side effects:** Emits `EntityEvent.SET_TINT_COLOR` when spawned.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#walkLoopedAnimations:member", + "docComment": "/**\n * Overrides the animation(s) that will play when the entity is walking.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setTintColor(tintColor: " - }, - { - "kind": "Reference", - "text": "RgbColor", - "canonicalReference": "server!RgbColor:interface" - }, - { - "kind": "Content", - "text": " | undefined" - }, - { - "kind": "Content", - "text": "): " + "text": "walkLoopedAnimations?: " }, { "kind": "Content", - "text": "void" + "text": "string[]" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 4, - "endIndex": 5 - }, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "tintColor", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "setTintColor" + "name": "walkLoopedAnimations", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, { - "kind": "Method", - "canonicalReference": "server!Entity#spawn:member(1)", - "docComment": "/**\n * Spawns the entity in the world.\n *\n * Use for: placing the entity into a world so it simulates and syncs to clients. Do NOT use for: reusing a single entity instance across multiple worlds.\n *\n * @remarks\n *\n * **Rotation default:** If no rotation is provided, entity spawns with identity rotation facing -Z. For Y-axis rotation (yaw): `{ x: 0, y: sin(yaw/2), z: 0, w: cos(yaw/2) }`. Yaw 0 = facing -Z.\n *\n * **Auto-collider creation:** If no colliders are provided, a default collider is auto-generated from the model bounds (or block half extents). Set `modelPreferredShape` to `ColliderShape.NONE` to disable.\n *\n * **Collision groups:** Colliders with default collision groups are auto-assigned based on `isEnvironmental` and `isSensor` flags. Environmental entities don't collide with blocks or other environmental entities.\n *\n * **Event enabling:** Collision/contact force events are auto-enabled on colliders if listeners are registered for `BLOCK_COLLISION`, `ENTITY_COLLISION`, `BLOCK_CONTACT_FORCE`, or `ENTITY_CONTACT_FORCE` prior to spawning.\n *\n * **Controller:** If a controller is attached, `controller.spawn()` is called after the entity is added to the physics simulation.\n *\n * **Parent handling:** If `parent` was set in options, `setParent()` is called after spawn with the provided position/rotation.\n *\n * @param world - The world to spawn the entity in.\n *\n * @param position - The position to spawn the entity at.\n *\n * @param rotation - The optional rotation to spawn the entity with.\n *\n * **Requires:** Entity must not already be spawned.\n *\n * **Side effects:** Registers the entity, adds it to the simulation, and emits `EntityEvent.SPAWN`.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DefaultPlayerEntityControllerOptions#walkVelocity:member", + "docComment": "/**\n * The normalized horizontal velocity applied to the entity when it walks.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "spawn(world: " - }, - { - "kind": "Reference", - "text": "World", - "canonicalReference": "server!World:class" + "text": "walkVelocity?: " }, { "kind": "Content", - "text": ", position: " - }, - { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" + "text": "number" }, { "kind": "Content", - "text": ", rotation?: " - }, - { - "kind": "Reference", - "text": "QuaternionLike", - "canonicalReference": "server!QuaternionLike:interface" - }, + "text": ";" + } + ], + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "walkVelocity", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + } + ], + "extendsTokenRanges": [] + }, + { + "kind": "TypeAlias", + "canonicalReference": "server!DefaultPlayerEntityOptions:type", + "docComment": "/**\n * Options for creating a DefaultPlayerEntity instance.\n *\n * Use for: customizing the default player avatar (for example cosmetic visibility). Do NOT use for: changing movement behavior; use `DefaultPlayerEntityControllerOptions`.\n *\n * **Category:** Entities\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export type DefaultPlayerEntityOptions = " + }, + { + "kind": "Content", + "text": "{\n cosmeticHiddenSlots?: " + }, + { + "kind": "Reference", + "text": "PlayerCosmeticSlot", + "canonicalReference": "server!PlayerCosmeticSlot:type" + }, + { + "kind": "Content", + "text": "[];\n} & " + }, + { + "kind": "Reference", + "text": "PlayerEntityOptions", + "canonicalReference": "server!PlayerEntityOptions:type" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "src/worlds/entities/DefaultPlayerEntity.ts", + "releaseTag": "Public", + "name": "DefaultPlayerEntityOptions", + "typeTokenRange": { + "startIndex": 1, + "endIndex": 5 + } + }, + { + "kind": "Interface", + "canonicalReference": "server!DynamicRigidBodyOptions:interface", + "docComment": "/**\n * The options for a dynamic rigid body, also the default type.\n *\n * Use for: physics-driven bodies affected by forces and collisions. Do NOT use for: kinematic bodies; use the kinematic option types instead.\n *\n * **Category:** Physics\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export interface DynamicRigidBodyOptions extends " + }, + { + "kind": "Reference", + "text": "BaseRigidBodyOptions", + "canonicalReference": "server!BaseRigidBodyOptions:interface" + }, + { + "kind": "Content", + "text": " " + } + ], + "fileUrlPath": "src/worlds/physics/RigidBody.ts", + "releaseTag": "Public", + "name": "DynamicRigidBodyOptions", + "preserveMemberOrder": false, + "members": [ + { + "kind": "PropertySignature", + "canonicalReference": "server!DynamicRigidBodyOptions#additionalMass:member", + "docComment": "/**\n * The additional mass of the rigid body.\n *\n * **Category:** Physics\n */\n", + "excerptTokens": [ { "kind": "Content", - "text": "): " + "text": "additionalMass?: " }, { "kind": "Content", - "text": "void" + "text": "number" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 7, - "endIndex": 8 - }, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "world", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - }, - { - "parameterName": "position", - "parameterTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, - "isOptional": false - }, - { - "parameterName": "rotation", - "parameterTypeTokenRange": { - "startIndex": 5, - "endIndex": 6 - }, - "isOptional": true - } - ], - "isOptional": false, - "isAbstract": false, - "name": "spawn" + "name": "additionalMass", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, { - "kind": "Method", - "canonicalReference": "server!Entity#stopAllModelAnimations:member(1)", - "docComment": "/**\n * Stops all model animations for the entity, optionally excluding the provided animations from stopping.\n *\n * @param exclusionFilter - The filter to determine if a model animation should be excluded from being stopped.\n *\n * **Side effects:** May emit `EntityModelAnimationEvent.STOP` for each stopped animation.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DynamicRigidBodyOptions#additionalMassProperties:member", + "docComment": "/**\n * The additional mass properties of the rigid body.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "stopAllModelAnimations(exclusionFilter?: " - }, - { - "kind": "Content", - "text": "(modelAnimation: " - }, - { - "kind": "Reference", - "text": "Readonly", - "canonicalReference": "!Readonly:type" - }, - { - "kind": "Content", - "text": "<" + "text": "additionalMassProperties?: " }, { "kind": "Reference", - "text": "EntityModelAnimation", - "canonicalReference": "server!EntityModelAnimation:class" + "text": "RigidBodyAdditionalMassProperties", + "canonicalReference": "server!RigidBodyAdditionalMassProperties:type" }, { "kind": "Content", - "text": ">) => boolean" - }, + "text": ";" + } + ], + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "additionalMassProperties", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, + { + "kind": "PropertySignature", + "canonicalReference": "server!DynamicRigidBodyOptions#additionalSolverIterations:member", + "docComment": "/**\n * The additional solver iterations of the rigid body.\n *\n * **Category:** Physics\n */\n", + "excerptTokens": [ { "kind": "Content", - "text": "): " + "text": "additionalSolverIterations?: " }, { "kind": "Content", - "text": "void" + "text": "number" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 7, - "endIndex": 8 - }, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "exclusionFilter", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 6 - }, - "isOptional": true - } - ], - "isOptional": false, - "isAbstract": false, - "name": "stopAllModelAnimations" + "name": "additionalSolverIterations", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, { - "kind": "Method", - "canonicalReference": "server!Entity#stopModelAnimations:member(1)", - "docComment": "/**\n * Stops the provided model animations for the entity.\n *\n * @param modelAnimationNames - The model animation names to stop.\n *\n * **Side effects:** May emit `EntityModelAnimationEvent.STOP` for each stopped animation.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DynamicRigidBodyOptions#angularDamping:member", + "docComment": "/**\n * The angular damping of the rigid body.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "stopModelAnimations(modelAnimationNames: " - }, - { - "kind": "Content", - "text": "readonly string[]" - }, - { - "kind": "Content", - "text": "): " + "text": "angularDamping?: " }, { "kind": "Content", - "text": "void" + "text": "number" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "modelAnimationNames", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "stopModelAnimations" + "name": "angularDamping", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } }, { - "kind": "Property", - "canonicalReference": "server!Entity#tag:member", - "docComment": "/**\n * An arbitrary identifier tag for your own logic.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DynamicRigidBodyOptions#angularVelocity:member", + "docComment": "/**\n * The angular velocity of the rigid body.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get tag(): " + "text": "angularVelocity?: " }, { - "kind": "Content", - "text": "string | undefined" + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, - "isOptional": false, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "name": "tag", + "name": "angularVelocity", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + } }, { - "kind": "Property", - "canonicalReference": "server!Entity#tintColor:member", - "docComment": "/**\n * The tint color of the entity.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DynamicRigidBodyOptions#ccdEnabled:member", + "docComment": "/**\n * Whether the rigid body has continuous collision detection enabled.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get tintColor(): " - }, - { - "kind": "Reference", - "text": "RgbColor", - "canonicalReference": "server!RgbColor:interface" + "text": "ccdEnabled?: " }, { "kind": "Content", - "text": " | undefined" + "text": "boolean" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, - "isOptional": false, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "name": "tintColor", + "name": "ccdEnabled", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 3 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "endIndex": 2 + } }, { - "kind": "Property", - "canonicalReference": "server!Entity#width:member", - "docComment": "/**\n * The width (X-axis) of the entity's model or block size.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DynamicRigidBodyOptions#dominanceGroup:member", + "docComment": "/**\n * The dominance group of the rigid body.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get width(): " + "text": "dominanceGroup?: " }, { "kind": "Content", @@ -17172,952 +16886,1050 @@ "text": ";" } ], - "isReadonly": true, - "isOptional": false, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "name": "width", + "name": "dominanceGroup", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + } }, { - "kind": "Property", - "canonicalReference": "server!Entity#world:member", - "docComment": "/**\n * The world the entity is in, if spawned.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!DynamicRigidBodyOptions#enabledPositions:member", + "docComment": "/**\n * The enabled axes of positional movement of the rigid body.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get world(): " + "text": "enabledPositions?: " }, { "kind": "Reference", - "text": "World", - "canonicalReference": "server!World:class" - }, - { - "kind": "Content", - "text": " | undefined" + "text": "Vector3Boolean", + "canonicalReference": "server!Vector3Boolean:interface" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, - "isOptional": false, + "isReadonly": false, + "isOptional": true, "releaseTag": "Public", - "name": "world", + "name": "enabledPositions", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 3 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - } - ], - "extendsTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "implementsTokenRanges": [ - { - "startIndex": 3, - "endIndex": 4 - } - ] - }, - { - "kind": "Enum", - "canonicalReference": "server!EntityEvent:enum", - "docComment": "/**\n * Event types an Entity instance can emit.\n *\n * See `EntityEventPayloads` for the payloads.\n *\n * **Category:** Events\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export declare enum EntityEvent " - } - ], - "fileUrlPath": "src/worlds/entities/Entity.ts", - "releaseTag": "Public", - "name": "EntityEvent", - "preserveMemberOrder": false, - "members": [ + "endIndex": 2 + } + }, { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.BLOCK_COLLISION:member", - "docComment": "", + "kind": "PropertySignature", + "canonicalReference": "server!DynamicRigidBodyOptions#enabledRotations:member", + "docComment": "/**\n * The enabled rotations of the rigid body.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "BLOCK_COLLISION = " + "text": "enabledRotations?: " + }, + { + "kind": "Reference", + "text": "Vector3Boolean", + "canonicalReference": "server!Vector3Boolean:interface" }, { "kind": "Content", - "text": "\"ENTITY.BLOCK_COLLISION\"" + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "enabledRotations", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 - }, - "releaseTag": "Public", - "name": "BLOCK_COLLISION" + } }, { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.BLOCK_CONTACT_FORCE:member", - "docComment": "", + "kind": "PropertySignature", + "canonicalReference": "server!DynamicRigidBodyOptions#gravityScale:member", + "docComment": "/**\n * The gravity scale of the rigid body.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "BLOCK_CONTACT_FORCE = " + "text": "gravityScale?: " }, { "kind": "Content", - "text": "\"ENTITY.BLOCK_CONTACT_FORCE\"" + "text": "number" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "gravityScale", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 - }, - "releaseTag": "Public", - "name": "BLOCK_CONTACT_FORCE" + } }, { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.DESPAWN:member", - "docComment": "", + "kind": "PropertySignature", + "canonicalReference": "server!DynamicRigidBodyOptions#linearDamping:member", + "docComment": "/**\n * The linear damping of the rigid body.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "DESPAWN = " + "text": "linearDamping?: " }, { "kind": "Content", - "text": "\"ENTITY.DESPAWN\"" + "text": "number" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "linearDamping", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 - }, - "releaseTag": "Public", - "name": "DESPAWN" + } }, { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.ENTITY_COLLISION:member", - "docComment": "", + "kind": "PropertySignature", + "canonicalReference": "server!DynamicRigidBodyOptions#linearVelocity:member", + "docComment": "/**\n * The linear velocity of the rigid body.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "ENTITY_COLLISION = " + "text": "linearVelocity?: " + }, + { + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", - "text": "\"ENTITY.ENTITY_COLLISION\"" + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "linearVelocity", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 - }, - "releaseTag": "Public", - "name": "ENTITY_COLLISION" + } }, { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.ENTITY_CONTACT_FORCE:member", - "docComment": "", + "kind": "PropertySignature", + "canonicalReference": "server!DynamicRigidBodyOptions#sleeping:member", + "docComment": "/**\n * Whether the rigid body is sleeping.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "ENTITY_CONTACT_FORCE = " + "text": "sleeping?: " }, { "kind": "Content", - "text": "\"ENTITY.ENTITY_CONTACT_FORCE\"" + "text": "boolean" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "sleeping", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 - }, - "releaseTag": "Public", - "name": "ENTITY_CONTACT_FORCE" + } }, { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.INTERACT:member", - "docComment": "", + "kind": "PropertySignature", + "canonicalReference": "server!DynamicRigidBodyOptions#softCcdPrediction:member", + "docComment": "/**\n * The soft continuous collision detection prediction of the rigid body.\n *\n * **Category:** Physics\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "INTERACT = " + "text": "softCcdPrediction?: " }, { "kind": "Content", - "text": "\"ENTITY.INTERACT\"" - } - ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "name": "INTERACT" - }, - { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.REMOVE_MODEL_NODE_OVERRIDE:member", - "docComment": "", - "excerptTokens": [ - { - "kind": "Content", - "text": "REMOVE_MODEL_NODE_OVERRIDE = " + "text": "number" }, { "kind": "Content", - "text": "\"ENTITY.REMOVE_MODEL_NODE_OVERRIDE\"" + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "softCcdPrediction", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 - }, - "releaseTag": "Public", - "name": "REMOVE_MODEL_NODE_OVERRIDE" + } }, { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.SET_BLOCK_TEXTURE_URI:member", + "kind": "PropertySignature", + "canonicalReference": "server!DynamicRigidBodyOptions#type:member", "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "SET_BLOCK_TEXTURE_URI = " + "text": "type: " + }, + { + "kind": "Reference", + "text": "RigidBodyType.DYNAMIC", + "canonicalReference": "server!RigidBodyType.DYNAMIC:member" }, { "kind": "Content", - "text": "\"ENTITY.SET_BLOCK_TEXTURE_URI\"" + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "type", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 - }, - "releaseTag": "Public", - "name": "SET_BLOCK_TEXTURE_URI" + } + } + ], + "extendsTokenRanges": [ + { + "startIndex": 1, + "endIndex": 2 + } + ] + }, + { + "kind": "Class", + "canonicalReference": "server!Entity:class", + "docComment": "/**\n * Represents a dynamic or static object in a world.\n *\n * When to use: any non-player object that needs physics, visuals, or interactions. Do NOT use for: player-controlled avatars (use `PlayerEntity` / `DefaultPlayerEntity`). Do NOT use for: voxel blocks (use block APIs on `ChunkLattice`).\n *\n * @remarks\n *\n * Entities are created from a block texture or a `.gltf` model and can have rigid bodies, colliders, animations, and controllers.\n *\n *

Coordinate System

\n *\n * HYTOPIA uses a right-handed coordinate system where: - **+X** is right - **+Y** is up - **-Z** is forward (identity orientation)\n *\n * Models should be authored with their front/forward facing the **-Z axis**. When an entity has identity rotation (0,0,0,1 quaternion or yaw=0), it faces -Z.\n *\n *

Events

\n *\n * This class is an EventRouter, and instances of it emit events with payloads listed under `EntityEventPayloads`.\n *\n * @example\n * ```typescript\n * const spider = new Entity({\n * name: 'Spider',\n * modelUri: 'models/spider.gltf',\n * rigidBodyOptions: {\n * type: RigidBodyType.DYNAMIC,\n * enabledRotations: { x: false, y: true, z: false },\n * colliders: [\n * {\n * shape: ColliderShape.ROUND_CYLINDER,\n * borderRadius: 0.1,\n * halfHeight: 0.225,\n * radius: 0.5,\n * tag: 'body',\n * }\n * ],\n * },\n * });\n *\n * spider.spawn(world, { x: 20, y: 6, z: 10 });\n * ```\n *\n * **Category:** Entities\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export default class Entity extends " }, { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.SET_EMISSIVE_COLOR:member", - "docComment": "", + "kind": "Reference", + "text": "RigidBody", + "canonicalReference": "server!RigidBody:class" + }, + { + "kind": "Content", + "text": " implements " + }, + { + "kind": "Reference", + "text": "protocol.Serializable", + "canonicalReference": "@hytopia.com/server-protocol!Serializable:interface" + }, + { + "kind": "Content", + "text": " " + } + ], + "fileUrlPath": "src/worlds/entities/Entity.ts", + "releaseTag": "Public", + "isAbstract": false, + "name": "Entity", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Constructor", + "canonicalReference": "server!Entity:constructor(1)", + "docComment": "/**\n * Creates a new Entity instance.\n *\n * Use for: defining a new entity before spawning it into a world. Do NOT use for: player-controlled avatars (use `PlayerEntity` or `DefaultPlayerEntity`).\n *\n * @remarks\n *\n * Exactly one of `blockTextureUri` or `modelUri` must be provided. If `controller` is provided, `controller.attach(this)` is called during construction (before spawn).\n *\n * @param options - The options for the entity.\n *\n * **Requires:** If `parent` is provided, it must already be spawned.\n *\n * **Side effects:** May attach the provided controller.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "SET_EMISSIVE_COLOR = " + "text": "constructor(options: " + }, + { + "kind": "Reference", + "text": "EntityOptions", + "canonicalReference": "server!EntityOptions:type" }, { "kind": "Content", - "text": "\"ENTITY.SET_EMISSIVE_COLOR\"" + "text": ");" } ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, "releaseTag": "Public", - "name": "SET_EMISSIVE_COLOR" + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "options", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ] }, { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.SET_EMISSIVE_INTENSITY:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Entity#availableModelAnimationNames:member", + "docComment": "/**\n * The names of the animations available in the entity's model.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "SET_EMISSIVE_INTENSITY = " + "text": "get availableModelAnimationNames(): " }, { - "kind": "Content", - "text": "\"ENTITY.SET_EMISSIVE_INTENSITY\"" - } - ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "name": "SET_EMISSIVE_INTENSITY" - }, - { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.SET_MODEL_SCALE:member", - "docComment": "", - "excerptTokens": [ + "kind": "Reference", + "text": "Readonly", + "canonicalReference": "!Readonly:type" + }, { "kind": "Content", - "text": "SET_MODEL_SCALE = " + "text": "" }, { "kind": "Content", - "text": "\"ENTITY.SET_MODEL_SCALE\"" + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "availableModelAnimationNames", + "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 2 + "endIndex": 3 }, - "releaseTag": "Public", - "name": "SET_MODEL_SCALE" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.SET_MODEL_SCALE_INTERPOLATION_MS:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Entity#availableModelNodeNames:member", + "docComment": "/**\n * The names of the nodes available in the entity's model.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "SET_MODEL_SCALE_INTERPOLATION_MS = " + "text": "get availableModelNodeNames(): " }, { - "kind": "Content", - "text": "\"ENTITY.SET_MODEL_SCALE_INTERPOLATION_MS\"" - } - ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "name": "SET_MODEL_SCALE_INTERPOLATION_MS" - }, - { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.SET_MODEL_TEXTURE_URI:member", - "docComment": "", - "excerptTokens": [ + "kind": "Reference", + "text": "Readonly", + "canonicalReference": "!Readonly:type" + }, { "kind": "Content", - "text": "SET_MODEL_TEXTURE_URI = " + "text": "" }, { "kind": "Content", - "text": "\"ENTITY.SET_MODEL_TEXTURE_URI\"" + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "availableModelNodeNames", + "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 2 + "endIndex": 3 }, - "releaseTag": "Public", - "name": "SET_MODEL_TEXTURE_URI" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.SET_OPACITY:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Entity#blockHalfExtents:member", + "docComment": "/**\n * The half extents of the block entity's visual size.\n *\n * @remarks\n *\n * Only set for block entities.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "SET_OPACITY = " + "text": "get blockHalfExtents(): " }, { - "kind": "Content", - "text": "\"ENTITY.SET_OPACITY\"" - } - ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "name": "SET_OPACITY" - }, - { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.SET_OUTLINE:member", - "docComment": "", - "excerptTokens": [ + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" + }, { "kind": "Content", - "text": "SET_OUTLINE = " + "text": " | undefined" }, { "kind": "Content", - "text": "\"ENTITY.SET_OUTLINE\"" + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "blockHalfExtents", + "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 2 + "endIndex": 3 }, - "releaseTag": "Public", - "name": "SET_OUTLINE" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.SET_PARENT:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Entity#blockTextureUri:member", + "docComment": "/**\n * The texture URI for block entities.\n *\n * @remarks\n *\n * When set, this entity is treated as a block entity.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "SET_PARENT = " + "text": "get blockTextureUri(): " }, { "kind": "Content", - "text": "\"ENTITY.SET_PARENT\"" + "text": "string | undefined" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "blockTextureUri", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", - "name": "SET_PARENT" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.SET_POSITION_INTERPOLATION_MS:member", - "docComment": "", + "kind": "Method", + "canonicalReference": "server!Entity#clearModelNodeOverrides:member(1)", + "docComment": "/**\n * Clears all model node overrides from the entity's model.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "SET_POSITION_INTERPOLATION_MS = " + "text": "clearModelNodeOverrides(): " }, { "kind": "Content", - "text": "\"ENTITY.SET_POSITION_INTERPOLATION_MS\"" + "text": "void" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isStatic": false, + "returnTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, "releaseTag": "Public", - "name": "SET_POSITION_INTERPOLATION_MS" + "isProtected": false, + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "clearModelNodeOverrides" }, { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.SET_ROTATION_INTERPOLATION_MS:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Entity#controller:member", + "docComment": "/**\n * The controller for the entity.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "SET_ROTATION_INTERPOLATION_MS = " + "text": "get controller(): " }, { - "kind": "Content", - "text": "\"ENTITY.SET_ROTATION_INTERPOLATION_MS\"" - } - ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "name": "SET_ROTATION_INTERPOLATION_MS" - }, - { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.SET_TINT_COLOR:member", - "docComment": "", - "excerptTokens": [ + "kind": "Reference", + "text": "BaseEntityController", + "canonicalReference": "server!BaseEntityController:class" + }, { "kind": "Content", - "text": "SET_TINT_COLOR = " + "text": " | undefined" }, { "kind": "Content", - "text": "\"ENTITY.SET_TINT_COLOR\"" + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "controller", + "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 2 + "endIndex": 3 }, - "releaseTag": "Public", - "name": "SET_TINT_COLOR" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.SPAWN:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Entity#depth:member", + "docComment": "/**\n * The depth (Z-axis) of the entity's model or block size.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "SPAWN = " + "text": "get depth(): " }, { "kind": "Content", - "text": "\"ENTITY.SPAWN\"" + "text": "number" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "depth", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", - "name": "SPAWN" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.TICK:member", - "docComment": "", + "kind": "Method", + "canonicalReference": "server!Entity#despawn:member(1)", + "docComment": "/**\n * Despawns the entity and all children from the world.\n *\n * Use for: removing entities from the world. Do NOT use for: temporary hiding; consider visibility or animations instead.\n *\n * @remarks\n *\n * **Cascading:** Recursively despawns all child entities first (depth-first).\n *\n * **Controller:** Calls `controller.detach()` then `controller.despawn()` if attached.\n *\n * **Cleanup:** Automatically unregisters attached audios, despawns attached particle emitters, and unloads attached scene UIs from their respective managers.\n *\n * **Simulation:** Removes from physics simulation.\n *\n * **Side effects:** Emits `EntityEvent.DESPAWN` and unregisters from world managers.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "TICK = " + "text": "despawn(): " }, { "kind": "Content", - "text": "\"ENTITY.TICK\"" + "text": "void" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isStatic": false, + "returnTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, "releaseTag": "Public", - "name": "TICK" + "isProtected": false, + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "despawn" }, { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.UPDATE_POSITION:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Entity#emissiveColor:member", + "docComment": "/**\n * The emissive color of the entity.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "UPDATE_POSITION = " + "text": "get emissiveColor(): " + }, + { + "kind": "Reference", + "text": "RgbColor", + "canonicalReference": "server!RgbColor:interface" }, { "kind": "Content", - "text": "\"ENTITY.UPDATE_POSITION\"" + "text": " | undefined" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "emissiveColor", + "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 2 + "endIndex": 3 }, - "releaseTag": "Public", - "name": "UPDATE_POSITION" + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "EnumMember", - "canonicalReference": "server!EntityEvent.UPDATE_ROTATION:member", - "docComment": "", + "kind": "Property", + "canonicalReference": "server!Entity#emissiveIntensity:member", + "docComment": "/**\n * The emissive intensity of the entity.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "UPDATE_ROTATION = " + "text": "get emissiveIntensity(): " }, { "kind": "Content", - "text": "\"ENTITY.UPDATE_ROTATION\"" + "text": "number | undefined" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "emissiveIntensity", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", - "name": "UPDATE_ROTATION" - } - ] - }, - { - "kind": "Interface", - "canonicalReference": "server!EntityEventPayloads:interface", - "docComment": "/**\n * Event payloads for Entity emitted events.\n *\n * **Category:** Events\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export interface EntityEventPayloads " - } - ], - "fileUrlPath": "src/worlds/entities/Entity.ts", - "releaseTag": "Public", - "name": "EntityEventPayloads", - "preserveMemberOrder": false, - "members": [ + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.BLOCK_COLLISION\":member", - "docComment": "/**\n * Emitted when an entity collides with a block type.\n */\n", + "kind": "Method", + "canonicalReference": "server!Entity#getModelAnimation:member(1)", + "docComment": "/**\n * Gets or lazily creates a model animation for the entity's model by name.\n *\n * @remarks\n *\n * Model entities only; returns `undefined` for block entities. If the animation does not yet exist, a new instance with default settings is created and added to `modelAnimations`. Use `availableModelAnimationNames` to discover which animation names exist in the model.\n *\n * @param name - The name of the animation to get or create.\n *\n * @returns The model animation instance, or `undefined` for block entities.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "[" - }, - { - "kind": "Reference", - "text": "EntityEvent.BLOCK_COLLISION", - "canonicalReference": "server!EntityEvent.BLOCK_COLLISION:member" - }, - { - "kind": "Content", - "text": "]: " + "text": "getModelAnimation(name: " }, { "kind": "Content", - "text": "{\n entity: " - }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": "string" }, { "kind": "Content", - "text": ";\n blockType: " + "text": "): " }, { "kind": "Reference", - "text": "BlockType", - "canonicalReference": "server!BlockType:class" + "text": "EntityModelAnimation", + "canonicalReference": "server!EntityModelAnimation:class" }, { "kind": "Content", - "text": ";\n started: boolean;\n colliderHandleA: number;\n colliderHandleB: number;\n }" + "text": " | undefined" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "\"ENTITY.BLOCK_COLLISION\"", - "propertyTypeTokenRange": { + "isStatic": false, + "returnTypeTokenRange": { "startIndex": 3, - "endIndex": 8 - } + "endIndex": 5 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "name", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "getModelAnimation" }, { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.BLOCK_CONTACT_FORCE\":member", - "docComment": "/**\n * Emitted when an entity's contact force is applied to a block type.\n */\n", + "kind": "Method", + "canonicalReference": "server!Entity#getModelNodeOverride:member(1)", + "docComment": "/**\n * Gets or lazily creates a model node override for the entity's model.\n *\n * @remarks\n *\n * Model entities only; returns `undefined` for block entities. If the override does not yet exist, a new instance with default settings is created and added to `modelNodeOverrides`. Use `availableModelNodeNames` to discover which node names exist in the model.\n *\n * @param nameMatch - The node selector for the model node override to get or create. Case-insensitive exact match by default, with optional edge wildcard (`head*`, `*head`, `*head*`).\n *\n * @returns The model node override instance, or `undefined` for block entities.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "[" - }, - { - "kind": "Reference", - "text": "EntityEvent.BLOCK_CONTACT_FORCE", - "canonicalReference": "server!EntityEvent.BLOCK_CONTACT_FORCE:member" + "text": "getModelNodeOverride(nameMatch: " }, { "kind": "Content", - "text": "]: " + "text": "string" }, { "kind": "Content", - "text": "{\n entity: " + "text": "): " }, { "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": "EntityModelNodeOverride", + "canonicalReference": "server!EntityModelNodeOverride:class" }, { "kind": "Content", - "text": ";\n blockType: " - }, - { - "kind": "Reference", - "text": "BlockType", - "canonicalReference": "server!BlockType:class" + "text": " | undefined" }, { "kind": "Content", - "text": ";\n contactForceData: " - }, + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 5 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ { - "kind": "Reference", - "text": "ContactForceData", - "canonicalReference": "server!ContactForceData:type" + "parameterName": "nameMatch", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "getModelNodeOverride" + }, + { + "kind": "Property", + "canonicalReference": "server!Entity#height:member", + "docComment": "/**\n * The height (Y-axis) of the entity's model or block size.\n *\n * **Category:** Entities\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "get height(): " }, { "kind": "Content", - "text": ";\n }" + "text": "number" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, + "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "\"ENTITY.BLOCK_CONTACT_FORCE\"", + "name": "height", "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 10 - } + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.DESPAWN\":member", - "docComment": "/**\n * Emitted when an entity is despawned.\n */\n", + "kind": "Property", + "canonicalReference": "server!Entity#id:member", + "docComment": "/**\n * The unique identifier for the entity.\n *\n * @remarks\n *\n * Assigned when the entity is spawned.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "[" - }, - { - "kind": "Reference", - "text": "EntityEvent.DESPAWN", - "canonicalReference": "server!EntityEvent.DESPAWN:member" - }, - { - "kind": "Content", - "text": "]: " - }, - { - "kind": "Content", - "text": "{\n entity: " - }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": "get id(): " }, { "kind": "Content", - "text": ";\n }" + "text": "number | undefined" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, + "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "\"ENTITY.DESPAWN\"", + "name": "id", "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 6 - } + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.ENTITY_COLLISION\":member", - "docComment": "/**\n * Emitted when an entity collides with another entity.\n */\n", + "kind": "Method", + "canonicalReference": "server!Entity#interact:member(1)", + "docComment": "/**\n * Triggers an interaction on the entity from a player.\n *\n * Use for: programmatic interactions that should mimic a player click/tap. Do NOT use for: server-only effects without player context.\n *\n * @remarks\n *\n * This is automatically called when a player clicks or taps the entity, but can also be called directly for programmatic interactions. Emits `EntityEvent.INTERACT`.\n *\n * @param player - The player interacting with the entity.\n *\n * @param raycastHit - The raycast hit result, if the interaction was triggered by a client-side click/tap.\n *\n * **Requires:** Entity must be spawned.\n *\n * **Side effects:** Emits `EntityEvent.INTERACT`.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "[" + "text": "interact(player: " }, { "kind": "Reference", - "text": "EntityEvent.ENTITY_COLLISION", - "canonicalReference": "server!EntityEvent.ENTITY_COLLISION:member" - }, - { - "kind": "Content", - "text": "]: " + "text": "Player", + "canonicalReference": "server!Player:class" }, { "kind": "Content", - "text": "{\n entity: " + "text": ", raycastHit?: " }, { "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": "RaycastHit", + "canonicalReference": "server!RaycastHit:type" }, { "kind": "Content", - "text": ";\n otherEntity: " - }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": "): " }, { "kind": "Content", - "text": ";\n started: boolean;\n colliderHandleA: number;\n colliderHandleB: number;\n }" + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": false, + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, "releaseTag": "Public", - "name": "\"ENTITY.ENTITY_COLLISION\"", - "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 8 - } + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "player", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "raycastHit", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": true + } + ], + "isOptional": false, + "isAbstract": false, + "name": "interact" }, { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.ENTITY_CONTACT_FORCE\":member", - "docComment": "/**\n * Emitted when an entity's contact force is applied to another entity.\n */\n", + "kind": "Property", + "canonicalReference": "server!Entity#isBlockEntity:member", + "docComment": "/**\n * Whether this entity is a block entity.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "[" - }, - { - "kind": "Reference", - "text": "EntityEvent.ENTITY_CONTACT_FORCE", - "canonicalReference": "server!EntityEvent.ENTITY_CONTACT_FORCE:member" + "text": "get isBlockEntity(): " }, { "kind": "Content", - "text": "]: " + "text": "boolean" }, { "kind": "Content", - "text": "{\n entity: " - }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" - }, + "text": ";" + } + ], + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "isBlockEntity", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Property", + "canonicalReference": "server!Entity#isEnvironmental:member", + "docComment": "/**\n * Whether the entity is environmental.\n *\n * @remarks\n *\n * Environmental entities are excluded from per-tick controller updates and update emission.\n *\n * **Category:** Entities\n */\n", + "excerptTokens": [ { "kind": "Content", - "text": ";\n otherEntity: " + "text": "get isEnvironmental(): " }, { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "kind": "Content", + "text": "boolean" }, { "kind": "Content", - "text": ";\n contactForceData: " - }, + "text": ";" + } + ], + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "isEnvironmental", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Property", + "canonicalReference": "server!Entity#isModelEntity:member", + "docComment": "/**\n * Whether this entity is a model entity.\n *\n * **Category:** Entities\n */\n", + "excerptTokens": [ { - "kind": "Reference", - "text": "ContactForceData", - "canonicalReference": "server!ContactForceData:type" + "kind": "Content", + "text": "get isModelEntity(): " }, { "kind": "Content", - "text": ";\n }" + "text": "boolean" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, + "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "\"ENTITY.ENTITY_CONTACT_FORCE\"", + "name": "isModelEntity", "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 10 - } + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.INTERACT\":member", - "docComment": "/**\n * Emitted when a player interacts with the entity by clicking or tapping it.\n */\n", + "kind": "Property", + "canonicalReference": "server!Entity#isSpawned:member", + "docComment": "/**\n * Whether the entity is spawned in a world.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "[" - }, - { - "kind": "Reference", - "text": "EntityEvent.INTERACT", - "canonicalReference": "server!EntityEvent.INTERACT:member" + "text": "get isSpawned(): " }, { "kind": "Content", - "text": "]: " + "text": "boolean" }, { "kind": "Content", - "text": "{\n entity: " - }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" - }, + "text": ";" + } + ], + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "isSpawned", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Property", + "canonicalReference": "server!Entity#modelAnimations:member", + "docComment": "/**\n * The animations of the entity's model that have been accessed or configured.\n *\n * @remarks\n *\n * Animations are lazily created on first access via `getModelAnimation()`. This array only contains animations that have been explicitly used, not every clip in the model.\n *\n * **Category:** Entities\n */\n", + "excerptTokens": [ { "kind": "Content", - "text": ";\n player: " + "text": "get modelAnimations(): " }, { "kind": "Reference", - "text": "Player", - "canonicalReference": "server!Player:class" + "text": "Readonly", + "canonicalReference": "!Readonly:type" }, { "kind": "Content", - "text": ";\n raycastHit?: " + "text": "<" }, { "kind": "Reference", - "text": "RaycastHit", - "canonicalReference": "server!RaycastHit:type" + "text": "EntityModelAnimation", + "canonicalReference": "server!EntityModelAnimation:class" }, { "kind": "Content", - "text": ";\n }" + "text": "[]>" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, + "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "\"ENTITY.INTERACT\"", + "name": "modelAnimations", "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 10 - } + "startIndex": 1, + "endIndex": 5 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.REMOVE_MODEL_NODE_OVERRIDE\":member", - "docComment": "/**\n * Emitted when a model node override is removed from the entity's model.\n */\n", + "kind": "Property", + "canonicalReference": "server!Entity#modelNodeOverrides:member", + "docComment": "/**\n * The node overrides of the entity's model that have been accessed or configured.\n *\n * @remarks\n *\n * Node overrides are lazily created on first access via `getModelNodeOverride()`. This array only contains overrides that have been explicitly used.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "[" - }, - { - "kind": "Reference", - "text": "EntityEvent.REMOVE_MODEL_NODE_OVERRIDE", - "canonicalReference": "server!EntityEvent.REMOVE_MODEL_NODE_OVERRIDE:member" - }, - { - "kind": "Content", - "text": "]: " - }, - { - "kind": "Content", - "text": "{\n entity: " + "text": "get modelNodeOverrides(): " }, { "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": "Readonly", + "canonicalReference": "!Readonly:type" }, { "kind": "Content", - "text": ";\n entityModelNodeOverride: " + "text": "<" }, { "kind": "Reference", @@ -18126,916 +17938,852 @@ }, { "kind": "Content", - "text": ";\n }" + "text": "[]>" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, + "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "\"ENTITY.REMOVE_MODEL_NODE_OVERRIDE\"", + "name": "modelNodeOverrides", "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 8 - } + "startIndex": 1, + "endIndex": 5 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_BLOCK_TEXTURE_URI\":member", - "docComment": "/**\n * Emitted when the texture uri of a block entity is set.\n */\n", + "kind": "Property", + "canonicalReference": "server!Entity#modelPreferredShape:member", + "docComment": "/**\n * The preferred collider shape when auto-generating colliders from the model.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "[" - }, - { - "kind": "Reference", - "text": "EntityEvent.SET_BLOCK_TEXTURE_URI", - "canonicalReference": "server!EntityEvent.SET_BLOCK_TEXTURE_URI:member" - }, - { - "kind": "Content", - "text": "]: " - }, - { - "kind": "Content", - "text": "{\n entity: " + "text": "get modelPreferredShape(): " }, { "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": "ColliderShape", + "canonicalReference": "server!ColliderShape:enum" }, { "kind": "Content", - "text": ";\n blockTextureUri: string | undefined;\n }" + "text": " | undefined" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, + "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "\"ENTITY.SET_BLOCK_TEXTURE_URI\"", + "name": "modelPreferredShape", "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 6 - } + "startIndex": 1, + "endIndex": 3 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_EMISSIVE_COLOR\":member", - "docComment": "/**\n * Emitted when the emissive color is set.\n */\n", + "kind": "Property", + "canonicalReference": "server!Entity#modelScale:member", + "docComment": "/**\n * The scale of the entity's model.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "[" + "text": "get modelScale(): " }, { "kind": "Reference", - "text": "EntityEvent.SET_EMISSIVE_COLOR", - "canonicalReference": "server!EntityEvent.SET_EMISSIVE_COLOR:member" - }, - { - "kind": "Content", - "text": "]: " + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", - "text": "{\n entity: " - }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" - }, + "text": ";" + } + ], + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "modelScale", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Property", + "canonicalReference": "server!Entity#modelScaleInterpolationMs:member", + "docComment": "/**\n * The interpolation time in milliseconds applied to model scale changes.\n *\n * **Category:** Entities\n */\n", + "excerptTokens": [ { "kind": "Content", - "text": ";\n emissiveColor: " - }, - { - "kind": "Reference", - "text": "RgbColor", - "canonicalReference": "server!RgbColor:interface" + "text": "get modelScaleInterpolationMs(): " }, { "kind": "Content", - "text": " | undefined;\n }" + "text": "number | undefined" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, + "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "\"ENTITY.SET_EMISSIVE_COLOR\"", + "name": "modelScaleInterpolationMs", "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 8 - } + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_EMISSIVE_INTENSITY\":member", - "docComment": "/**\n * Emitted when the emissive intensity is set.\n */\n", + "kind": "Property", + "canonicalReference": "server!Entity#modelTextureUri:member", + "docComment": "/**\n * The texture URI that overrides the model entity's default texture.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "[" - }, - { - "kind": "Reference", - "text": "EntityEvent.SET_EMISSIVE_INTENSITY", - "canonicalReference": "server!EntityEvent.SET_EMISSIVE_INTENSITY:member" + "text": "get modelTextureUri(): " }, { "kind": "Content", - "text": "]: " + "text": "string | undefined" }, { "kind": "Content", - "text": "{\n entity: " - }, + "text": ";" + } + ], + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "modelTextureUri", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Property", + "canonicalReference": "server!Entity#modelUri:member", + "docComment": "/**\n * The URI or path to the `.gltf` model asset.\n *\n * **Category:** Entities\n */\n", + "excerptTokens": [ { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "kind": "Content", + "text": "get modelUri(): " }, { "kind": "Content", - "text": ";\n emissiveIntensity: number | undefined;\n }" + "text": "string | undefined" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, + "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "\"ENTITY.SET_EMISSIVE_INTENSITY\"", + "name": "modelUri", "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 6 - } + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_MODEL_SCALE_INTERPOLATION_MS\":member", - "docComment": "/**\n * Emitted when the interpolation time in milliseconds applied to model scale changes is set.\n */\n", + "kind": "Property", + "canonicalReference": "server!Entity#name:member", + "docComment": "/**\n * The name of the entity.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "[" - }, - { - "kind": "Reference", - "text": "EntityEvent.SET_MODEL_SCALE_INTERPOLATION_MS", - "canonicalReference": "server!EntityEvent.SET_MODEL_SCALE_INTERPOLATION_MS:member" + "text": "get name(): " }, { "kind": "Content", - "text": "]: " + "text": "string" }, { "kind": "Content", - "text": "{\n entity: " - }, + "text": ";" + } + ], + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "name", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Property", + "canonicalReference": "server!Entity#opacity:member", + "docComment": "/**\n * The opacity of the entity between 0 and 1.\n *\n * **Category:** Entities\n */\n", + "excerptTokens": [ { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "kind": "Content", + "text": "get opacity(): " }, { "kind": "Content", - "text": ";\n interpolationMs: number | undefined;\n }" + "text": "number" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, + "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "\"ENTITY.SET_MODEL_SCALE_INTERPOLATION_MS\"", + "name": "opacity", "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 6 - } + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_MODEL_SCALE\":member", - "docComment": "/**\n * Emitted when the scale of the entity's model is set.\n */\n", + "kind": "Property", + "canonicalReference": "server!Entity#outline:member", + "docComment": "/**\n * The outline rendering options for the entity.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "[" + "text": "get outline(): " }, { "kind": "Reference", - "text": "EntityEvent.SET_MODEL_SCALE", - "canonicalReference": "server!EntityEvent.SET_MODEL_SCALE:member" + "text": "Outline", + "canonicalReference": "server!Outline:interface" }, { "kind": "Content", - "text": "]: " + "text": " | undefined" }, { "kind": "Content", - "text": "{\n entity: " - }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" - }, + "text": ";" + } + ], + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "outline", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Property", + "canonicalReference": "server!Entity#parent:member", + "docComment": "/**\n * The parent entity, if attached.\n *\n * **Category:** Entities\n */\n", + "excerptTokens": [ { "kind": "Content", - "text": ";\n modelScale: " + "text": "get parent(): " }, { "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" + "text": "Entity", + "canonicalReference": "server!Entity:class" }, { "kind": "Content", - "text": ";\n }" + "text": " | undefined" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, + "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "\"ENTITY.SET_MODEL_SCALE\"", + "name": "parent", "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 8 - } + "startIndex": 1, + "endIndex": 3 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_MODEL_TEXTURE_URI\":member", - "docComment": "/**\n * Emitted when the texture uri of the entity's model is set.\n */\n", + "kind": "Property", + "canonicalReference": "server!Entity#parentNodeName:member", + "docComment": "/**\n * The parent model node name, if attached.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "[" - }, - { - "kind": "Reference", - "text": "EntityEvent.SET_MODEL_TEXTURE_URI", - "canonicalReference": "server!EntityEvent.SET_MODEL_TEXTURE_URI:member" + "text": "get parentNodeName(): " }, { "kind": "Content", - "text": "]: " + "text": "string | undefined" }, { "kind": "Content", - "text": "{\n entity: " - }, + "text": ";" + } + ], + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "parentNodeName", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Property", + "canonicalReference": "server!Entity#positionInterpolationMs:member", + "docComment": "/**\n * The interpolation time in milliseconds applied to position changes.\n *\n * **Category:** Entities\n */\n", + "excerptTokens": [ { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "kind": "Content", + "text": "get positionInterpolationMs(): " }, { "kind": "Content", - "text": ";\n modelTextureUri: string | undefined;\n }" + "text": "number | undefined" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, + "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "\"ENTITY.SET_MODEL_TEXTURE_URI\"", + "name": "positionInterpolationMs", "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 6 - } + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_OPACITY\":member", - "docComment": "/**\n * Emitted when the opacity of the entity is set.\n */\n", + "kind": "Method", + "canonicalReference": "server!Entity#removeModelNodeOverride:member(1)", + "docComment": "/**\n * Removes a model node override from the entity's model.\n *\n * @param nameMatch - The name match of the model node override to remove.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "[" - }, - { - "kind": "Reference", - "text": "EntityEvent.SET_OPACITY", - "canonicalReference": "server!EntityEvent.SET_OPACITY:member" + "text": "removeModelNodeOverride(nameMatch: " }, { "kind": "Content", - "text": "]: " + "text": "string" }, { "kind": "Content", - "text": "{\n entity: " - }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": "): " }, { "kind": "Content", - "text": ";\n opacity: number;\n }" + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "\"ENTITY.SET_OPACITY\"", - "propertyTypeTokenRange": { + "isStatic": false, + "returnTypeTokenRange": { "startIndex": 3, - "endIndex": 6 - } + "endIndex": 4 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "nameMatch", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "removeModelNodeOverride" }, { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_OUTLINE\":member", - "docComment": "/**\n * Emitted when the outline of the entity is set.\n */\n", + "kind": "Method", + "canonicalReference": "server!Entity#removeModelNodeOverrides:member(1)", + "docComment": "/**\n * Removes multiple model node overrides from the entity's model.\n *\n * @param nameMatches - The name matches of the model node overrides to remove.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "[" - }, - { - "kind": "Reference", - "text": "EntityEvent.SET_OUTLINE", - "canonicalReference": "server!EntityEvent.SET_OUTLINE:member" + "text": "removeModelNodeOverrides(nameMatches: " }, { "kind": "Content", - "text": "]: " + "text": "string[]" }, { "kind": "Content", - "text": "{\n entity: " + "text": "): " }, { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "kind": "Content", + "text": "void" }, { "kind": "Content", - "text": ";\n outline: " - }, + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ { - "kind": "Reference", - "text": "Outline", - "canonicalReference": "server!Outline:interface" - }, + "parameterName": "nameMatches", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "removeModelNodeOverrides" + }, + { + "kind": "Property", + "canonicalReference": "server!Entity#rotationInterpolationMs:member", + "docComment": "/**\n * The interpolation time in milliseconds applied to rotation changes.\n *\n * **Category:** Entities\n */\n", + "excerptTokens": [ { "kind": "Content", - "text": " | undefined;\n forPlayer?: " - }, - { - "kind": "Reference", - "text": "Player", - "canonicalReference": "server!Player:class" + "text": "get rotationInterpolationMs(): " }, { "kind": "Content", - "text": ";\n }" + "text": "number | undefined" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, + "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "\"ENTITY.SET_OUTLINE\"", + "name": "rotationInterpolationMs", "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 10 - } + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false }, { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_PARENT\":member", - "docComment": "/**\n * Emitted when the parent of the entity is set.\n */\n", + "kind": "Method", + "canonicalReference": "server!Entity#setBlockTextureUri:member(1)", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "[" - }, - { - "kind": "Reference", - "text": "EntityEvent.SET_PARENT", - "canonicalReference": "server!EntityEvent.SET_PARENT:member" - }, - { - "kind": "Content", - "text": "]: " + "text": "setBlockTextureUri(blockTextureUri: " }, { "kind": "Content", - "text": "{\n entity: " - }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": "string | undefined" }, { "kind": "Content", - "text": ";\n parent: " - }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": "): " }, { "kind": "Content", - "text": " | undefined;\n parentNodeName: string | undefined;\n }" + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "\"ENTITY.SET_PARENT\"", - "propertyTypeTokenRange": { + "isStatic": false, + "returnTypeTokenRange": { "startIndex": 3, - "endIndex": 8 - } + "endIndex": 4 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "blockTextureUri", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "setBlockTextureUri" }, { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_POSITION_INTERPOLATION_MS\":member", - "docComment": "/**\n * Emitted when the interpolation time in milliseconds applied to position changes is set.\n */\n", + "kind": "Method", + "canonicalReference": "server!Entity#setEmissiveColor:member(1)", + "docComment": "/**\n * Sets the emissive color of the entity.\n *\n * Use for: glow effects or highlighted states.\n *\n * @param emissiveColor - The emissive color of the entity.\n *\n * **Side effects:** Emits `EntityEvent.SET_EMISSIVE_COLOR` when spawned.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "[" + "text": "setEmissiveColor(emissiveColor: " }, { "kind": "Reference", - "text": "EntityEvent.SET_POSITION_INTERPOLATION_MS", - "canonicalReference": "server!EntityEvent.SET_POSITION_INTERPOLATION_MS:member" + "text": "RgbColor", + "canonicalReference": "server!RgbColor:interface" }, { "kind": "Content", - "text": "]: " + "text": " | undefined" }, { "kind": "Content", - "text": "{\n entity: " - }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": "): " }, { "kind": "Content", - "text": ";\n interpolationMs: number | undefined;\n }" + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": false, + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 4, + "endIndex": 5 + }, "releaseTag": "Public", - "name": "\"ENTITY.SET_POSITION_INTERPOLATION_MS\"", - "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 6 - } + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "emissiveColor", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "setEmissiveColor" }, { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_ROTATION_INTERPOLATION_MS\":member", - "docComment": "/**\n * Emitted when the interpolation time in milliseconds applied to rotation changes is set.\n */\n", + "kind": "Method", + "canonicalReference": "server!Entity#setEmissiveIntensity:member(1)", + "docComment": "/**\n * Sets the emissive intensity of the entity.\n *\n * @param emissiveIntensity - The emissive intensity of the entity. Use a value over 1 for brighter emissive effects.\n *\n * **Side effects:** Emits `EntityEvent.SET_EMISSIVE_INTENSITY` when spawned.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "[" - }, - { - "kind": "Reference", - "text": "EntityEvent.SET_ROTATION_INTERPOLATION_MS", - "canonicalReference": "server!EntityEvent.SET_ROTATION_INTERPOLATION_MS:member" + "text": "setEmissiveIntensity(emissiveIntensity: " }, { "kind": "Content", - "text": "]: " + "text": "number | undefined" }, { "kind": "Content", - "text": "{\n entity: " - }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": "): " }, { "kind": "Content", - "text": ";\n interpolationMs: number | undefined;\n }" + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "\"ENTITY.SET_ROTATION_INTERPOLATION_MS\"", - "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 6 - } - }, - { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_TINT_COLOR\":member", - "docComment": "/**\n * Emitted when the tint color of the entity is set.\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "[" - }, - { - "kind": "Reference", - "text": "EntityEvent.SET_TINT_COLOR", - "canonicalReference": "server!EntityEvent.SET_TINT_COLOR:member" - }, - { - "kind": "Content", - "text": "]: " - }, - { - "kind": "Content", - "text": "{\n entity: " - }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" - }, - { - "kind": "Content", - "text": ";\n tintColor: " - }, - { - "kind": "Reference", - "text": "RgbColor", - "canonicalReference": "server!RgbColor:interface" - }, - { - "kind": "Content", - "text": " | undefined;\n }" - }, + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ { - "kind": "Content", - "text": ";" + "parameterName": "emissiveIntensity", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false } ], - "isReadonly": false, "isOptional": false, - "releaseTag": "Public", - "name": "\"ENTITY.SET_TINT_COLOR\"", - "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 8 - } + "isAbstract": false, + "name": "setEmissiveIntensity" }, { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SPAWN\":member", - "docComment": "/**\n * Emitted when the entity is spawned.\n */\n", + "kind": "Method", + "canonicalReference": "server!Entity#setModelAnimationsPlaybackRate:member(1)", + "docComment": "/**\n * Sets the playback rate for all of the entity's model animations.\n *\n * @remarks\n *\n * A value of 1 is normal speed, 0.5 is half speed, 2 is double speed. A negative value will play the animation in reverse.\n *\n * @param playbackRate - The playback rate of the entity's model animations.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "[" - }, - { - "kind": "Reference", - "text": "EntityEvent.SPAWN", - "canonicalReference": "server!EntityEvent.SPAWN:member" + "text": "setModelAnimationsPlaybackRate(playbackRate: " }, { "kind": "Content", - "text": "]: " + "text": "number" }, { "kind": "Content", - "text": "{\n entity: " - }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": "): " }, { "kind": "Content", - "text": ";\n }" + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "\"ENTITY.SPAWN\"", - "propertyTypeTokenRange": { + "isStatic": false, + "returnTypeTokenRange": { "startIndex": 3, - "endIndex": 6 - } - }, - { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.TICK\":member", - "docComment": "/**\n * Emitted when the entity is ticked.\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "[" - }, - { - "kind": "Reference", - "text": "EntityEvent.TICK", - "canonicalReference": "server!EntityEvent.TICK:member" - }, - { - "kind": "Content", - "text": "]: " - }, - { - "kind": "Content", - "text": "{\n entity: " - }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" - }, - { - "kind": "Content", - "text": ";\n tickDeltaMs: number;\n }" - }, + "endIndex": 4 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ { - "kind": "Content", - "text": ";" + "parameterName": "playbackRate", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false } ], - "isReadonly": false, "isOptional": false, - "releaseTag": "Public", - "name": "\"ENTITY.TICK\"", - "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 6 - } + "isAbstract": false, + "name": "setModelAnimationsPlaybackRate" }, { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.UPDATE_POSITION\":member", - "docComment": "/**\n * Emitted when the position of the entity is updated at the end of the tick, either directly or by physics.\n */\n", + "kind": "Method", + "canonicalReference": "server!Entity#setModelNodeEmissiveColor:member(1)", + "docComment": "/**\n * Sets the emissive color for a model node by name.\n *\n * @param nodeName - The node name to target.\n *\n * @param color - The RGB color to set, or undefined to clear.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "[" - }, - { - "kind": "Reference", - "text": "EntityEvent.UPDATE_POSITION", - "canonicalReference": "server!EntityEvent.UPDATE_POSITION:member" + "text": "setModelNodeEmissiveColor(nodeName: " }, { "kind": "Content", - "text": "]: " + "text": "string" }, { "kind": "Content", - "text": "{\n entity: " + "text": ", color: " }, { "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": "RgbColor", + "canonicalReference": "server!RgbColor:interface" }, { "kind": "Content", - "text": ";\n position: " + "text": " | undefined" }, { - "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" + "kind": "Content", + "text": "): " }, { "kind": "Content", - "text": ";\n }" + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": false, + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 6, + "endIndex": 7 + }, "releaseTag": "Public", - "name": "\"ENTITY.UPDATE_POSITION\"", - "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 8 - } + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "nodeName", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "color", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 5 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "setModelNodeEmissiveColor" }, { - "kind": "PropertySignature", - "canonicalReference": "server!EntityEventPayloads#\"ENTITY.UPDATE_ROTATION\":member", - "docComment": "/**\n * Emitted when the rotation of the entity is updated at the end of the tick, either directly or by physics.\n */\n", + "kind": "Method", + "canonicalReference": "server!Entity#setModelNodeEmissiveIntensity:member(1)", + "docComment": "/**\n * Sets the emissive intensity for a model node by name.\n *\n * @param nodeName - The node name to target.\n *\n * @param intensity - The intensity value to set, or undefined to clear.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "[" - }, - { - "kind": "Reference", - "text": "EntityEvent.UPDATE_ROTATION", - "canonicalReference": "server!EntityEvent.UPDATE_ROTATION:member" + "text": "setModelNodeEmissiveIntensity(nodeName: " }, { "kind": "Content", - "text": "]: " + "text": "string" }, { "kind": "Content", - "text": "{\n entity: " - }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": ", intensity: " }, { "kind": "Content", - "text": ";\n rotation: " + "text": "number | undefined" }, { - "kind": "Reference", - "text": "QuaternionLike", - "canonicalReference": "server!QuaternionLike:interface" + "kind": "Content", + "text": "): " }, { "kind": "Content", - "text": ";\n }" + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isReadonly": false, - "isOptional": false, + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, "releaseTag": "Public", - "name": "\"ENTITY.UPDATE_ROTATION\"", - "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 8 - } - } - ], - "extendsTokenRanges": [] - }, - { - "kind": "Class", - "canonicalReference": "server!EntityManager:class", - "docComment": "/**\n * Manages entities in a world.\n *\n * When to use: querying and filtering entities within a specific world. Do NOT use for: cross-world queries; access each world's manager separately.\n *\n * @remarks\n *\n * The EntityManager is created internally per `World` instance.\n *\n * The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `EntityManager` class.\n *\n * @example\n * ```typescript\n * // Get all entities in the world\n * const entityManager = world.entityManager;\n * const entities = entityManager.getAllEntities();\n * ```\n *\n * **Category:** Entities\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export default class EntityManager " - } - ], - "fileUrlPath": "src/worlds/entities/EntityManager.ts", - "releaseTag": "Public", - "isAbstract": false, - "name": "EntityManager", - "preserveMemberOrder": false, - "members": [ - { - "kind": "Property", - "canonicalReference": "server!EntityManager#entityCount:member", - "docComment": "/**\n * The number of spawned entities in the world.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "get entityCount(): " - }, + "isProtected": false, + "overloadIndex": 1, + "parameters": [ { - "kind": "Content", - "text": "number" + "parameterName": "nodeName", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false }, { - "kind": "Content", - "text": ";" + "parameterName": "intensity", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": false } ], - "isReadonly": true, "isOptional": false, - "releaseTag": "Public", - "name": "entityCount", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "isAbstract": false, + "name": "setModelNodeEmissiveIntensity" }, { "kind": "Method", - "canonicalReference": "server!EntityManager#getAllEntities:member(1)", - "docComment": "/**\n * Gets all spawned entities in the world.\n *\n * @returns All spawned entities in the world.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!Entity#setModelScale:member(1)", + "docComment": "/**\n * Sets the scale of the entity's model and proportionally scales its colliders.\n *\n * @remarks\n *\n * Model entities only; no effect for block entities.\n *\n * **Collider scaling is relative:** Colliders are scaled by the ratio of new/old scale, not set to absolute values. Example: scaling from 1 to 2 doubles collider size; scaling from 2 to 4 also doubles it.\n *\n * **Reference equality check:** Uses `===` to compare with current scale, so passing the same object reference will early return even if values changed. Always pass a new object.\n *\n * @param modelScale - The scale of the entity's model. Can be a vector or a number for uniform scaling.\n *\n * **Side effects:** Scales existing colliders and emits `EntityEvent.SET_MODEL_SCALE` when spawned.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "getAllEntities(): " + "text": "setModelScale(modelScale: " }, { "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", - "text": "[]" + "text": " | number" }, { "kind": "Content", - "text": ";" - } - ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - }, - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [], - "isOptional": false, - "isAbstract": false, - "name": "getAllEntities" - }, - { - "kind": "Method", - "canonicalReference": "server!EntityManager#getAllPlayerEntities:member(1)", - "docComment": "/**\n * Gets all spawned player entities in the world.\n *\n * @returns All spawned player entities in the world.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "getAllPlayerEntities(): " - }, - { - "kind": "Reference", - "text": "PlayerEntity", - "canonicalReference": "server!PlayerEntity:class" + "text": "): " }, { "kind": "Content", - "text": "[]" + "text": "void" }, { "kind": "Content", @@ -19044,42 +18792,46 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 + "startIndex": 4, + "endIndex": 5 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, - "parameters": [], + "parameters": [ + { + "parameterName": "modelScale", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + }, + "isOptional": false + } + ], "isOptional": false, "isAbstract": false, - "name": "getAllPlayerEntities" + "name": "setModelScale" }, { "kind": "Method", - "canonicalReference": "server!EntityManager#getEntitiesByTag:member(1)", - "docComment": "/**\n * Gets all spawned entities in the world with a specific tag.\n *\n * @param tag - The tag to get the entities for.\n *\n * @returns All spawned entities in the world with the provided tag.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!Entity#setModelScaleInterpolationMs:member(1)", + "docComment": "/**\n * Sets the interpolation time in milliseconds applied to model scale changes.\n *\n * @param interpolationMs - The interpolation time in milliseconds to set.\n *\n * **Side effects:** Emits `EntityEvent.SET_MODEL_SCALE_INTERPOLATION_MS` when spawned.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "getEntitiesByTag(tag: " + "text": "setModelScaleInterpolationMs(interpolationMs: " }, { "kind": "Content", - "text": "string" + "text": "number | undefined" }, { "kind": "Content", "text": "): " }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" - }, { "kind": "Content", - "text": "[]" + "text": "void" }, { "kind": "Content", @@ -19089,14 +18841,14 @@ "isStatic": false, "returnTypeTokenRange": { "startIndex": 3, - "endIndex": 5 + "endIndex": 4 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "tag", + "parameterName": "interpolationMs", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -19106,33 +18858,28 @@ ], "isOptional": false, "isAbstract": false, - "name": "getEntitiesByTag" + "name": "setModelScaleInterpolationMs" }, { "kind": "Method", - "canonicalReference": "server!EntityManager#getEntitiesByTagSubstring:member(1)", - "docComment": "/**\n * Gets all spawned entities in the world with a tag that includes a specific substring.\n *\n * @param tagSubstring - The tag substring to get the entities for.\n *\n * @returns All spawned entities in the world with a tag that includes the provided substring.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!Entity#setModelTextureUri:member(1)", + "docComment": "/**\n * Sets the texture uri of the entity's model. Setting this overrides the model's default texture.\n *\n * @remarks\n *\n * Model entities only; no effect for block entities.\n *\n * @param modelTextureUri - The texture uri of the entity's model.\n *\n * **Side effects:** Emits `EntityEvent.SET_MODEL_TEXTURE_URI` when spawned.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "getEntitiesByTagSubstring(tagSubstring: " + "text": "setModelTextureUri(modelTextureUri: " }, { "kind": "Content", - "text": "string" + "text": "string | undefined" }, { "kind": "Content", "text": "): " }, - { - "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" - }, { "kind": "Content", - "text": "[]" + "text": "void" }, { "kind": "Content", @@ -19142,14 +18889,14 @@ "isStatic": false, "returnTypeTokenRange": { "startIndex": 3, - "endIndex": 5 + "endIndex": 4 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "tagSubstring", + "parameterName": "modelTextureUri", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -19159,25 +18906,16 @@ ], "isOptional": false, "isAbstract": false, - "name": "getEntitiesByTagSubstring" + "name": "setModelTextureUri" }, { "kind": "Method", - "canonicalReference": "server!EntityManager#getEntity:member(1)", - "docComment": "/**\n * Gets a spawned entity in the world by its ID.\n *\n * @param id - The ID of the entity to get.\n *\n * @returns The spawned entity with the provided ID, or undefined if no entity is found.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!Entity#setOpacity:member(1)", + "docComment": "/**\n * Sets the opacity of the entity.\n *\n * @param opacity - The opacity of the entity between 0 and 1. 0 is fully transparent, 1 is fully opaque.\n *\n * **Side effects:** Emits `EntityEvent.SET_OPACITY` when spawned.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "getEntity(id: " + "text": "setOpacity(opacity: " }, { "kind": "Content", @@ -19189,74 +18927,69 @@ }, { "kind": "Content", - "text": "T | undefined" + "text": "void" }, { "kind": "Content", "text": ";" } ], - "typeParameters": [ - { - "typeParameterName": "T", - "constraintTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "defaultTypeTokenRange": { - "startIndex": 0, - "endIndex": 0 - } - } - ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 5, - "endIndex": 6 + "startIndex": 3, + "endIndex": 4 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "id", + "parameterName": "opacity", "parameterTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 + "startIndex": 1, + "endIndex": 2 }, "isOptional": false } ], "isOptional": false, "isAbstract": false, - "name": "getEntity" + "name": "setOpacity" }, { "kind": "Method", - "canonicalReference": "server!EntityManager#getEntityChildren:member(1)", - "docComment": "/**\n * Gets all child entities of an entity.\n *\n * @remarks\n *\n * Direct children only; does not include recursive descendants.\n *\n * @param entity - The entity to get the children for.\n *\n * @returns All direct child entities of the entity.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!Entity#setOutline:member(1)", + "docComment": "/**\n * Sets the outline rendering options for the entity.\n *\n * @param outline - The outline options, or undefined to remove the outline.\n *\n * @param forPlayer - The player to set the outline for, if undefined the outline will be set for all players.\n *\n * **Side effects:** Emits `EntityEvent.SET_OUTLINE` when spawned.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "getEntityChildren(entity: " + "text": "setOutline(outline: " }, { "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": "Outline", + "canonicalReference": "server!Outline:interface" }, { "kind": "Content", - "text": "): " + "text": " | undefined" + }, + { + "kind": "Content", + "text": ", forPlayer?: " }, { "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": "Player", + "canonicalReference": "server!Player:class" }, { "kind": "Content", - "text": "[]" + "text": "): " + }, + { + "kind": "Content", + "text": "void" }, { "kind": "Content", @@ -19265,52 +18998,85 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 5 + "startIndex": 6, + "endIndex": 7 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "entity", + "parameterName": "outline", "parameterTypeTokenRange": { "startIndex": 1, - "endIndex": 2 + "endIndex": 3 }, "isOptional": false + }, + { + "parameterName": "forPlayer", + "parameterTypeTokenRange": { + "startIndex": 4, + "endIndex": 5 + }, + "isOptional": true } ], "isOptional": false, "isAbstract": false, - "name": "getEntityChildren" + "name": "setOutline" }, { "kind": "Method", - "canonicalReference": "server!EntityManager#getPlayerEntitiesByPlayer:member(1)", - "docComment": "/**\n * Gets all spawned player entities in the world assigned to the provided player.\n *\n * @param player - The player to get the entities for.\n *\n * @returns All spawned player entities in the world assigned to the player.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!Entity#setParent:member(1)", + "docComment": "/**\n * Sets the parent of the entity and resets this entity's position and rotation.\n *\n * @remarks\n *\n * When setting the parent, all forces, torques and velocities of this entity are reset. Additionally, this entity's type will be set to `KINEMATIC_VELOCITY` if it is not already. All colliders of this entity will be disabled when parent is not undefined. If the provided parent is undefined, this entity will be removed from its parent and all colliders will be re-enabled. When setting an undefined parent to remove this entity from its parent, this entity's type will be set to the last type it was set to before being a child.\n *\n * @param parent - The parent entity to set, or undefined to remove from an existing parent.\n *\n * @param parentNodeName - The name of the parent's node (if parent is a model entity) this entity will attach to.\n *\n * @param position - The position to set for the entity. If parent is provided, this is relative to the parent's attachment point.\n *\n * @param rotation - The rotation to set for the entity. If parent is provided, this is relative to the parent's rotation.\n *\n * **Requires:** If `parent` is provided, it must be spawned.\n *\n * **Side effects:** Disables/enables colliders, changes rigid body type, and emits `EntityEvent.SET_PARENT`.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "getPlayerEntitiesByPlayer(player: " + "text": "setParent(parent: " }, { "kind": "Reference", - "text": "Player", - "canonicalReference": "server!Player:class" + "text": "Entity", + "canonicalReference": "server!Entity:class" }, { "kind": "Content", - "text": "): " + "text": " | undefined" + }, + { + "kind": "Content", + "text": ", parentNodeName?: " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": ", position?: " }, { "kind": "Reference", - "text": "PlayerEntity", - "canonicalReference": "server!PlayerEntity:class" + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", - "text": "[]" + "text": ", rotation?: " + }, + { + "kind": "Reference", + "text": "QuaternionLike", + "canonicalReference": "server!QuaternionLike:interface" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "void" }, { "kind": "Content", @@ -19319,429 +19085,298 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 5 + "startIndex": 10, + "endIndex": 11 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "player", + "parameterName": "parent", "parameterTypeTokenRange": { "startIndex": 1, - "endIndex": 2 + "endIndex": 3 }, "isOptional": false + }, + { + "parameterName": "parentNodeName", + "parameterTypeTokenRange": { + "startIndex": 4, + "endIndex": 5 + }, + "isOptional": true + }, + { + "parameterName": "position", + "parameterTypeTokenRange": { + "startIndex": 6, + "endIndex": 7 + }, + "isOptional": true + }, + { + "parameterName": "rotation", + "parameterTypeTokenRange": { + "startIndex": 8, + "endIndex": 9 + }, + "isOptional": true } ], "isOptional": false, "isAbstract": false, - "name": "getPlayerEntitiesByPlayer" + "name": "setParent" }, { - "kind": "Property", - "canonicalReference": "server!EntityManager#world:member", - "docComment": "/**\n * The world this manager is for.\n *\n * **Category:** Entities\n */\n", + "kind": "Method", + "canonicalReference": "server!Entity#setPositionInterpolationMs:member(1)", + "docComment": "/**\n * Sets the interpolation time in milliseconds applied to position changes.\n *\n * @param interpolationMs - The interpolation time in milliseconds to set.\n *\n * **Side effects:** Emits `EntityEvent.SET_POSITION_INTERPOLATION_MS` when spawned.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get world(): " - }, - { - "kind": "Reference", - "text": "World", - "canonicalReference": "server!World:class" + "text": "setPositionInterpolationMs(interpolationMs: " }, { "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "world", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - } - ], - "implementsTokenRanges": [] - }, - { - "kind": "Class", - "canonicalReference": "server!EntityModelAnimation:class", - "docComment": "/**\n * Represents a single animation of the model used for an Entity.\n *\n * When to use: controlling individual animation playback, blending, and looping on model entities. Do NOT use for: block entities (they have no model animations).\n *\n * @remarks\n *\n * EntityModelAnimation instances are composed by an Entity and represent a single animation clip from the entity's model. Events are emitted through the parent Entity's event router and its world.\n *\n *

Events

\n *\n * Events emitted by this class are listed under `EntityModelAnimationEventPayloads`. They are emitted via the parent entity's event router.\n *\n * @example\n * ```typescript\n * const walkAnimation = entity.getModelAnimation('walk');\n * walkAnimation.setLoopMode(EntityModelAnimationLoopMode.LOOP);\n * walkAnimation.play();\n * walkAnimation.setPlaybackRate(2);\n * ```\n *\n * **Category:** Entities\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export default class EntityModelAnimation implements " - }, - { - "kind": "Reference", - "text": "protocol.Serializable", - "canonicalReference": "@hytopia.com/server-protocol!Serializable:interface" - }, - { - "kind": "Content", - "text": " " - } - ], - "fileUrlPath": "src/worlds/entities/EntityModelAnimation.ts", - "releaseTag": "Public", - "isAbstract": false, - "name": "EntityModelAnimation", - "preserveMemberOrder": false, - "members": [ - { - "kind": "Constructor", - "canonicalReference": "server!EntityModelAnimation:constructor(1)", - "docComment": "/**\n * Creates a new EntityModelAnimation instance.\n *\n * @param options - The options for the entity model animation.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ + "text": "number | undefined" + }, { "kind": "Content", - "text": "constructor(options: " + "text": "): " }, { - "kind": "Reference", - "text": "EntityModelAnimationOptions", - "canonicalReference": "server!EntityModelAnimationOptions:interface" + "kind": "Content", + "text": "void" }, { "kind": "Content", - "text": ");" + "text": ";" } ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "options", + "parameterName": "interpolationMs", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, "isOptional": false } - ] + ], + "isOptional": false, + "isAbstract": false, + "name": "setPositionInterpolationMs" }, { - "kind": "Property", - "canonicalReference": "server!EntityModelAnimation#blendMode:member", - "docComment": "/**\n * The blend mode of the entity model animation.\n *\n * **Category:** Entities\n */\n", + "kind": "Method", + "canonicalReference": "server!Entity#setRotationInterpolationMs:member(1)", + "docComment": "/**\n * Sets the interpolation time in milliseconds applied to rotation changes.\n *\n * @param interpolationMs - The interpolation time in milliseconds to set.\n *\n * **Side effects:** Emits `EntityEvent.SET_ROTATION_INTERPOLATION_MS` when spawned.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get blendMode(): " - }, - { - "kind": "Reference", - "text": "EntityModelAnimationBlendMode", - "canonicalReference": "server!EntityModelAnimationBlendMode:enum" + "text": "setRotationInterpolationMs(interpolationMs: " }, { "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "blendMode", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "server!EntityModelAnimation#clampWhenFinished:member", - "docComment": "/**\n * Whether the animation should clamp when finished, holding the last frame.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ + "text": "number | undefined" + }, { "kind": "Content", - "text": "get clampWhenFinished(): " + "text": "): " }, { "kind": "Content", - "text": "boolean" + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "clampWhenFinished", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "releaseTag": "Public", "isProtected": false, - "isAbstract": false + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "interpolationMs", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "setRotationInterpolationMs" }, { - "kind": "Property", - "canonicalReference": "server!EntityModelAnimation#entity:member", - "docComment": "/**\n * The entity that the entity model animation belongs to.\n *\n * **Category:** Entities\n */\n", + "kind": "Method", + "canonicalReference": "server!Entity#setTintColor:member(1)", + "docComment": "/**\n * Sets the tint color of the entity.\n *\n * @param tintColor - The tint color of the entity.\n *\n * **Side effects:** Emits `EntityEvent.SET_TINT_COLOR` when spawned.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get entity(): " + "text": "setTintColor(tintColor: " }, { "kind": "Reference", - "text": "Entity", - "canonicalReference": "server!Entity:class" + "text": "RgbColor", + "canonicalReference": "server!RgbColor:interface" }, { "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "entity", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "server!EntityModelAnimation#fadesIn:member", - "docComment": "/**\n * Whether the animation fades in when played or restarted.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ + "text": " | undefined" + }, { "kind": "Content", - "text": "get fadesIn(): " + "text": "): " }, { "kind": "Content", - "text": "boolean" + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "fadesIn", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 4, + "endIndex": 5 + }, + "releaseTag": "Public", "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "server!EntityModelAnimation#fadesOut:member", - "docComment": "/**\n * Whether the animation fades out when paused or stopped.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "get fadesOut(): " - }, - { - "kind": "Content", - "text": "boolean" - }, + "overloadIndex": 1, + "parameters": [ { - "kind": "Content", - "text": ";" + "parameterName": "tintColor", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + }, + "isOptional": false } ], - "isReadonly": true, "isOptional": false, - "releaseTag": "Public", - "name": "fadesOut", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "isAbstract": false, + "name": "setTintColor" }, { - "kind": "Property", - "canonicalReference": "server!EntityModelAnimation#isPaused:member", - "docComment": "/**\n * Whether the animation is currently paused.\n *\n * **Category:** Entities\n */\n", + "kind": "Method", + "canonicalReference": "server!Entity#spawn:member(1)", + "docComment": "/**\n * Spawns the entity in the world.\n *\n * Use for: placing the entity into a world so it simulates and syncs to clients. Do NOT use for: reusing a single entity instance across multiple worlds.\n *\n * @remarks\n *\n * **Rotation default:** If no rotation is provided, entity spawns with identity rotation facing -Z. For Y-axis rotation (yaw): `{ x: 0, y: sin(yaw/2), z: 0, w: cos(yaw/2) }`. Yaw 0 = facing -Z.\n *\n * **Auto-collider creation:** If no colliders are provided, a default collider is auto-generated from the model bounds (or block half extents). Set `modelPreferredShape` to `ColliderShape.NONE` to disable.\n *\n * **Collision groups:** Colliders with default collision groups are auto-assigned based on `isEnvironmental` and `isSensor` flags. Environmental entities don't collide with blocks or other environmental entities.\n *\n * **Event enabling:** Collision/contact force events are auto-enabled on colliders if listeners are registered for `BLOCK_COLLISION`, `ENTITY_COLLISION`, `BLOCK_CONTACT_FORCE`, or `ENTITY_CONTACT_FORCE` prior to spawning.\n *\n * **Controller:** If a controller is attached, `controller.spawn()` is called after the entity is added to the physics simulation.\n *\n * **Parent handling:** If `parent` was set in options, `setParent()` is called after spawn with the provided position/rotation.\n *\n * @param world - The world to spawn the entity in.\n *\n * @param position - The position to spawn the entity at.\n *\n * @param rotation - The optional rotation to spawn the entity with.\n *\n * **Requires:** Entity must not already be spawned.\n *\n * **Side effects:** Registers the entity, adds it to the simulation, and emits `EntityEvent.SPAWN`.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get isPaused(): " + "text": "spawn(world: " }, { - "kind": "Content", - "text": "boolean" + "kind": "Reference", + "text": "World", + "canonicalReference": "server!World:class" }, { "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "isPaused", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "server!EntityModelAnimation#isPlaying:member", - "docComment": "/**\n * Whether the animation is currently playing.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ + "text": ", position: " + }, { - "kind": "Content", - "text": "get isPlaying(): " + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", - "text": "boolean" + "text": ", rotation?: " }, { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "isPlaying", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "server!EntityModelAnimation#isStopped:member", - "docComment": "/**\n * Whether the animation is currently stopped.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ + "kind": "Reference", + "text": "QuaternionLike", + "canonicalReference": "server!QuaternionLike:interface" + }, { "kind": "Content", - "text": "get isStopped(): " + "text": "): " }, { "kind": "Content", - "text": "boolean" + "text": "void" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "isStopped", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 7, + "endIndex": 8 + }, + "releaseTag": "Public", "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "server!EntityModelAnimation#loopMode:member", - "docComment": "/**\n * The loop mode of the entity model animation.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ + "overloadIndex": 1, + "parameters": [ { - "kind": "Content", - "text": "get loopMode(): " + "parameterName": "world", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false }, { - "kind": "Reference", - "text": "EntityModelAnimationLoopMode", - "canonicalReference": "server!EntityModelAnimationLoopMode:enum" + "parameterName": "position", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": false }, { - "kind": "Content", - "text": ";" + "parameterName": "rotation", + "parameterTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "isOptional": true } ], - "isReadonly": true, "isOptional": false, - "releaseTag": "Public", - "name": "loopMode", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "isAbstract": false, + "name": "spawn" }, { - "kind": "Property", - "canonicalReference": "server!EntityModelAnimation#name:member", - "docComment": "/**\n * The name of the entity model animation.\n *\n * @remarks\n *\n * This is the name of the animation as defined in the model.\n *\n * **Category:** Entities\n */\n", + "kind": "Method", + "canonicalReference": "server!Entity#startModelLoopedAnimations:member(1)", + "docComment": "/**\n * Starts looped animations by name on this entity's model.\n *\n * @param names - Animation names to start looping.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get name(): " + "text": "startModelLoopedAnimations(names: " }, { "kind": "Content", - "text": "string" + "text": "readonly string[]" }, { "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "name", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Method", - "canonicalReference": "server!EntityModelAnimation#pause:member(1)", - "docComment": "/**\n * Pauses the entity model animation, does nothing if already paused.\n *\n * **Side effects:** Emits `EntityModelAnimationEvent.PAUSE` when spawned.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "pause(): " + "text": "): " }, { "kind": "Content", @@ -19754,25 +19389,42 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 + "startIndex": 3, + "endIndex": 4 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, - "parameters": [], + "parameters": [ + { + "parameterName": "names", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], "isOptional": false, "isAbstract": false, - "name": "pause" + "name": "startModelLoopedAnimations" }, { "kind": "Method", - "canonicalReference": "server!EntityModelAnimation#play:member(1)", - "docComment": "/**\n * Plays the entity model animation, does nothing if already playing.\n *\n * **Side effects:** Emits `EntityModelAnimationEvent.PLAY` when spawned.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!Entity#startModelOneshotAnimations:member(1)", + "docComment": "/**\n * Starts one-shot animations by name on this entity's model.\n *\n * @param names - Animation names to play once.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "play(): " + "text": "startModelOneshotAnimations(names: " + }, + { + "kind": "Content", + "text": "readonly string[]" + }, + { + "kind": "Content", + "text": "): " }, { "kind": "Content", @@ -19785,139 +19437,56 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 + "startIndex": 3, + "endIndex": 4 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, - "parameters": [], + "parameters": [ + { + "parameterName": "names", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], "isOptional": false, "isAbstract": false, - "name": "play" + "name": "startModelOneshotAnimations" }, { - "kind": "Property", - "canonicalReference": "server!EntityModelAnimation#playbackRate:member", - "docComment": "/**\n * The playback rate of the entity model animation.\n *\n * **Category:** Entities\n */\n", + "kind": "Method", + "canonicalReference": "server!Entity#stopAllModelAnimations:member(1)", + "docComment": "/**\n * Stops all model animations for the entity, optionally excluding the provided animations from stopping.\n *\n * @param exclusionFilter - The filter to determine if a model animation should be excluded from being stopped.\n *\n * **Side effects:** May emit `EntityModelAnimationEvent.STOP` for each stopped animation.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get playbackRate(): " - }, - { - "kind": "Content", - "text": "number" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "playbackRate", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Method", - "canonicalReference": "server!EntityModelAnimation#restart:member(1)", - "docComment": "/**\n * Restarts the entity model animation from the beginning.\n *\n * @remarks\n *\n * Unlike `play()`, this always emits even if the animation is already playing, allowing the animation to restart from the beginning.\n *\n * **Side effects:** Emits `EntityModelAnimationEvent.RESTART` when spawned.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "restart(): " - }, - { - "kind": "Content", - "text": "void" + "text": "stopAllModelAnimations(exclusionFilter?: " }, { "kind": "Content", - "text": ";" - } - ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [], - "isOptional": false, - "isAbstract": false, - "name": "restart" - }, - { - "kind": "Method", - "canonicalReference": "server!EntityModelAnimation#setBlendMode:member(1)", - "docComment": "/**\n * Sets the blend mode of the entity model animation.\n *\n * @param blendMode - The blend mode of the entity model animation.\n *\n * **Side effects:** Emits `EntityModelAnimationEvent.SET_BLEND_MODE` when spawned.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "setBlendMode(blendMode: " + "text": "(modelAnimation: " }, { "kind": "Reference", - "text": "EntityModelAnimationBlendMode", - "canonicalReference": "server!EntityModelAnimationBlendMode:enum" - }, - { - "kind": "Content", - "text": "): " + "text": "Readonly", + "canonicalReference": "!Readonly:type" }, { "kind": "Content", - "text": "void" + "text": "<" }, { - "kind": "Content", - "text": ";" - } - ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "blendMode", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "setBlendMode" - }, - { - "kind": "Method", - "canonicalReference": "server!EntityModelAnimation#setClampWhenFinished:member(1)", - "docComment": "/**\n * Sets whether the animation should clamp when finished, holding the last frame.\n *\n * @param clampWhenFinished - Whether to clamp when finished.\n *\n * **Side effects:** Emits `EntityModelAnimationEvent.SET_CLAMP_WHEN_FINISHED` when spawned.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "setClampWhenFinished(clampWhenFinished: " + "kind": "Reference", + "text": "EntityModelAnimation", + "canonicalReference": "server!EntityModelAnimation:class" }, { "kind": "Content", - "text": "boolean" + "text": ">) => boolean" }, { "kind": "Content", @@ -19934,38 +19503,38 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 + "startIndex": 7, + "endIndex": 8 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, "parameters": [ { - "parameterName": "clampWhenFinished", + "parameterName": "exclusionFilter", "parameterTypeTokenRange": { "startIndex": 1, - "endIndex": 2 + "endIndex": 6 }, - "isOptional": false + "isOptional": true } ], "isOptional": false, "isAbstract": false, - "name": "setClampWhenFinished" + "name": "stopAllModelAnimations" }, { "kind": "Method", - "canonicalReference": "server!EntityModelAnimation#setFadesIn:member(1)", - "docComment": "/**\n * Sets whether the animation fades in when played or restarted.\n *\n * @param fadesIn - Whether the animation should fade in when played or restarted.\n *\n * **Side effects:** Emits `EntityModelAnimationEvent.SET_FADES_IN` when spawned.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!Entity#stopModelAnimations:member(1)", + "docComment": "/**\n * Stops the provided model animations for the entity.\n *\n * @param modelAnimationNames - The model animation names to stop.\n *\n * **Side effects:** May emit `EntityModelAnimationEvent.STOP` for each stopped animation.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setFadesIn(fadesIn: " + "text": "stopModelAnimations(modelAnimationNames: " }, { "kind": "Content", - "text": "boolean" + "text": "readonly string[]" }, { "kind": "Content", @@ -19990,7 +19559,7 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "fadesIn", + "parameterName": "modelAnimationNames", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -20000,244 +19569,120 @@ ], "isOptional": false, "isAbstract": false, - "name": "setFadesIn" + "name": "stopModelAnimations" }, { - "kind": "Method", - "canonicalReference": "server!EntityModelAnimation#setFadesOut:member(1)", - "docComment": "/**\n * Sets whether the animation fades out when paused or stopped.\n *\n * @param fadesOut - Whether the animation should fade out when paused or stopped.\n *\n * **Side effects:** Emits `EntityModelAnimationEvent.SET_FADES_OUT` when spawned.\n *\n * **Category:** Entities\n */\n", + "kind": "Property", + "canonicalReference": "server!Entity#tag:member", + "docComment": "/**\n * An arbitrary identifier tag for your own logic.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setFadesOut(fadesOut: " - }, - { - "kind": "Content", - "text": "boolean" - }, - { - "kind": "Content", - "text": "): " + "text": "get tag(): " }, { "kind": "Content", - "text": "void" + "text": "string | undefined" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, + "isReadonly": true, + "isOptional": false, "releaseTag": "Public", + "name": "tag", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "fadesOut", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "setFadesOut" + "isAbstract": false }, { - "kind": "Method", - "canonicalReference": "server!EntityModelAnimation#setLoopMode:member(1)", - "docComment": "/**\n * Sets the loop mode of the entity model animation.\n *\n * @param loopMode - The loop mode of the entity model animation.\n *\n * **Side effects:** Emits `EntityModelAnimationEvent.SET_LOOP_MODE` when spawned.\n *\n * **Category:** Entities\n */\n", + "kind": "Property", + "canonicalReference": "server!Entity#tintColor:member", + "docComment": "/**\n * The tint color of the entity.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setLoopMode(loopMode: " + "text": "get tintColor(): " }, { "kind": "Reference", - "text": "EntityModelAnimationLoopMode", - "canonicalReference": "server!EntityModelAnimationLoopMode:enum" - }, - { - "kind": "Content", - "text": "): " + "text": "RgbColor", + "canonicalReference": "server!RgbColor:interface" }, { "kind": "Content", - "text": "void" + "text": " | undefined" }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "loopMode", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], + "isReadonly": true, "isOptional": false, - "isAbstract": false, - "name": "setLoopMode" - }, - { - "kind": "Method", - "canonicalReference": "server!EntityModelAnimation#setPlaybackRate:member(1)", - "docComment": "/**\n * Sets the playback rate of the entity model animation.\n *\n * @remarks\n *\n * A positive value plays the animation forward, a negative value plays it in reverse. Defaults to 1.\n *\n * @param playbackRate - The playback rate of the entity model animation.\n *\n * **Side effects:** Emits `EntityModelAnimationEvent.SET_PLAYBACK_RATE` when spawned.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "setPlaybackRate(playbackRate: " - }, - { - "kind": "Content", - "text": "number" - }, - { - "kind": "Content", - "text": "): " - }, - { - "kind": "Content", - "text": "void" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, "releaseTag": "Public", + "name": "tintColor", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + }, + "isStatic": false, "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "playbackRate", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "setPlaybackRate" + "isAbstract": false }, { - "kind": "Method", - "canonicalReference": "server!EntityModelAnimation#setWeight:member(1)", - "docComment": "/**\n * Sets the weight of the entity model animation for blending with other playing animations.\n *\n * @param weight - The weight of the entity model animation.\n *\n * **Side effects:** Emits `EntityModelAnimationEvent.SET_WEIGHT` when spawned.\n *\n * **Category:** Entities\n */\n", + "kind": "Property", + "canonicalReference": "server!Entity#width:member", + "docComment": "/**\n * The width (X-axis) of the entity's model or block size.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setWeight(weight: " + "text": "get width(): " }, { "kind": "Content", "text": "number" }, - { - "kind": "Content", - "text": "): " - }, - { - "kind": "Content", - "text": "void" - }, { "kind": "Content", "text": ";" } ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "weight", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], + "isReadonly": true, "isOptional": false, - "isAbstract": false, - "name": "setWeight" - }, - { - "kind": "Method", - "canonicalReference": "server!EntityModelAnimation#stop:member(1)", - "docComment": "/**\n * Stops the entity model animation, does nothing if already stopped.\n *\n * **Side effects:** Emits `EntityModelAnimationEvent.STOP` when spawned.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "stop(): " - }, - { - "kind": "Content", - "text": "void" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isStatic": false, - "returnTypeTokenRange": { + "releaseTag": "Public", + "name": "width", + "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "releaseTag": "Public", + "isStatic": false, "isProtected": false, - "overloadIndex": 1, - "parameters": [], - "isOptional": false, - "isAbstract": false, - "name": "stop" + "isAbstract": false }, { "kind": "Property", - "canonicalReference": "server!EntityModelAnimation#weight:member", - "docComment": "/**\n * The weight of the entity model animation.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!Entity#world:member", + "docComment": "/**\n * The world the entity is in, if spawned.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get weight(): " + "text": "get world(): " + }, + { + "kind": "Reference", + "text": "World", + "canonicalReference": "server!World:class" }, { "kind": "Content", - "text": "number" + "text": " | undefined" }, { "kind": "Content", @@ -20247,50 +19692,54 @@ "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "weight", + "name": "world", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 2 + "endIndex": 3 }, "isStatic": false, "isProtected": false, "isAbstract": false } ], + "extendsTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, "implementsTokenRanges": [ { - "startIndex": 1, - "endIndex": 2 + "startIndex": 3, + "endIndex": 4 } ] }, { "kind": "Enum", - "canonicalReference": "server!EntityModelAnimationBlendMode:enum", - "docComment": "/**\n * The blend mode of an entity model animation.\n *\n * **Category:** Entities\n *\n * @public\n */\n", + "canonicalReference": "server!EntityEvent:enum", + "docComment": "/**\n * Event types an Entity instance can emit.\n *\n * See `EntityEventPayloads` for the payloads.\n *\n * **Category:** Events\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "export declare enum EntityModelAnimationBlendMode " + "text": "export declare enum EntityEvent " } ], - "fileUrlPath": "src/worlds/entities/EntityModelAnimation.ts", + "fileUrlPath": "src/worlds/entities/Entity.ts", "releaseTag": "Public", - "name": "EntityModelAnimationBlendMode", + "name": "EntityEvent", "preserveMemberOrder": false, "members": [ { "kind": "EnumMember", - "canonicalReference": "server!EntityModelAnimationBlendMode.ADDITIVE:member", + "canonicalReference": "server!EntityEvent.BLOCK_COLLISION:member", "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "ADDITIVE = " + "text": "BLOCK_COLLISION = " }, { "kind": "Content", - "text": "0" + "text": "\"ENTITY.BLOCK_COLLISION\"" } ], "initializerTokenRange": { @@ -20298,20 +19747,20 @@ "endIndex": 2 }, "releaseTag": "Public", - "name": "ADDITIVE" + "name": "BLOCK_COLLISION" }, { "kind": "EnumMember", - "canonicalReference": "server!EntityModelAnimationBlendMode.NORMAL:member", + "canonicalReference": "server!EntityEvent.BLOCK_CONTACT_FORCE:member", "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "NORMAL = " + "text": "BLOCK_CONTACT_FORCE = " }, { "kind": "Content", - "text": "1" + "text": "\"ENTITY.BLOCK_CONTACT_FORCE\"" } ], "initializerTokenRange": { @@ -20319,37 +19768,20 @@ "endIndex": 2 }, "releaseTag": "Public", - "name": "NORMAL" - } - ] - }, - { - "kind": "Enum", - "canonicalReference": "server!EntityModelAnimationEvent:enum", - "docComment": "/**\n * Event types an EntityModelAnimation instance can emit.\n *\n * See `EntityModelAnimationEventPayloads` for the payloads.\n *\n * **Category:** Events\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export declare enum EntityModelAnimationEvent " - } - ], - "fileUrlPath": "src/worlds/entities/EntityModelAnimation.ts", - "releaseTag": "Public", - "name": "EntityModelAnimationEvent", - "preserveMemberOrder": false, - "members": [ + "name": "BLOCK_CONTACT_FORCE" + }, { "kind": "EnumMember", - "canonicalReference": "server!EntityModelAnimationEvent.PAUSE:member", + "canonicalReference": "server!EntityEvent.DESPAWN:member", "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "PAUSE = " + "text": "DESPAWN = " }, { "kind": "Content", - "text": "\"ENTITY_MODEL_ANIMATION.PAUSE\"" + "text": "\"ENTITY.DESPAWN\"" } ], "initializerTokenRange": { @@ -20357,20 +19789,20 @@ "endIndex": 2 }, "releaseTag": "Public", - "name": "PAUSE" + "name": "DESPAWN" }, { "kind": "EnumMember", - "canonicalReference": "server!EntityModelAnimationEvent.PLAY:member", + "canonicalReference": "server!EntityEvent.ENTITY_COLLISION:member", "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "PLAY = " + "text": "ENTITY_COLLISION = " }, { "kind": "Content", - "text": "\"ENTITY_MODEL_ANIMATION.PLAY\"" + "text": "\"ENTITY.ENTITY_COLLISION\"" } ], "initializerTokenRange": { @@ -20378,20 +19810,20 @@ "endIndex": 2 }, "releaseTag": "Public", - "name": "PLAY" + "name": "ENTITY_COLLISION" }, { "kind": "EnumMember", - "canonicalReference": "server!EntityModelAnimationEvent.RESTART:member", + "canonicalReference": "server!EntityEvent.ENTITY_CONTACT_FORCE:member", "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "RESTART = " + "text": "ENTITY_CONTACT_FORCE = " }, { "kind": "Content", - "text": "\"ENTITY_MODEL_ANIMATION.RESTART\"" + "text": "\"ENTITY.ENTITY_CONTACT_FORCE\"" } ], "initializerTokenRange": { @@ -20399,20 +19831,20 @@ "endIndex": 2 }, "releaseTag": "Public", - "name": "RESTART" + "name": "ENTITY_CONTACT_FORCE" }, { "kind": "EnumMember", - "canonicalReference": "server!EntityModelAnimationEvent.SET_BLEND_MODE:member", + "canonicalReference": "server!EntityEvent.INTERACT:member", "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "SET_BLEND_MODE = " + "text": "INTERACT = " }, { "kind": "Content", - "text": "\"ENTITY_MODEL_ANIMATION.SET_BLEND_MODE\"" + "text": "\"ENTITY.INTERACT\"" } ], "initializerTokenRange": { @@ -20420,20 +19852,20 @@ "endIndex": 2 }, "releaseTag": "Public", - "name": "SET_BLEND_MODE" + "name": "INTERACT" }, { "kind": "EnumMember", - "canonicalReference": "server!EntityModelAnimationEvent.SET_CLAMP_WHEN_FINISHED:member", + "canonicalReference": "server!EntityEvent.REMOVE_MODEL_NODE_OVERRIDE:member", "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "SET_CLAMP_WHEN_FINISHED = " + "text": "REMOVE_MODEL_NODE_OVERRIDE = " }, { "kind": "Content", - "text": "\"ENTITY_MODEL_ANIMATION.SET_CLAMP_WHEN_FINISHED\"" + "text": "\"ENTITY.REMOVE_MODEL_NODE_OVERRIDE\"" } ], "initializerTokenRange": { @@ -20441,20 +19873,20 @@ "endIndex": 2 }, "releaseTag": "Public", - "name": "SET_CLAMP_WHEN_FINISHED" + "name": "REMOVE_MODEL_NODE_OVERRIDE" }, { "kind": "EnumMember", - "canonicalReference": "server!EntityModelAnimationEvent.SET_FADES_IN:member", + "canonicalReference": "server!EntityEvent.SET_BLOCK_TEXTURE_URI:member", "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "SET_FADES_IN = " + "text": "SET_BLOCK_TEXTURE_URI = " }, { "kind": "Content", - "text": "\"ENTITY_MODEL_ANIMATION.SET_FADES_IN\"" + "text": "\"ENTITY.SET_BLOCK_TEXTURE_URI\"" } ], "initializerTokenRange": { @@ -20462,20 +19894,20 @@ "endIndex": 2 }, "releaseTag": "Public", - "name": "SET_FADES_IN" + "name": "SET_BLOCK_TEXTURE_URI" }, { "kind": "EnumMember", - "canonicalReference": "server!EntityModelAnimationEvent.SET_FADES_OUT:member", + "canonicalReference": "server!EntityEvent.SET_EMISSIVE_COLOR:member", "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "SET_FADES_OUT = " + "text": "SET_EMISSIVE_COLOR = " }, { "kind": "Content", - "text": "\"ENTITY_MODEL_ANIMATION.SET_FADES_OUT\"" + "text": "\"ENTITY.SET_EMISSIVE_COLOR\"" } ], "initializerTokenRange": { @@ -20483,20 +19915,20 @@ "endIndex": 2 }, "releaseTag": "Public", - "name": "SET_FADES_OUT" + "name": "SET_EMISSIVE_COLOR" }, { "kind": "EnumMember", - "canonicalReference": "server!EntityModelAnimationEvent.SET_LOOP_MODE:member", + "canonicalReference": "server!EntityEvent.SET_EMISSIVE_INTENSITY:member", "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "SET_LOOP_MODE = " + "text": "SET_EMISSIVE_INTENSITY = " }, { "kind": "Content", - "text": "\"ENTITY_MODEL_ANIMATION.SET_LOOP_MODE\"" + "text": "\"ENTITY.SET_EMISSIVE_INTENSITY\"" } ], "initializerTokenRange": { @@ -20504,20 +19936,20 @@ "endIndex": 2 }, "releaseTag": "Public", - "name": "SET_LOOP_MODE" + "name": "SET_EMISSIVE_INTENSITY" }, { "kind": "EnumMember", - "canonicalReference": "server!EntityModelAnimationEvent.SET_PLAYBACK_RATE:member", + "canonicalReference": "server!EntityEvent.SET_MODEL_SCALE:member", "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "SET_PLAYBACK_RATE = " + "text": "SET_MODEL_SCALE = " }, { "kind": "Content", - "text": "\"ENTITY_MODEL_ANIMATION.SET_PLAYBACK_RATE\"" + "text": "\"ENTITY.SET_MODEL_SCALE\"" } ], "initializerTokenRange": { @@ -20525,20 +19957,20 @@ "endIndex": 2 }, "releaseTag": "Public", - "name": "SET_PLAYBACK_RATE" + "name": "SET_MODEL_SCALE" }, { "kind": "EnumMember", - "canonicalReference": "server!EntityModelAnimationEvent.SET_WEIGHT:member", + "canonicalReference": "server!EntityEvent.SET_MODEL_SCALE_INTERPOLATION_MS:member", "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "SET_WEIGHT = " + "text": "SET_MODEL_SCALE_INTERPOLATION_MS = " }, { "kind": "Content", - "text": "\"ENTITY_MODEL_ANIMATION.SET_WEIGHT\"" + "text": "\"ENTITY.SET_MODEL_SCALE_INTERPOLATION_MS\"" } ], "initializerTokenRange": { @@ -20546,20 +19978,20 @@ "endIndex": 2 }, "releaseTag": "Public", - "name": "SET_WEIGHT" + "name": "SET_MODEL_SCALE_INTERPOLATION_MS" }, { "kind": "EnumMember", - "canonicalReference": "server!EntityModelAnimationEvent.STOP:member", + "canonicalReference": "server!EntityEvent.SET_MODEL_TEXTURE_URI:member", "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "STOP = " + "text": "SET_MODEL_TEXTURE_URI = " }, { "kind": "Content", - "text": "\"ENTITY_MODEL_ANIMATION.STOP\"" + "text": "\"ENTITY.SET_MODEL_TEXTURE_URI\"" } ], "initializerTokenRange": { @@ -20567,74 +19999,239 @@ "endIndex": 2 }, "releaseTag": "Public", - "name": "STOP" - } - ] - }, - { - "kind": "Interface", - "canonicalReference": "server!EntityModelAnimationEventPayloads:interface", - "docComment": "/**\n * Event payloads for EntityModelAnimation emitted events.\n *\n * **Category:** Events\n *\n * @public\n */\n", - "excerptTokens": [ + "name": "SET_MODEL_TEXTURE_URI" + }, { - "kind": "Content", - "text": "export interface EntityModelAnimationEventPayloads " - } - ], - "fileUrlPath": "src/worlds/entities/EntityModelAnimation.ts", - "releaseTag": "Public", - "name": "EntityModelAnimationEventPayloads", - "preserveMemberOrder": false, - "members": [ + "kind": "EnumMember", + "canonicalReference": "server!EntityEvent.SET_OPACITY:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "SET_OPACITY = " + }, + { + "kind": "Content", + "text": "\"ENTITY.SET_OPACITY\"" + } + ], + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "SET_OPACITY" + }, { - "kind": "PropertySignature", - "canonicalReference": "server!EntityModelAnimationEventPayloads#\"ENTITY_MODEL_ANIMATION.PAUSE\":member", - "docComment": "/**\n * Emitted when an entity model animation is paused.\n */\n", + "kind": "EnumMember", + "canonicalReference": "server!EntityEvent.SET_OUTLINE:member", + "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "[" + "text": "SET_OUTLINE = " }, { - "kind": "Reference", - "text": "EntityModelAnimationEvent.PAUSE", - "canonicalReference": "server!EntityModelAnimationEvent.PAUSE:member" + "kind": "Content", + "text": "\"ENTITY.SET_OUTLINE\"" + } + ], + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "SET_OUTLINE" + }, + { + "kind": "EnumMember", + "canonicalReference": "server!EntityEvent.SET_PARENT:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "SET_PARENT = " }, { "kind": "Content", - "text": "]: " + "text": "\"ENTITY.SET_PARENT\"" + } + ], + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "SET_PARENT" + }, + { + "kind": "EnumMember", + "canonicalReference": "server!EntityEvent.SET_POSITION_INTERPOLATION_MS:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "SET_POSITION_INTERPOLATION_MS = " }, { "kind": "Content", - "text": "{\n entityModelAnimation: " + "text": "\"ENTITY.SET_POSITION_INTERPOLATION_MS\"" + } + ], + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "SET_POSITION_INTERPOLATION_MS" + }, + { + "kind": "EnumMember", + "canonicalReference": "server!EntityEvent.SET_ROTATION_INTERPOLATION_MS:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "SET_ROTATION_INTERPOLATION_MS = " }, { - "kind": "Reference", - "text": "EntityModelAnimation", - "canonicalReference": "server!EntityModelAnimation:class" + "kind": "Content", + "text": "\"ENTITY.SET_ROTATION_INTERPOLATION_MS\"" + } + ], + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "SET_ROTATION_INTERPOLATION_MS" + }, + { + "kind": "EnumMember", + "canonicalReference": "server!EntityEvent.SET_TINT_COLOR:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "SET_TINT_COLOR = " }, { "kind": "Content", - "text": ";\n }" + "text": "\"ENTITY.SET_TINT_COLOR\"" + } + ], + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "SET_TINT_COLOR" + }, + { + "kind": "EnumMember", + "canonicalReference": "server!EntityEvent.SPAWN:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "SPAWN = " }, { "kind": "Content", - "text": ";" + "text": "\"ENTITY.SPAWN\"" } ], - "isReadonly": false, - "isOptional": false, + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, "releaseTag": "Public", - "name": "\"ENTITY_MODEL_ANIMATION.PAUSE\"", - "propertyTypeTokenRange": { - "startIndex": 3, - "endIndex": 6 - } + "name": "SPAWN" + }, + { + "kind": "EnumMember", + "canonicalReference": "server!EntityEvent.TICK:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "TICK = " + }, + { + "kind": "Content", + "text": "\"ENTITY.TICK\"" + } + ], + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "TICK" + }, + { + "kind": "EnumMember", + "canonicalReference": "server!EntityEvent.UPDATE_POSITION:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "UPDATE_POSITION = " + }, + { + "kind": "Content", + "text": "\"ENTITY.UPDATE_POSITION\"" + } + ], + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "UPDATE_POSITION" }, + { + "kind": "EnumMember", + "canonicalReference": "server!EntityEvent.UPDATE_ROTATION:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "UPDATE_ROTATION = " + }, + { + "kind": "Content", + "text": "\"ENTITY.UPDATE_ROTATION\"" + } + ], + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "UPDATE_ROTATION" + } + ] + }, + { + "kind": "Interface", + "canonicalReference": "server!EntityEventPayloads:interface", + "docComment": "/**\n * Event payloads for Entity emitted events.\n *\n * **Category:** Events\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export interface EntityEventPayloads " + } + ], + "fileUrlPath": "src/worlds/entities/Entity.ts", + "releaseTag": "Public", + "name": "EntityEventPayloads", + "preserveMemberOrder": false, + "members": [ { "kind": "PropertySignature", - "canonicalReference": "server!EntityModelAnimationEventPayloads#\"ENTITY_MODEL_ANIMATION.PLAY\":member", - "docComment": "/**\n * Emitted when an entity model animation is played.\n */\n", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.BLOCK_COLLISION\":member", + "docComment": "/**\n * Emitted when an entity collides with a block type.\n */\n", "excerptTokens": [ { "kind": "Content", @@ -20642,8 +20239,8 @@ }, { "kind": "Reference", - "text": "EntityModelAnimationEvent.PLAY", - "canonicalReference": "server!EntityModelAnimationEvent.PLAY:member" + "text": "EntityEvent.BLOCK_COLLISION", + "canonicalReference": "server!EntityEvent.BLOCK_COLLISION:member" }, { "kind": "Content", @@ -20651,16 +20248,25 @@ }, { "kind": "Content", - "text": "{\n entityModelAnimation: " + "text": "{\n entity: " }, { "kind": "Reference", - "text": "EntityModelAnimation", - "canonicalReference": "server!EntityModelAnimation:class" + "text": "Entity", + "canonicalReference": "server!Entity:class" }, { "kind": "Content", - "text": ";\n }" + "text": ";\n blockType: " + }, + { + "kind": "Reference", + "text": "BlockType", + "canonicalReference": "server!BlockType:class" + }, + { + "kind": "Content", + "text": ";\n started: boolean;\n colliderHandleA: number;\n colliderHandleB: number;\n }" }, { "kind": "Content", @@ -20670,16 +20276,16 @@ "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "\"ENTITY_MODEL_ANIMATION.PLAY\"", + "name": "\"ENTITY.BLOCK_COLLISION\"", "propertyTypeTokenRange": { "startIndex": 3, - "endIndex": 6 + "endIndex": 8 } }, { "kind": "PropertySignature", - "canonicalReference": "server!EntityModelAnimationEventPayloads#\"ENTITY_MODEL_ANIMATION.RESTART\":member", - "docComment": "/**\n * Emitted when an entity model animation is restarted.\n */\n", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.BLOCK_CONTACT_FORCE\":member", + "docComment": "/**\n * Emitted when an entity's contact force is applied to a block type.\n */\n", "excerptTokens": [ { "kind": "Content", @@ -20687,8 +20293,8 @@ }, { "kind": "Reference", - "text": "EntityModelAnimationEvent.RESTART", - "canonicalReference": "server!EntityModelAnimationEvent.RESTART:member" + "text": "EntityEvent.BLOCK_CONTACT_FORCE", + "canonicalReference": "server!EntityEvent.BLOCK_CONTACT_FORCE:member" }, { "kind": "Content", @@ -20696,12 +20302,30 @@ }, { "kind": "Content", - "text": "{\n entityModelAnimation: " + "text": "{\n entity: " }, { "kind": "Reference", - "text": "EntityModelAnimation", - "canonicalReference": "server!EntityModelAnimation:class" + "text": "Entity", + "canonicalReference": "server!Entity:class" + }, + { + "kind": "Content", + "text": ";\n blockType: " + }, + { + "kind": "Reference", + "text": "BlockType", + "canonicalReference": "server!BlockType:class" + }, + { + "kind": "Content", + "text": ";\n contactForceData: " + }, + { + "kind": "Reference", + "text": "ContactForceData", + "canonicalReference": "server!ContactForceData:type" }, { "kind": "Content", @@ -20715,16 +20339,16 @@ "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "\"ENTITY_MODEL_ANIMATION.RESTART\"", + "name": "\"ENTITY.BLOCK_CONTACT_FORCE\"", "propertyTypeTokenRange": { "startIndex": 3, - "endIndex": 6 + "endIndex": 10 } }, { "kind": "PropertySignature", - "canonicalReference": "server!EntityModelAnimationEventPayloads#\"ENTITY_MODEL_ANIMATION.SET_BLEND_MODE\":member", - "docComment": "/**\n * Emitted when the blend mode of an entity model animation is set.\n */\n", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.DESPAWN\":member", + "docComment": "/**\n * Emitted when an entity is despawned.\n */\n", "excerptTokens": [ { "kind": "Content", @@ -20732,8 +20356,8 @@ }, { "kind": "Reference", - "text": "EntityModelAnimationEvent.SET_BLEND_MODE", - "canonicalReference": "server!EntityModelAnimationEvent.SET_BLEND_MODE:member" + "text": "EntityEvent.DESPAWN", + "canonicalReference": "server!EntityEvent.DESPAWN:member" }, { "kind": "Content", @@ -20741,21 +20365,12 @@ }, { "kind": "Content", - "text": "{\n entityModelAnimation: " - }, - { - "kind": "Reference", - "text": "EntityModelAnimation", - "canonicalReference": "server!EntityModelAnimation:class" - }, - { - "kind": "Content", - "text": ";\n blendMode: " + "text": "{\n entity: " }, { "kind": "Reference", - "text": "EntityModelAnimationBlendMode", - "canonicalReference": "server!EntityModelAnimationBlendMode:enum" + "text": "Entity", + "canonicalReference": "server!Entity:class" }, { "kind": "Content", @@ -20769,16 +20384,16 @@ "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "\"ENTITY_MODEL_ANIMATION.SET_BLEND_MODE\"", + "name": "\"ENTITY.DESPAWN\"", "propertyTypeTokenRange": { "startIndex": 3, - "endIndex": 8 + "endIndex": 6 } }, { "kind": "PropertySignature", - "canonicalReference": "server!EntityModelAnimationEventPayloads#\"ENTITY_MODEL_ANIMATION.SET_CLAMP_WHEN_FINISHED\":member", - "docComment": "/**\n * Emitted when the clamp when finished setting of an entity model animation is set.\n */\n", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.ENTITY_COLLISION\":member", + "docComment": "/**\n * Emitted when an entity collides with another entity.\n */\n", "excerptTokens": [ { "kind": "Content", @@ -20786,8 +20401,8 @@ }, { "kind": "Reference", - "text": "EntityModelAnimationEvent.SET_CLAMP_WHEN_FINISHED", - "canonicalReference": "server!EntityModelAnimationEvent.SET_CLAMP_WHEN_FINISHED:member" + "text": "EntityEvent.ENTITY_COLLISION", + "canonicalReference": "server!EntityEvent.ENTITY_COLLISION:member" }, { "kind": "Content", @@ -20795,16 +20410,25 @@ }, { "kind": "Content", - "text": "{\n entityModelAnimation: " + "text": "{\n entity: " }, { "kind": "Reference", - "text": "EntityModelAnimation", - "canonicalReference": "server!EntityModelAnimation:class" + "text": "Entity", + "canonicalReference": "server!Entity:class" }, { "kind": "Content", - "text": ";\n clampWhenFinished: boolean;\n }" + "text": ";\n otherEntity: " + }, + { + "kind": "Reference", + "text": "Entity", + "canonicalReference": "server!Entity:class" + }, + { + "kind": "Content", + "text": ";\n started: boolean;\n colliderHandleA: number;\n colliderHandleB: number;\n }" }, { "kind": "Content", @@ -20814,16 +20438,16 @@ "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "\"ENTITY_MODEL_ANIMATION.SET_CLAMP_WHEN_FINISHED\"", + "name": "\"ENTITY.ENTITY_COLLISION\"", "propertyTypeTokenRange": { "startIndex": 3, - "endIndex": 6 + "endIndex": 8 } }, { "kind": "PropertySignature", - "canonicalReference": "server!EntityModelAnimationEventPayloads#\"ENTITY_MODEL_ANIMATION.SET_FADES_IN\":member", - "docComment": "/**\n * Emitted when the fade in behavior of an entity model animation is set.\n */\n", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.ENTITY_CONTACT_FORCE\":member", + "docComment": "/**\n * Emitted when an entity's contact force is applied to another entity.\n */\n", "excerptTokens": [ { "kind": "Content", @@ -20831,8 +20455,8 @@ }, { "kind": "Reference", - "text": "EntityModelAnimationEvent.SET_FADES_IN", - "canonicalReference": "server!EntityModelAnimationEvent.SET_FADES_IN:member" + "text": "EntityEvent.ENTITY_CONTACT_FORCE", + "canonicalReference": "server!EntityEvent.ENTITY_CONTACT_FORCE:member" }, { "kind": "Content", @@ -20840,16 +20464,34 @@ }, { "kind": "Content", - "text": "{\n entityModelAnimation: " + "text": "{\n entity: " }, { "kind": "Reference", - "text": "EntityModelAnimation", - "canonicalReference": "server!EntityModelAnimation:class" + "text": "Entity", + "canonicalReference": "server!Entity:class" }, { "kind": "Content", - "text": ";\n fadesIn: boolean;\n }" + "text": ";\n otherEntity: " + }, + { + "kind": "Reference", + "text": "Entity", + "canonicalReference": "server!Entity:class" + }, + { + "kind": "Content", + "text": ";\n contactForceData: " + }, + { + "kind": "Reference", + "text": "ContactForceData", + "canonicalReference": "server!ContactForceData:type" + }, + { + "kind": "Content", + "text": ";\n }" }, { "kind": "Content", @@ -20859,16 +20501,16 @@ "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "\"ENTITY_MODEL_ANIMATION.SET_FADES_IN\"", + "name": "\"ENTITY.ENTITY_CONTACT_FORCE\"", "propertyTypeTokenRange": { "startIndex": 3, - "endIndex": 6 + "endIndex": 10 } }, { "kind": "PropertySignature", - "canonicalReference": "server!EntityModelAnimationEventPayloads#\"ENTITY_MODEL_ANIMATION.SET_FADES_OUT\":member", - "docComment": "/**\n * Emitted when the fade out behavior of an entity model animation is set.\n */\n", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.INTERACT\":member", + "docComment": "/**\n * Emitted when a player interacts with the entity by clicking or tapping it.\n */\n", "excerptTokens": [ { "kind": "Content", @@ -20876,8 +20518,8 @@ }, { "kind": "Reference", - "text": "EntityModelAnimationEvent.SET_FADES_OUT", - "canonicalReference": "server!EntityModelAnimationEvent.SET_FADES_OUT:member" + "text": "EntityEvent.INTERACT", + "canonicalReference": "server!EntityEvent.INTERACT:member" }, { "kind": "Content", @@ -20885,16 +20527,34 @@ }, { "kind": "Content", - "text": "{\n entityModelAnimation: " + "text": "{\n entity: " }, { "kind": "Reference", - "text": "EntityModelAnimation", - "canonicalReference": "server!EntityModelAnimation:class" + "text": "Entity", + "canonicalReference": "server!Entity:class" }, { "kind": "Content", - "text": ";\n fadesOut: boolean;\n }" + "text": ";\n player: " + }, + { + "kind": "Reference", + "text": "Player", + "canonicalReference": "server!Player:class" + }, + { + "kind": "Content", + "text": ";\n raycastHit?: " + }, + { + "kind": "Reference", + "text": "RaycastHit", + "canonicalReference": "server!RaycastHit:type" + }, + { + "kind": "Content", + "text": ";\n }" }, { "kind": "Content", @@ -20904,16 +20564,16 @@ "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "\"ENTITY_MODEL_ANIMATION.SET_FADES_OUT\"", + "name": "\"ENTITY.INTERACT\"", "propertyTypeTokenRange": { "startIndex": 3, - "endIndex": 6 + "endIndex": 10 } }, { "kind": "PropertySignature", - "canonicalReference": "server!EntityModelAnimationEventPayloads#\"ENTITY_MODEL_ANIMATION.SET_LOOP_MODE\":member", - "docComment": "/**\n * Emitted when the loop mode of an entity model animation is set.\n */\n", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.REMOVE_MODEL_NODE_OVERRIDE\":member", + "docComment": "/**\n * Emitted when a model node override is removed from the entity's model.\n */\n", "excerptTokens": [ { "kind": "Content", @@ -20921,8 +20581,8 @@ }, { "kind": "Reference", - "text": "EntityModelAnimationEvent.SET_LOOP_MODE", - "canonicalReference": "server!EntityModelAnimationEvent.SET_LOOP_MODE:member" + "text": "EntityEvent.REMOVE_MODEL_NODE_OVERRIDE", + "canonicalReference": "server!EntityEvent.REMOVE_MODEL_NODE_OVERRIDE:member" }, { "kind": "Content", @@ -20930,21 +20590,21 @@ }, { "kind": "Content", - "text": "{\n entityModelAnimation: " + "text": "{\n entity: " }, { "kind": "Reference", - "text": "EntityModelAnimation", - "canonicalReference": "server!EntityModelAnimation:class" + "text": "Entity", + "canonicalReference": "server!Entity:class" }, { "kind": "Content", - "text": ";\n loopMode: " + "text": ";\n entityModelNodeOverride: " }, { "kind": "Reference", - "text": "EntityModelAnimationLoopMode", - "canonicalReference": "server!EntityModelAnimationLoopMode:enum" + "text": "EntityModelNodeOverride", + "canonicalReference": "server!EntityModelNodeOverride:class" }, { "kind": "Content", @@ -20958,7 +20618,7 @@ "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "\"ENTITY_MODEL_ANIMATION.SET_LOOP_MODE\"", + "name": "\"ENTITY.REMOVE_MODEL_NODE_OVERRIDE\"", "propertyTypeTokenRange": { "startIndex": 3, "endIndex": 8 @@ -20966,8 +20626,8 @@ }, { "kind": "PropertySignature", - "canonicalReference": "server!EntityModelAnimationEventPayloads#\"ENTITY_MODEL_ANIMATION.SET_PLAYBACK_RATE\":member", - "docComment": "/**\n * Emitted when the playback rate of an entity model animation is set.\n */\n", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_BLOCK_TEXTURE_URI\":member", + "docComment": "/**\n * Emitted when the texture uri of a block entity is set.\n */\n", "excerptTokens": [ { "kind": "Content", @@ -20975,8 +20635,8 @@ }, { "kind": "Reference", - "text": "EntityModelAnimationEvent.SET_PLAYBACK_RATE", - "canonicalReference": "server!EntityModelAnimationEvent.SET_PLAYBACK_RATE:member" + "text": "EntityEvent.SET_BLOCK_TEXTURE_URI", + "canonicalReference": "server!EntityEvent.SET_BLOCK_TEXTURE_URI:member" }, { "kind": "Content", @@ -20984,16 +20644,16 @@ }, { "kind": "Content", - "text": "{\n entityModelAnimation: " + "text": "{\n entity: " }, { "kind": "Reference", - "text": "EntityModelAnimation", - "canonicalReference": "server!EntityModelAnimation:class" + "text": "Entity", + "canonicalReference": "server!Entity:class" }, { "kind": "Content", - "text": ";\n playbackRate: number;\n }" + "text": ";\n blockTextureUri: string | undefined;\n }" }, { "kind": "Content", @@ -21003,7 +20663,7 @@ "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "\"ENTITY_MODEL_ANIMATION.SET_PLAYBACK_RATE\"", + "name": "\"ENTITY.SET_BLOCK_TEXTURE_URI\"", "propertyTypeTokenRange": { "startIndex": 3, "endIndex": 6 @@ -21011,8 +20671,8 @@ }, { "kind": "PropertySignature", - "canonicalReference": "server!EntityModelAnimationEventPayloads#\"ENTITY_MODEL_ANIMATION.SET_WEIGHT\":member", - "docComment": "/**\n * Emitted when the weight of an entity model animation is set.\n */\n", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_EMISSIVE_COLOR\":member", + "docComment": "/**\n * Emitted when the emissive color is set.\n */\n", "excerptTokens": [ { "kind": "Content", @@ -21020,8 +20680,8 @@ }, { "kind": "Reference", - "text": "EntityModelAnimationEvent.SET_WEIGHT", - "canonicalReference": "server!EntityModelAnimationEvent.SET_WEIGHT:member" + "text": "EntityEvent.SET_EMISSIVE_COLOR", + "canonicalReference": "server!EntityEvent.SET_EMISSIVE_COLOR:member" }, { "kind": "Content", @@ -21029,16 +20689,25 @@ }, { "kind": "Content", - "text": "{\n entityModelAnimation: " + "text": "{\n entity: " }, { "kind": "Reference", - "text": "EntityModelAnimation", - "canonicalReference": "server!EntityModelAnimation:class" + "text": "Entity", + "canonicalReference": "server!Entity:class" }, { "kind": "Content", - "text": ";\n weight: number;\n }" + "text": ";\n emissiveColor: " + }, + { + "kind": "Reference", + "text": "RgbColor", + "canonicalReference": "server!RgbColor:interface" + }, + { + "kind": "Content", + "text": " | undefined;\n }" }, { "kind": "Content", @@ -21048,16 +20717,16 @@ "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "\"ENTITY_MODEL_ANIMATION.SET_WEIGHT\"", + "name": "\"ENTITY.SET_EMISSIVE_COLOR\"", "propertyTypeTokenRange": { "startIndex": 3, - "endIndex": 6 + "endIndex": 8 } }, { "kind": "PropertySignature", - "canonicalReference": "server!EntityModelAnimationEventPayloads#\"ENTITY_MODEL_ANIMATION.STOP\":member", - "docComment": "/**\n * Emitted when an entity model animation is stopped.\n */\n", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_EMISSIVE_INTENSITY\":member", + "docComment": "/**\n * Emitted when the emissive intensity is set.\n */\n", "excerptTokens": [ { "kind": "Content", @@ -21065,8 +20734,8 @@ }, { "kind": "Reference", - "text": "EntityModelAnimationEvent.STOP", - "canonicalReference": "server!EntityModelAnimationEvent.STOP:member" + "text": "EntityEvent.SET_EMISSIVE_INTENSITY", + "canonicalReference": "server!EntityEvent.SET_EMISSIVE_INTENSITY:member" }, { "kind": "Content", @@ -21074,16 +20743,16 @@ }, { "kind": "Content", - "text": "{\n entityModelAnimation: " + "text": "{\n entity: " }, { "kind": "Reference", - "text": "EntityModelAnimation", - "canonicalReference": "server!EntityModelAnimation:class" + "text": "Entity", + "canonicalReference": "server!Entity:class" }, { "kind": "Content", - "text": ";\n }" + "text": ";\n emissiveIntensity: number | undefined;\n }" }, { "kind": "Content", @@ -21093,123 +20762,96 @@ "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "\"ENTITY_MODEL_ANIMATION.STOP\"", + "name": "\"ENTITY.SET_EMISSIVE_INTENSITY\"", "propertyTypeTokenRange": { "startIndex": 3, "endIndex": 6 } - } - ], - "extendsTokenRanges": [] - }, - { - "kind": "Enum", - "canonicalReference": "server!EntityModelAnimationLoopMode:enum", - "docComment": "/**\n * The loop mode of an entity model animation.\n *\n * **Category:** Entities\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export declare enum EntityModelAnimationLoopMode " - } - ], - "fileUrlPath": "src/worlds/entities/EntityModelAnimation.ts", - "releaseTag": "Public", - "name": "EntityModelAnimationLoopMode", - "preserveMemberOrder": false, - "members": [ + }, { - "kind": "EnumMember", - "canonicalReference": "server!EntityModelAnimationLoopMode.LOOP:member", - "docComment": "", + "kind": "PropertySignature", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_MODEL_SCALE_INTERPOLATION_MS\":member", + "docComment": "/**\n * Emitted when the interpolation time in milliseconds applied to model scale changes is set.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "LOOP = " + "text": "[" + }, + { + "kind": "Reference", + "text": "EntityEvent.SET_MODEL_SCALE_INTERPOLATION_MS", + "canonicalReference": "server!EntityEvent.SET_MODEL_SCALE_INTERPOLATION_MS:member" }, { "kind": "Content", - "text": "1" - } - ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "name": "LOOP" - }, - { - "kind": "EnumMember", - "canonicalReference": "server!EntityModelAnimationLoopMode.ONCE:member", - "docComment": "", - "excerptTokens": [ + "text": "]: " + }, { "kind": "Content", - "text": "ONCE = " + "text": "{\n entity: " + }, + { + "kind": "Reference", + "text": "Entity", + "canonicalReference": "server!Entity:class" }, { "kind": "Content", - "text": "0" + "text": ";\n interpolationMs: number | undefined;\n }" + }, + { + "kind": "Content", + "text": ";" } ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, + "isReadonly": false, + "isOptional": false, "releaseTag": "Public", - "name": "ONCE" + "name": "\"ENTITY.SET_MODEL_SCALE_INTERPOLATION_MS\"", + "propertyTypeTokenRange": { + "startIndex": 3, + "endIndex": 6 + } }, { - "kind": "EnumMember", - "canonicalReference": "server!EntityModelAnimationLoopMode.PING_PONG:member", - "docComment": "", + "kind": "PropertySignature", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_MODEL_SCALE\":member", + "docComment": "/**\n * Emitted when the scale of the entity's model is set.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "PING_PONG = " + "text": "[" + }, + { + "kind": "Reference", + "text": "EntityEvent.SET_MODEL_SCALE", + "canonicalReference": "server!EntityEvent.SET_MODEL_SCALE:member" }, { "kind": "Content", - "text": "2" - } - ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "name": "PING_PONG" - } - ] - }, - { - "kind": "Interface", - "canonicalReference": "server!EntityModelAnimationOptions:interface", - "docComment": "/**\n * The options for creating an EntityModelAnimation instance.\n *\n * **Category:** Entities\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export interface EntityModelAnimationOptions " - } - ], - "fileUrlPath": "src/worlds/entities/EntityModelAnimation.ts", - "releaseTag": "Public", - "name": "EntityModelAnimationOptions", - "preserveMemberOrder": false, - "members": [ - { - "kind": "PropertySignature", - "canonicalReference": "server!EntityModelAnimationOptions#blendMode:member", - "docComment": "/**\n * The initial blend mode of the entity model animation.\n */\n", - "excerptTokens": [ + "text": "]: " + }, { "kind": "Content", - "text": "blendMode?: " + "text": "{\n entity: " }, { "kind": "Reference", - "text": "EntityModelAnimationBlendMode", - "canonicalReference": "server!EntityModelAnimationBlendMode:enum" + "text": "Entity", + "canonicalReference": "server!Entity:class" + }, + { + "kind": "Content", + "text": ";\n modelScale: " + }, + { + "kind": "Reference", + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" + }, + { + "kind": "Content", + "text": ";\n }" }, { "kind": "Content", @@ -21217,26 +20859,44 @@ } ], "isReadonly": false, - "isOptional": true, + "isOptional": false, "releaseTag": "Public", - "name": "blendMode", + "name": "\"ENTITY.SET_MODEL_SCALE\"", "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 + "startIndex": 3, + "endIndex": 8 } }, { "kind": "PropertySignature", - "canonicalReference": "server!EntityModelAnimationOptions#clampWhenFinished:member", - "docComment": "/**\n * Whether the animation should clamp when finished, holding the last frame.\n */\n", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_MODEL_TEXTURE_URI\":member", + "docComment": "/**\n * Emitted when the texture uri of the entity's model is set.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "clampWhenFinished?: " + "text": "[" + }, + { + "kind": "Reference", + "text": "EntityEvent.SET_MODEL_TEXTURE_URI", + "canonicalReference": "server!EntityEvent.SET_MODEL_TEXTURE_URI:member" }, { "kind": "Content", - "text": "boolean" + "text": "]: " + }, + { + "kind": "Content", + "text": "{\n entity: " + }, + { + "kind": "Reference", + "text": "Entity", + "canonicalReference": "server!Entity:class" + }, + { + "kind": "Content", + "text": ";\n modelTextureUri: string | undefined;\n }" }, { "kind": "Content", @@ -21244,28 +20904,45 @@ } ], "isReadonly": false, - "isOptional": true, + "isOptional": false, "releaseTag": "Public", - "name": "clampWhenFinished", + "name": "\"ENTITY.SET_MODEL_TEXTURE_URI\"", "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 + "startIndex": 3, + "endIndex": 6 } }, { "kind": "PropertySignature", - "canonicalReference": "server!EntityModelAnimationOptions#entity:member", - "docComment": "/**\n * The entity that the entity model animation belongs to.\n */\n", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_OPACITY\":member", + "docComment": "/**\n * Emitted when the opacity of the entity is set.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "entity: " + "text": "[" + }, + { + "kind": "Reference", + "text": "EntityEvent.SET_OPACITY", + "canonicalReference": "server!EntityEvent.SET_OPACITY:member" + }, + { + "kind": "Content", + "text": "]: " + }, + { + "kind": "Content", + "text": "{\n entity: " }, { "kind": "Reference", "text": "Entity", "canonicalReference": "server!Entity:class" }, + { + "kind": "Content", + "text": ";\n opacity: number;\n }" + }, { "kind": "Content", "text": ";" @@ -21274,24 +20951,60 @@ "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "entity", + "name": "\"ENTITY.SET_OPACITY\"", "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 + "startIndex": 3, + "endIndex": 6 } }, { "kind": "PropertySignature", - "canonicalReference": "server!EntityModelAnimationOptions#fadesIn:member", - "docComment": "/**\n * Whether the animation fades in when played or restarted.\n */\n", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_OUTLINE\":member", + "docComment": "/**\n * Emitted when the outline of the entity is set.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "fadesIn?: " + "text": "[" + }, + { + "kind": "Reference", + "text": "EntityEvent.SET_OUTLINE", + "canonicalReference": "server!EntityEvent.SET_OUTLINE:member" }, { "kind": "Content", - "text": "boolean" + "text": "]: " + }, + { + "kind": "Content", + "text": "{\n entity: " + }, + { + "kind": "Reference", + "text": "Entity", + "canonicalReference": "server!Entity:class" + }, + { + "kind": "Content", + "text": ";\n outline: " + }, + { + "kind": "Reference", + "text": "Outline", + "canonicalReference": "server!Outline:interface" + }, + { + "kind": "Content", + "text": " | undefined;\n forPlayer?: " + }, + { + "kind": "Reference", + "text": "Player", + "canonicalReference": "server!Player:class" + }, + { + "kind": "Content", + "text": ";\n }" }, { "kind": "Content", @@ -21299,26 +21012,53 @@ } ], "isReadonly": false, - "isOptional": true, + "isOptional": false, "releaseTag": "Public", - "name": "fadesIn", + "name": "\"ENTITY.SET_OUTLINE\"", "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 + "startIndex": 3, + "endIndex": 10 } }, { "kind": "PropertySignature", - "canonicalReference": "server!EntityModelAnimationOptions#fadesOut:member", - "docComment": "/**\n * Whether the animation fades out when paused or stopped.\n */\n", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_PARENT\":member", + "docComment": "/**\n * Emitted when the parent of the entity is set.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "fadesOut?: " + "text": "[" + }, + { + "kind": "Reference", + "text": "EntityEvent.SET_PARENT", + "canonicalReference": "server!EntityEvent.SET_PARENT:member" }, { "kind": "Content", - "text": "boolean" + "text": "]: " + }, + { + "kind": "Content", + "text": "{\n entity: " + }, + { + "kind": "Reference", + "text": "Entity", + "canonicalReference": "server!Entity:class" + }, + { + "kind": "Content", + "text": ";\n parent: " + }, + { + "kind": "Reference", + "text": "Entity", + "canonicalReference": "server!Entity:class" + }, + { + "kind": "Content", + "text": " | undefined;\n parentNodeName: string | undefined;\n }" }, { "kind": "Content", @@ -21326,27 +21066,44 @@ } ], "isReadonly": false, - "isOptional": true, + "isOptional": false, "releaseTag": "Public", - "name": "fadesOut", + "name": "\"ENTITY.SET_PARENT\"", "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 + "startIndex": 3, + "endIndex": 8 } }, { "kind": "PropertySignature", - "canonicalReference": "server!EntityModelAnimationOptions#loopMode:member", - "docComment": "/**\n * The initial loop mode of the entity model animation.\n */\n", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_POSITION_INTERPOLATION_MS\":member", + "docComment": "/**\n * Emitted when the interpolation time in milliseconds applied to position changes is set.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "loopMode?: " + "text": "[" }, { "kind": "Reference", - "text": "EntityModelAnimationLoopMode", - "canonicalReference": "server!EntityModelAnimationLoopMode:enum" + "text": "EntityEvent.SET_POSITION_INTERPOLATION_MS", + "canonicalReference": "server!EntityEvent.SET_POSITION_INTERPOLATION_MS:member" + }, + { + "kind": "Content", + "text": "]: " + }, + { + "kind": "Content", + "text": "{\n entity: " + }, + { + "kind": "Reference", + "text": "Entity", + "canonicalReference": "server!Entity:class" + }, + { + "kind": "Content", + "text": ";\n interpolationMs: number | undefined;\n }" }, { "kind": "Content", @@ -21354,26 +21111,44 @@ } ], "isReadonly": false, - "isOptional": true, + "isOptional": false, "releaseTag": "Public", - "name": "loopMode", + "name": "\"ENTITY.SET_POSITION_INTERPOLATION_MS\"", "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 + "startIndex": 3, + "endIndex": 6 } }, { "kind": "PropertySignature", - "canonicalReference": "server!EntityModelAnimationOptions#name:member", - "docComment": "/**\n * The name of the entity model animation.\n */\n", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_ROTATION_INTERPOLATION_MS\":member", + "docComment": "/**\n * Emitted when the interpolation time in milliseconds applied to rotation changes is set.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "name: " + "text": "[" + }, + { + "kind": "Reference", + "text": "EntityEvent.SET_ROTATION_INTERPOLATION_MS", + "canonicalReference": "server!EntityEvent.SET_ROTATION_INTERPOLATION_MS:member" }, { "kind": "Content", - "text": "string" + "text": "]: " + }, + { + "kind": "Content", + "text": "{\n entity: " + }, + { + "kind": "Reference", + "text": "Entity", + "canonicalReference": "server!Entity:class" + }, + { + "kind": "Content", + "text": ";\n interpolationMs: number | undefined;\n }" }, { "kind": "Content", @@ -21383,24 +21158,51 @@ "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "name", + "name": "\"ENTITY.SET_ROTATION_INTERPOLATION_MS\"", "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 + "startIndex": 3, + "endIndex": 6 } }, { "kind": "PropertySignature", - "canonicalReference": "server!EntityModelAnimationOptions#play:member", - "docComment": "/**\n * Whether the animation should start playing on construction.\n */\n", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SET_TINT_COLOR\":member", + "docComment": "/**\n * Emitted when the tint color of the entity is set.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "play?: " + "text": "[" + }, + { + "kind": "Reference", + "text": "EntityEvent.SET_TINT_COLOR", + "canonicalReference": "server!EntityEvent.SET_TINT_COLOR:member" }, { "kind": "Content", - "text": "boolean" + "text": "]: " + }, + { + "kind": "Content", + "text": "{\n entity: " + }, + { + "kind": "Reference", + "text": "Entity", + "canonicalReference": "server!Entity:class" + }, + { + "kind": "Content", + "text": ";\n tintColor: " + }, + { + "kind": "Reference", + "text": "RgbColor", + "canonicalReference": "server!RgbColor:interface" + }, + { + "kind": "Content", + "text": " | undefined;\n }" }, { "kind": "Content", @@ -21408,26 +21210,44 @@ } ], "isReadonly": false, - "isOptional": true, + "isOptional": false, "releaseTag": "Public", - "name": "play", + "name": "\"ENTITY.SET_TINT_COLOR\"", "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 + "startIndex": 3, + "endIndex": 8 } }, { "kind": "PropertySignature", - "canonicalReference": "server!EntityModelAnimationOptions#playbackRate:member", - "docComment": "/**\n * The initial playback rate of the entity model animation.\n */\n", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.SPAWN\":member", + "docComment": "/**\n * Emitted when the entity is spawned.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "playbackRate?: " + "text": "[" + }, + { + "kind": "Reference", + "text": "EntityEvent.SPAWN", + "canonicalReference": "server!EntityEvent.SPAWN:member" }, { "kind": "Content", - "text": "number" + "text": "]: " + }, + { + "kind": "Content", + "text": "{\n entity: " + }, + { + "kind": "Reference", + "text": "Entity", + "canonicalReference": "server!Entity:class" + }, + { + "kind": "Content", + "text": ";\n }" }, { "kind": "Content", @@ -21435,26 +21255,44 @@ } ], "isReadonly": false, - "isOptional": true, + "isOptional": false, "releaseTag": "Public", - "name": "playbackRate", + "name": "\"ENTITY.SPAWN\"", "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 + "startIndex": 3, + "endIndex": 6 } }, { "kind": "PropertySignature", - "canonicalReference": "server!EntityModelAnimationOptions#weight:member", - "docComment": "/**\n * The initial blend weight of the entity model animation.\n */\n", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.TICK\":member", + "docComment": "/**\n * Emitted when the entity is ticked.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "weight?: " + "text": "[" + }, + { + "kind": "Reference", + "text": "EntityEvent.TICK", + "canonicalReference": "server!EntityEvent.TICK:member" }, { "kind": "Content", - "text": "number" + "text": "]: " + }, + { + "kind": "Content", + "text": "{\n entity: " + }, + { + "kind": "Reference", + "text": "Entity", + "canonicalReference": "server!Entity:class" + }, + { + "kind": "Content", + "text": ";\n tickDeltaMs: number;\n }" }, { "kind": "Content", @@ -21462,228 +21300,89 @@ } ], "isReadonly": false, - "isOptional": true, + "isOptional": false, "releaseTag": "Public", - "name": "weight", + "name": "\"ENTITY.TICK\"", "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 + "startIndex": 3, + "endIndex": 6 } - } - ], - "extendsTokenRanges": [] - }, - { - "kind": "Enum", - "canonicalReference": "server!EntityModelAnimationState:enum", - "docComment": "/**\n * The state of an entity model animation.\n *\n * **Category:** Entities\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export declare enum EntityModelAnimationState " - } - ], - "fileUrlPath": "src/worlds/entities/EntityModelAnimation.ts", - "releaseTag": "Public", - "name": "EntityModelAnimationState", - "preserveMemberOrder": false, - "members": [ + }, { - "kind": "EnumMember", - "canonicalReference": "server!EntityModelAnimationState.PAUSED:member", - "docComment": "", + "kind": "PropertySignature", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.UPDATE_POSITION\":member", + "docComment": "/**\n * Emitted when the position of the entity is updated at the end of the tick, either directly or by physics.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "PAUSED = " + "text": "[" }, { - "kind": "Content", - "text": "1" - } - ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "name": "PAUSED" - }, - { - "kind": "EnumMember", - "canonicalReference": "server!EntityModelAnimationState.PLAYING:member", - "docComment": "", - "excerptTokens": [ - { - "kind": "Content", - "text": "PLAYING = " + "kind": "Reference", + "text": "EntityEvent.UPDATE_POSITION", + "canonicalReference": "server!EntityEvent.UPDATE_POSITION:member" }, { "kind": "Content", - "text": "0" - } - ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "name": "PLAYING" - }, - { - "kind": "EnumMember", - "canonicalReference": "server!EntityModelAnimationState.STOPPED:member", - "docComment": "", - "excerptTokens": [ - { - "kind": "Content", - "text": "STOPPED = " + "text": "]: " }, { "kind": "Content", - "text": "2" - } - ], - "initializerTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "name": "STOPPED" - } - ] - }, - { - "kind": "Class", - "canonicalReference": "server!EntityModelNodeOverride:class", - "docComment": "/**\n * Represents a name-match model node override rule for an Entity.\n *\n * When to use: configuring visual and transform overrides for one or more model nodes selected by name match.\n *\n * @remarks\n *\n * Node overrides are match-rule based and may target multiple nodes. Matching is case-insensitive. Exact match is used by default; wildcard matching is only enabled when `*` is used at the start and/or end of `nameMatch` (`head*`, `*head`, `*head*`). Supported override settings include emissive color/intensity, hidden state, and local position/rotation/scale.\n *\n * **Category:** Entities\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export default class EntityModelNodeOverride implements " - }, - { - "kind": "Reference", - "text": "protocol.Serializable", - "canonicalReference": "@hytopia.com/server-protocol!Serializable:interface" - }, - { - "kind": "Content", - "text": " " - } - ], - "fileUrlPath": "src/worlds/entities/EntityModelNodeOverride.ts", - "releaseTag": "Public", - "isAbstract": false, - "name": "EntityModelNodeOverride", - "preserveMemberOrder": false, - "members": [ - { - "kind": "Constructor", - "canonicalReference": "server!EntityModelNodeOverride:constructor(1)", - "docComment": "/**\n * Creates a new EntityModelNodeOverride instance.\n *\n * @param options - The options for the model node override.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "constructor(options: " + "text": "{\n entity: " }, { "kind": "Reference", - "text": "EntityModelNodeOverrideOptions", - "canonicalReference": "server!EntityModelNodeOverrideOptions:interface" + "text": "Entity", + "canonicalReference": "server!Entity:class" }, { "kind": "Content", - "text": ");" - } - ], - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "options", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ] - }, - { - "kind": "Property", - "canonicalReference": "server!EntityModelNodeOverride#emissiveColor:member", - "docComment": "/**\n * The emissive color for matching nodes.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "get emissiveColor(): " + "text": ";\n position: " }, { "kind": "Reference", - "text": "RgbColor", - "canonicalReference": "server!RgbColor:interface" + "text": "Vector3Like", + "canonicalReference": "server!Vector3Like:interface" }, { "kind": "Content", - "text": " | undefined" + "text": ";\n }" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, + "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "emissiveColor", + "name": "\"ENTITY.UPDATE_POSITION\"", "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "startIndex": 3, + "endIndex": 8 + } }, { - "kind": "Property", - "canonicalReference": "server!EntityModelNodeOverride#emissiveIntensity:member", - "docComment": "/**\n * The emissive intensity for matching nodes.\n *\n * **Category:** Entities\n */\n", + "kind": "PropertySignature", + "canonicalReference": "server!EntityEventPayloads#\"ENTITY.UPDATE_ROTATION\":member", + "docComment": "/**\n * Emitted when the rotation of the entity is updated at the end of the tick, either directly or by physics.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get emissiveIntensity(): " + "text": "[" }, { - "kind": "Content", - "text": "number | undefined" + "kind": "Reference", + "text": "EntityEvent.UPDATE_ROTATION", + "canonicalReference": "server!EntityEvent.UPDATE_ROTATION:member" }, { "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "emissiveIntensity", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "server!EntityModelNodeOverride#entity:member", - "docComment": "/**\n * The entity that the model node override belongs to.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ + "text": "]: " + }, { "kind": "Content", - "text": "get entity(): " + "text": "{\n entity: " }, { "kind": "Reference", @@ -21692,98 +21391,62 @@ }, { "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "entity", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "server!EntityModelNodeOverride#isHidden:member", - "docComment": "/**\n * Whether the matched node(s) are hidden.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "get isHidden(): " - }, - { - "kind": "Content", - "text": "boolean" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "isHidden", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "server!EntityModelNodeOverride#localPosition:member", - "docComment": "/**\n * The local position set for matching nodes.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "get localPosition(): " + "text": ";\n rotation: " }, { "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" + "text": "QuaternionLike", + "canonicalReference": "server!QuaternionLike:interface" }, { "kind": "Content", - "text": " | undefined" + "text": ";\n }" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, + "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "localPosition", + "name": "\"ENTITY.UPDATE_ROTATION\"", "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, + "startIndex": 3, + "endIndex": 8 + } + } + ], + "extendsTokenRanges": [] + }, + { + "kind": "Class", + "canonicalReference": "server!EntityManager:class", + "docComment": "/**\n * Manages entities in a world.\n *\n * When to use: querying and filtering entities within a specific world. Do NOT use for: cross-world queries; access each world's manager separately.\n *\n * @remarks\n *\n * The EntityManager is created internally per `World` instance.\n *\n * The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `EntityManager` class.\n *\n * @example\n * ```typescript\n * // Get all entities in the world\n * const entityManager = world.entityManager;\n * const entities = entityManager.getAllEntities();\n * ```\n *\n * **Category:** Entities\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export default class EntityManager " + } + ], + "fileUrlPath": "src/worlds/entities/EntityManager.ts", + "releaseTag": "Public", + "isAbstract": false, + "name": "EntityManager", + "preserveMemberOrder": false, + "members": [ { "kind": "Property", - "canonicalReference": "server!EntityModelNodeOverride#localPositionInterpolationMs:member", - "docComment": "/**\n * The interpolation time in milliseconds applied to local position changes.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!EntityManager#entityCount:member", + "docComment": "/**\n * The number of spawned entities in the world.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get localPositionInterpolationMs(): " + "text": "get entityCount(): " }, { "kind": "Content", - "text": "number | undefined" + "text": "number" }, { "kind": "Content", @@ -21793,7 +21456,7 @@ "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "localPositionInterpolationMs", + "name": "entityCount", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -21803,173 +21466,85 @@ "isAbstract": false }, { - "kind": "Property", - "canonicalReference": "server!EntityModelNodeOverride#localRotation:member", - "docComment": "/**\n * The local rotation set for matching nodes.\n *\n * **Category:** Entities\n */\n", + "kind": "Method", + "canonicalReference": "server!EntityManager#getAllEntities:member(1)", + "docComment": "/**\n * Gets all spawned entities in the world.\n *\n * @returns All spawned entities in the world.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get localRotation(): " + "text": "getAllEntities(): " }, { "kind": "Reference", - "text": "QuaternionLike", - "canonicalReference": "server!QuaternionLike:interface" + "text": "Entity", + "canonicalReference": "server!Entity:class" }, { "kind": "Content", - "text": " | undefined" + "text": "[]" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "localRotation", - "propertyTypeTokenRange": { + "isStatic": false, + "returnTypeTokenRange": { "startIndex": 1, "endIndex": 3 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "server!EntityModelNodeOverride#localRotationInterpolationMs:member", - "docComment": "/**\n * The interpolation time in milliseconds applied to local rotation changes.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "get localRotationInterpolationMs(): " - }, - { - "kind": "Content", - "text": "number | undefined" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, "releaseTag": "Public", - "name": "localRotationInterpolationMs", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, "isProtected": false, - "isAbstract": false + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "getAllEntities" }, { - "kind": "Property", - "canonicalReference": "server!EntityModelNodeOverride#localScale:member", - "docComment": "/**\n * The local scale set for matching nodes.\n *\n * **Category:** Entities\n */\n", + "kind": "Method", + "canonicalReference": "server!EntityManager#getAllPlayerEntities:member(1)", + "docComment": "/**\n * Gets all spawned player entities in the world.\n *\n * @returns All spawned player entities in the world.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get localScale(): " + "text": "getAllPlayerEntities(): " }, { "kind": "Reference", - "text": "Vector3Like", - "canonicalReference": "server!Vector3Like:interface" + "text": "PlayerEntity", + "canonicalReference": "server!PlayerEntity:class" }, { "kind": "Content", - "text": " | undefined" + "text": "[]" }, { "kind": "Content", "text": ";" } ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "localScale", - "propertyTypeTokenRange": { + "isStatic": false, + "returnTypeTokenRange": { "startIndex": 1, "endIndex": 3 }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "server!EntityModelNodeOverride#localScaleInterpolationMs:member", - "docComment": "/**\n * The interpolation time in milliseconds applied to local scale changes.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "get localScaleInterpolationMs(): " - }, - { - "kind": "Content", - "text": "number | undefined" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, "releaseTag": "Public", - "name": "localScaleInterpolationMs", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "server!EntityModelNodeOverride#name:member", - "docComment": "/**\n * Alias used by networking serializer and protocol schema (`n`).\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "get name(): " - }, - { - "kind": "Content", - "text": "string" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, + "overloadIndex": 1, + "parameters": [], "isOptional": false, - "releaseTag": "Public", - "name": "name", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false + "isAbstract": false, + "name": "getAllPlayerEntities" }, { - "kind": "Property", - "canonicalReference": "server!EntityModelNodeOverride#nameMatch:member", - "docComment": "/**\n * The node name match selector for this override. Exact match by default, with optional edge wildcard (`head*`, `*head`, `*head*`).\n *\n * **Category:** Entities\n */\n", + "kind": "Method", + "canonicalReference": "server!EntityManager#getEntitiesByTag:member(1)", + "docComment": "/**\n * Gets all spawned entities in the world with a specific tag.\n *\n * @param tag - The tag to get the entities for.\n *\n * @returns All spawned entities in the world with the provided tag.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get nameMatch(): " + "text": "getEntitiesByTag(tag: " }, { "kind": "Content", @@ -21977,33 +21552,16 @@ }, { "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "nameMatch", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Method", - "canonicalReference": "server!EntityModelNodeOverride#remove:member(1)", - "docComment": "/**\n * Removes this model node override from its parent entity.\n *\n * @remarks\n *\n * This delegates to `Entity.removeModelNodeOverride()` so that map mutation and related event emission remain centralized on the entity.\n *\n * **Category:** Entities\n */\n", - "excerptTokens": [ + "text": "): " + }, { - "kind": "Content", - "text": "remove(): " + "kind": "Reference", + "text": "Entity", + "canonicalReference": "server!Entity:class" }, { "kind": "Content", - "text": "void" + "text": "[]" }, { "kind": "Content", @@ -22012,42 +21570,51 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 + "startIndex": 3, + "endIndex": 5 }, "releaseTag": "Public", "isProtected": false, "overloadIndex": 1, - "parameters": [], + "parameters": [ + { + "parameterName": "tag", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], "isOptional": false, "isAbstract": false, - "name": "remove" + "name": "getEntitiesByTag" }, { "kind": "Method", - "canonicalReference": "server!EntityModelNodeOverride#setEmissiveColor:member(1)", - "docComment": "/**\n * Sets the emissive color for matching nodes.\n *\n * @param emissiveColor - The emissive color to set.\n *\n * **Side effects:** Emits `EntityModelNodeOverrideEvent.SET_EMISSIVE_COLOR` when spawned.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!EntityManager#getEntitiesByTagSubstring:member(1)", + "docComment": "/**\n * Gets all spawned entities in the world with a tag that includes a specific substring.\n *\n * @param tagSubstring - The tag substring to get the entities for.\n *\n * @returns All spawned entities in the world with a tag that includes the provided substring.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setEmissiveColor(emissiveColor: " - }, - { - "kind": "Reference", - "text": "RgbColor", - "canonicalReference": "server!RgbColor:interface" + "text": "getEntitiesByTagSubstring(tagSubstring: " }, { "kind": "Content", - "text": " | undefined" + "text": "string" }, { "kind": "Content", "text": "): " }, + { + "kind": "Reference", + "text": "Entity", + "canonicalReference": "server!Entity:class" + }, { "kind": "Content", - "text": "void" + "text": "[]" }, { "kind": "Content", @@ -22056,7 +21623,7 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 4, + "startIndex": 3, "endIndex": 5 }, "releaseTag": "Public", @@ -22064,78 +21631,39 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "emissiveColor", + "parameterName": "tagSubstring", "parameterTypeTokenRange": { "startIndex": 1, - "endIndex": 3 + "endIndex": 2 }, "isOptional": false } ], "isOptional": false, "isAbstract": false, - "name": "setEmissiveColor" + "name": "getEntitiesByTagSubstring" }, { "kind": "Method", - "canonicalReference": "server!EntityModelNodeOverride#setEmissiveIntensity:member(1)", - "docComment": "/**\n * Sets the emissive intensity for matching nodes.\n *\n * @param emissiveIntensity - The emissive intensity to set.\n *\n * **Side effects:** Emits `EntityModelNodeOverrideEvent.SET_EMISSIVE_INTENSITY` when spawned.\n *\n * **Category:** Entities\n */\n", + "canonicalReference": "server!EntityManager#getEntity:member(1)", + "docComment": "/**\n * Gets a spawned entity in the world by its ID.\n *\n * @param id - The ID of the entity to get.\n *\n * @returns The spawned entity with the provided ID, or undefined if no entity is found.\n *\n * **Category:** Entities\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "setEmissiveIntensity(emissiveIntensity: " - }, - { - "kind": "Content", - "text": "number | undefined" + "text": "getEntity