Tracked slice of docs/plans/sdk-remediation.md Phase 3e. Filed per the plan's PR-sequence item 6. The plan flags this one as "expensive, calcifies if deferred" — the longer it waits, the more code copies the patterns.
1. Games cannot extend the sim loop
A genuinely good scheduler exists (game/systemSchedule.ts — topological before/after, order independent of imports), but stepPlayerMovement and advanceBehaviors are hardwired around onTick (runtime/headlessRunner.ts:135-145), outside the stage system and unreorderable.
- The fixed accumulator (
game/systemRuntime.ts:96-104) has no substep cap — spiral of death — while PhysicsWorld right beside it has maxSubsteps.
- No interpolation exists anywhere, so a 30Hz system renders as stutter with no fix short of a core change.
2. The entity store defeats its own spatial index
Map<string, SceneEntity>, closed struct (role: "player"|"npc"|"prop"), no components, extension only via meta: unknown.
- Every write fans out synchronously to every subscriber and bumps
spatialGeneration, rebuilding the entire grid (scene/spatial.ts:114-149). Move-entity-then-query-neighbours is O(N²) per frame.
setPose allocates a fresh 3-tuple per call.
- Fix: dirty sets + incremental grid update. This one violates CLAUDE.md's scale-by-default invariant directly.
3. Physics has no rotation, and the good solver is bypassed
- Zero hits for
angular|quat|torque|inertia in physicsWorld.ts. AABB forever — no tumbling vehicle, ragdoll, or toppling crate.
- The solver itself is excellent (SoA
Float32Array, grid broadphase, sequential impulses, sleeping, joints) and is almost entirely bypassed: 7 separate collision implementations, 5 independent spatial hash grids, 2 gravity integrators (movement/movementModel.ts:278 vs physicsWorld.ts:731), and the same ray-sphere quadratic written twice (scene/sceneRaycast.ts:104, multiplayer/lagCompensation.ts:127).
ballisticSweep.ts:33 and forceVolume.ts:180 do full O(n) scans past the grid sitting next to them.
PhysicsWorld has no snapshot/hydrate — physics sits outside both save and replication.
4. Replication serializes the whole world every commit
SnapshotModule.version() is module-granularity, so entities are dirty the instant anything moves, and area-of-interest filtering runs after the full snapshot is materialized. No dirty bits, deltas, or quantization.
5. Flat React scene tree
Every entity is its own element with its own useFrame and GLB clone; AuthoredScene.tsx:297-315 mounts the entire document at once; culling keeps components mounted and ticking. Editor-placed props are never instanced — 10k props = 10k draw calls. Instancing exists, but only for procedural scatter/city.
6. Animation is a 3-state FSM
idle|walk|run on scalar speed. No IK, blend trees, morphs, root motion, or animation notifies (so no hit frames or foot plants). Skeletal animation exists only on the per-entity path; the instanced path is un-skinned boxes — no route to hundreds of animated humanoids. A third disjoint system (PartMotion.tsx) cannot blend with the mixer path at all.
7. Feel constants are module globals
MOVEMENT_TUNING (accel 26, friction 18, gravity 24) is shared by every game — a heavy mech and a sprinter move identically. Only backpedalSpeedMultiplier is overridable. Same class as the Phase 2.3 module-global cleanup.
8. Audio
- Distance attenuation only, no
PannerNode/HRTF — and setListenerPose(position: Vec3) carries no orientation, so L/R cannot be derived. Widen that signature first; nothing positional works until it is.
- No ducking/sidechain.
crossfadeTo drives all other layers to 0, foreclosing vertical music stems. No streaming (everything is a decoded AudioBuffer).
- Zero audio assets in the repo and no audio catalog in
packages/assets. 2 of 11 games have any audio; none use the positional path.
9. ~10% of core is unreachable
2 fully dead modules and ~47 test-only dead (written, tested, barrel-exported, imported by nothing). world/lod.ts is the archetype — a correct distance-banded LOD scheduler with 8 tests, exported, wired into zero systems. Same for visibility/simulationCulling.ts, ai/interestScheduler.ts, and the whole ai/{flock,crowd,populationDirector,laneSelect} cluster.
Caveat carried from the audit: that reachability was measured with a static in-repo graph, so some modules may be reachable by external deep import. Verify before deleting anything.
Suggested split
Items 1, 2, and 8's signature change are the ones that calcify — schedule them first. Items 3–6 are large enough to be their own PRs. Item 9 is a cleanup pass, not a blocker, and should follow the Phase 1.1 export curation (#1547) so it prunes against the curated surface rather than the wildcard one.
Related: #1139, #1146.
Tracked slice of
docs/plans/sdk-remediation.mdPhase 3e. Filed per the plan's PR-sequence item 6. The plan flags this one as "expensive, calcifies if deferred" — the longer it waits, the more code copies the patterns.1. Games cannot extend the sim loop
A genuinely good scheduler exists (
game/systemSchedule.ts— topological before/after, order independent of imports), butstepPlayerMovementandadvanceBehaviorsare hardwired aroundonTick(runtime/headlessRunner.ts:135-145), outside the stage system and unreorderable.game/systemRuntime.ts:96-104) has no substep cap — spiral of death — whilePhysicsWorldright beside it hasmaxSubsteps.2. The entity store defeats its own spatial index
Map<string, SceneEntity>, closed struct (role: "player"|"npc"|"prop"), no components, extension only viameta: unknown.spatialGeneration, rebuilding the entire grid (scene/spatial.ts:114-149). Move-entity-then-query-neighbours is O(N²) per frame.setPoseallocates a fresh 3-tuple per call.3. Physics has no rotation, and the good solver is bypassed
angular|quat|torque|inertiainphysicsWorld.ts. AABB forever — no tumbling vehicle, ragdoll, or toppling crate.Float32Array, grid broadphase, sequential impulses, sleeping, joints) and is almost entirely bypassed: 7 separate collision implementations, 5 independent spatial hash grids, 2 gravity integrators (movement/movementModel.ts:278vsphysicsWorld.ts:731), and the same ray-sphere quadratic written twice (scene/sceneRaycast.ts:104,multiplayer/lagCompensation.ts:127).ballisticSweep.ts:33andforceVolume.ts:180do full O(n) scans past the grid sitting next to them.PhysicsWorldhas no snapshot/hydrate — physics sits outside both save and replication.4. Replication serializes the whole world every commit
SnapshotModule.version()is module-granularity, so entities are dirty the instant anything moves, and area-of-interest filtering runs after the full snapshot is materialized. No dirty bits, deltas, or quantization.5. Flat React scene tree
Every entity is its own element with its own
useFrameand GLB clone;AuthoredScene.tsx:297-315mounts the entire document at once; culling keeps components mounted and ticking. Editor-placed props are never instanced — 10k props = 10k draw calls. Instancing exists, but only for procedural scatter/city.6. Animation is a 3-state FSM
idle|walk|runon scalar speed. No IK, blend trees, morphs, root motion, or animation notifies (so no hit frames or foot plants). Skeletal animation exists only on the per-entity path; the instanced path is un-skinned boxes — no route to hundreds of animated humanoids. A third disjoint system (PartMotion.tsx) cannot blend with the mixer path at all.7. Feel constants are module globals
MOVEMENT_TUNING(accel 26, friction 18, gravity 24) is shared by every game — a heavy mech and a sprinter move identically. OnlybackpedalSpeedMultiplieris overridable. Same class as the Phase 2.3 module-global cleanup.8. Audio
PannerNode/HRTF — andsetListenerPose(position: Vec3)carries no orientation, so L/R cannot be derived. Widen that signature first; nothing positional works until it is.crossfadeTodrives all other layers to 0, foreclosing vertical music stems. No streaming (everything is a decodedAudioBuffer).packages/assets. 2 of 11 games have any audio; none use the positional path.9. ~10% of core is unreachable
2 fully dead modules and ~47 test-only dead (written, tested, barrel-exported, imported by nothing).
world/lod.tsis the archetype — a correct distance-banded LOD scheduler with 8 tests, exported, wired into zero systems. Same forvisibility/simulationCulling.ts,ai/interestScheduler.ts, and the wholeai/{flock,crowd,populationDirector,laneSelect}cluster.Suggested split
Items 1, 2, and 8's signature change are the ones that calcify — schedule them first. Items 3–6 are large enough to be their own PRs. Item 9 is a cleanup pass, not a blocker, and should follow the Phase 1.1 export curation (#1547) so it prunes against the curated surface rather than the wildcard one.
Related: #1139, #1146.