engine v5.0.1
Engine v5.0.1
June 6, 2026
A patch in the For Real line.
what's new
- Scripts that animate with
Math.sin, ease withMath.exp, or steer withMath.atan2now compute the exact same bits on the server and on every player's machine. Before, different browsers' built-in math disagreed by invisible amounts that physics contact could amplify into visible corrections — cars and props you drove through obstacles could stutter as the server "corrected" the client mid-drive. Math results may shift by amounts far below anything perceivable (the same on every machine), in exchange for driving, easing, and orbiting code that no longer fights the network. - Way faster world loading on big landscapes — your game no longer builds a kilometer of invisible terrain before it lets you in
- Fixed a freeze-then-storm that could happen if terrain got stuck building behind the scenes
- Savi gets honest signals while your world is still loading, so she stops guessing at the wrong cause
- Custom cameras in 2D worlds work again — the camera you authored is the camera you get
- Smoother building: changing fog or adding textures mid-session no longer causes frame hitches
- Editing room layouts updates the physics instantly — no more invisible walls where old walls used to be
- Removing a world's terrain actually removes it now
- Room-builder worlds got a deep clean: doors at corners and junctions no longer leave holes, doors in raised rooms actually open onto the floor, pits are really pits, and indoor spaces finally have proper shadows
- Savi can't silently go quiet mid-build anymore — if a reply fails, she tells you and your message is safe to resend
- The ground half of realistic skies follows your chosen ground color now (sunset tints it like everything else)
- Cars and other multi-piece builds no longer look like they're pulling apart while you drive them — wheels, body and spoiler stay welded to the chassis on screen, including when you slam into things on a laggy connection.
- Walking into a door or portal no longer rubber-bands you right after you arrive — the first steps in a new place are smooth instead of snapping you back once before settling.
- Long play sessions no longer hiccup every 10 minutes — the brief "Connection lost. Reconnecting..." freeze that interrupted games on a timer is gone. Your game's server now stays up as long as anyone is connected, and still winds down a few minutes after the last player leaves.
- Multiplayer physics gets calmer: crates and props you stand next to (or on) no longer flicker between asleep and awake under the hood, which removes a whole class of tiny rotation jitters and client/server corrections around settled piles. Pushing, shoving, and knocking stacks over feels exactly the same — things still wake the moment anything actually touches or hits them.
- Cars, physics props, and characters no longer fight the network over invisible precision: speeds read in 0.05 m/s steps and physics positions in 1 cm steps — far below anything a player can perceive — and in exchange, gameplay state derived from them (steering, gear/RPM, follow distances) stays perfectly in sync between server and client instead of triggering correction churn while driving.
- Big builds feel smooth while they appear: dropping in lots of models or recoloring many things at once no longer freezes the frame — new content fades in as it's ready instead of stopping the world
- Changing the sky or environment mid-session no longer hitches the whole scene
- Giant interior worlds (room builder) load dramatically faster and stay fast
- A typo in a room layout can't erase your terrain anymore — Savi gets a note about exactly which character was wrong and the rest of the world builds fine
- Rivers and lakes stop glowing in the dark — water finally goes properly moody at night
- Movement on a shaky connection feels calmer: brief input hiccups smooth out in one correction instead of repeatedly tugging you back
- God mode got a polish pass: material edits stick, placed shapes stay put, every part of a combo object gets its tuning chips, menu picks always hit the item you clicked, and opening god mode no longer triggers your microphone
- Joining a world that's still waking up now waits at the loading screen instead of dropping you into an empty default world
›technical notes
- The script-sandbox Math shim (every script compiled through
tome/compiler.ts: behaviors, generators, jobs, camera, terrain/geometry/voxel/spline/fx/IK) now routes every implementation-approximated Math member through a deterministic table (tome/deterministic-math.ts, bridged asglobalThis.__tomeDetMath— same pattern as__tomeSeededRng). ECMA-262 leavessin/cos/tan/asin/acos/atan/atan2/exp/expm1/log/log1p/log2/log10/pow/cbrt/sinh/cosh/tanh/asinh/acosh/atanh/hypotimplementation-defined; the server (bun/JSC) and client (Chrome/V8) natives disagree by 1-2 f64 ulps, so a mode-"both" behavior calling them seeded state/motion divergence on every tick it ran (wave-8: the straight-road car is bit-clean parked, noisy driving). f32 output quantization could not close the seam — two implementations near a quantization boundary still split; computing identically does. - Kernels:
dexp/dlog/dtan/dpoware faithful fdlibm ports (e_exp.c, e_log.c, s_tan.c+k_tan.c, e_pow.c incl. scalbn) added toengine/physics/mantle/math/scalar.tsnext to the existing dsin/dcos/datan family, built from exactly-rounded primitives and spec-defined IEEE-754 word access only. The fdlibm e_pow special-case ladder is the ECMA-262 Number::exponentiate table (V8's own Math.pow is this port), verified case-by-case. The long tail (log2/log10/log1p/expm1/cbrt/sinh/cosh/tanh/asinh/acosh/atanh) is derived intome/deterministic-math.tsfrom those kernels with fdlibm-structured identities and exact ops — derived, not faithful ports, and labeled as such.hypotis deliberately REWRITTEN as sqrt of the exact-op sum of squares (native hypot is implementation-defined — wave-8 removed it from the engine's own rotation sanitize for the same reason); on f32-quantized sandbox inputs its overflow-scaling regime is unreachable. - Precision contract: deterministic ≠ native-exact. Sandbox transcendentals can differ from a given engine's native Math by ~1-2 f64 ulps (kernels ≤1, tan ≤3 at extreme reduction args, derived ≤3, measured over 50k-200k point sweeps) — on BOTH sides equally, so existing game behavior changes microscopically and uniformly. The existing f32 in/out quantization wrappers are kept and composed unchanged (
fround(det(fround(x)))): sandbox numbers keep living on the f32 grid, the consumed contract of state codecs and the tome/state compare, and the f32 grid (2^29 × coarser than f64) absorbs almost every det-vs-native ulp delta. Spec-exact members (abs/ceil/floor/fround/imul/clz32/max/min/round/sign/sqrt/trunc) stay native;Math.randomstays on the seeded-RNG bridge; Math constants now come from exactly-parsed literals (still f32-quantized). - Cross-runtime proof: 1.9M-sample bit-pattern hash identical on bun/JSC and node/V8 (natives differ on the same stream); pinned exact f64 bit patterns at boundary-hostile inputs (denormal in/out, overflow edges, near-π/2, 2^20-edge reduction, negative/huge pow operands) in
scalar.test.tsanddeterministic-math.test.tsfail on ANY future runtime divergence. Golden FNV hashes extended to the new kernels. - Perf: deterministic sin+exp pair ≈ 1.5-1.8× native (35ns vs 20ns per pair over 1e6 calls); pow is the heaviest at ~3.4-4.8× in a pow+tan+log mix (~100ns/triple). Behaviors are not the sim hot path (the engine's own kernels were already deterministic); correctness over micro-perf.
- Known residual: the
**operator in creator scripts still lowers to the runtime's native pow (the shim shadows the Math object, not operators). No in-repo game script uses**today; closing it would need an AST rewrite in the compiler. - Client chunk-build liveness: stale/lost terrain build jobs are re-armed instead of parking forever (mirror of the server-side collider-gate fix) — ends permanent grounded/ceiling mismatch storms after a wedged build
- Extended-profile terrain streaming clamps appended far bands at actual visibility (fog saturation × screen-corner factor, camera far plane) — lobby-shaped places drop ~4,225 → ~961 desired chunks on boot; authored lodRanges stream exactly as authored
- Material compile scene-sweeps coalesce to one deferred sweep per 250ms during chunk-install storms (single installs unchanged)
- Frame-budget CPU-bound diagnostics now report live terrain build backlog ("terrain build in progress: N/M chunks") instead of misdirecting at object counts
- run_script execution gates on world hydration (bounded 10s wait) and returns an honest world-not-ready error instead of succeeding against an empty world
- 2D places no longer replace authored cameras with the builtin follow cam (regression since 2026-05-02) — custom camera scripts, setCamera() overrides, and overlays run again in 2d-top/2d-side places; the builtin only fills absence
- Fog changes (add/clear/linear<->exp2/color/range) no longer recompile the whole scene — one retained fog node per scene, all fog ops are uniform writes
- Async texture arrivals (MagicCDN) no longer recompile live materials mid-frame — texture channels bind a placeholder at material build and arrival is a binding swap; adding a texture to an existing object compiles once, not twice
- 3d-rooms collider signatures are content-derived — live layout edits (move a wall, punch a door, raise a floor) rebuild physics on both engines instead of leaving stale colliders
- GPT-lane wisp dispatch schema fixed (z.tuple -> fixed-length array) — technical wisps work again (chat deploy)
- The mismatch digest splits server-input-decay ticks (late frames) into their own decayed-input bucket instead of paging them as drift
- updatePlace({terrain: null}) now fully turns terrain off ({kind:"off"}, monolithic) instead of silently resurrecting the default starter ground
- 3d-rooms overhaul: openings stay watertight at any wall topology (corners, junctions, wall-ends); walls and openings measure from the local floor datum (doors in raised rooms are passable); render/physics parity everywhere (boundary walls solid, void pits open, no outside-layout rescue floor); ambiguous door orientation degrades with a warning instead of wiping the place's terrain; trim follows the cut; documented roughness/normal-map fields work; interiors receive and (tier-gated) cast shadows
- Chat resilience: model output is UTF-16-repaired at the state boundary (one malformed emoji can no longer permanently wedge a room), failed turns say so in chat instead of vanishing, repeated persist failures escalate loudly
- sky.groundAlbedo now colors the entire below-horizon sky dome, lit by the live sky (dusk tints it); unauthored skies are byte-identical
- Zoo: new Rooms 3D museum zone (key 8) exercising every 3d-rooms feature
- Parented assemblies no longer render non-cohesive while driven (ledger #136 — Tucker's race car: parts trail the chassis "most of the time" and pull apart under physics interactions). Root cause: timeline-frame mixing around the locally-predicted assembly root. The predicted root's live pose runs AHEAD of the server (client prediction), while its non-predicted children carry direct-applied SERVER-frame poses; every place that divided or composed the two across frames folded
rootVelocity × timelineSkew(≈ speed × RTT, meters at race speed) into the assembly:tome/local-transform-projectionderived childLocal*asinv(parentLive) ⊗ childServer— laundering the skew into the authored local offsets, which both hierarchy solves then faithfully composed.- The renderer's smoothing (
synthetic-transform-delta) converted child rows to parent-relative space against the parent's NEWEST ring snapshot even when the row's batch tick was older (resim rewrites, burst delivery) — polluting the rel timeline through every correction. - Children with their own dynamic physics bodies were skipped by
hierarchy-render-solve(physics owns their pose) and classified client-predicted (body-type branch), so nothing ever expressed them in the assembly's frame at all.
- Fix — one law, applied at each composition root: every member of a parented assembly is expressed in ONE frame before use.
Local*reconstruction for non-predicted children now divides entirely inside the authoritative frame (ServerOplogResourcenewest state — both operands server truth); predicted entities keep the live division (their frames already agree). New shared reader:tome/systems/authoritative-world-frame.ts.- The renderer's world↔rel conversions are tick-coherent: pendings convert against the parent's ring SAMPLED AT THE BATCH TICK (
worldPoseAtTickInto/sampleRingAtInto, shared with the displayed-pose sampler) instead of the newest snapshot. Past-newest semantics split by call site: displayed-pose sampling keeps the playout clock's velocity extrapolation (bounded byMAX_EXTRAPOLATION_TICKS); historical conversion sampling clamps at the newest snapshot — a ring with no row at the tick means the entity didn't change that tick, and a resting parent must never be extrapolated into a child's conversion. hierarchy-render-solverebases non-predicted dynamic-bodied children of a predicted assembly root into the parent's live frame, preserving the SERVER's parent-relative articulation:childLive = parentLive ⊗ (inv(parentServer) ⊗ childServer).- Prediction classifier: a dynamic body WELDED into an assembly (
tome/parent) is no longer client-predicted — nothing on the client simulates the weld (no physics joint for the hierarchy edge; hierarchy solves skip dynamic bodies), so predicting it only withheld its replicated pose and ground the mismatch comparator against truth the client cannot reproduce.
draw/interpolationstill does NOT propagate parent→child, by design: a rigid child rides the parent's DISPLAYED pose (window-4 hierarchy-consistent interpolation), so its own missing/default config cannot create an independent world timeline; an authored child config only shapes parent-relative articulation smoothing.- New fail-on-parent suite
engine/runtime/__tests__/car-weld-displayed-rigidity.test.ts: the full-stack harness (real server netcode + acks + client prediction + render channel + playout clock + renderer smoothing) extended with genuine client-AHEAD prediction of the driven root and a server-side collision jolt the client cannot predict. Asserts < 0.1 m displayed-pose rigidity across correction events for interpolation-less physics children, pure visual children, scripted (server-realm) physics children, and the authored-override child. All four fail on the parent commit (up to 2.9 m of drawn separation); all pass with the fix. - One-time rubber-band/snap when starting to move right after entering a place (ledger #133, dump c871a5d5 — tiger: "right in the beginning when I start moving it snaps/rubber bands me, then I move fine"; mismatch record: 7 mismatched ticks at entry, world-feet-position x/z up to 2.62 m + state.velocity rows, 0% drift steady-state after). Root cause: the place-transition projection reset discarded the traveler's own in-flight input tail. Walking through a portal, the client keeps sending tick-stamped movement frames for the travel round-trip (the human can't see the new place yet); the server correctly applies them in the NEW place (walk-through-the-portal continuity). The client's reset handling adopted the snapshot wholesale and WIPED the stored input history (
getPlaceTransitionTickin runtime-client), so it could neither replay the tail nor re-predict the ticks between the snapshot and its own lead — a guaranteed divergence of the tail's displacement, hidden by the 30-tick place-transition compare-suppression window, then cashed out as one visible correction at the first compare. A second seam booked a phantom drift mispredict at the travel tick itself: pending compares referencing the abandoned pre-reset baseline ran one phase before spec-sync could stamp the settle window's resource. - Fix — the reset is reconciled like a correction, not a wipe:
ClientRuntimeHandle.noteProjectionReset(serverTick)(called from runtime-worker'shandleProjectionReset): drops pending compares against the abandoned baseline (same rule asadoptLatestAuthoritativeBaseline) and schedules a reset-anchored rollback+replay of the surviving input frames afterserverTickat the next simulation slot — presentation-invisible (runs before the tick's simulation phase commits). Joins and idle travels replay nothing (no frames after the baseline) and keep today's behavior byte-for-byte.- The place-transition input-history wipe is removed. Replays can never reach pre-reset ticks — both oplogs rebase to the reset's baseline, so no mismatch anchors before it.
- The reset replay rolls back with a WHOLESALE scope (
wholesale: trueonrunResimulation): a selective scope derived from the old baseline's deltas would leave the reset's own writes (place membership, teleported transforms) stale in the client oplog base and book phantom compares the next time the entity changes. - Reset replays are baseline maintenance, not mispredictions: they no longer book a mismatch tick into resimulation stats.
isRemotePredictionEntityreadsTomeBehaviorRefthroughtryGetComponentlike every other component read in the classifier (the one bareworld.getthrew on minimal test registries).- New fail-on-parent suite
tome/__tests__/place-travel-input-tail.test.ts: real server netcode + input server (router/acks) + tome input applier + behavior update +ObjectAPI.enterPlace, against the real client runtime (prediction capture, mismatch detection, settle window, resimulation) wired exactly like runtime-worker, over a transport with real latency. Pins: walking in the origin place predicts cleanly; the server player lands at the destination spawn and KEEPS WALKING on the in-flight tail; and the whole travel → stand → first-movement sequence books ZERO mismatched compares with both sides converged on the tail's end position. On the parent the same run books a drift mismatch at the travel tick (server at home spawn + tail, client still on the old-place prediction). - Continuous-play sessions no longer freeze 3-5s every ~10 minutes (ledger #147 — tiger's team report:
[kernel-startup] "Connection lost. Reconnecting..."on a clean 10-minute cadence). Root cause lives in cf-edge's container lifecycle, not the kernel: the@cloudflare/containerslib renews itssleepAfteractivity clock only when a request is PROXIED through the DO (containerFetch→renewActivityTimeout, once per request). A gameplay WebSocket renews exactly once — at the upgrade; after the 101 the frames flow through the runtime's spliced connection without invoking DO code. A room whose only traffic is its gameplay sockets therefore looks idle to the DO, and the lib's alarm loop SIGTERMs the container everysleepAfterwindow (10m) mid-session. Every player's socket dies with it; the client's reconnect (1s first retry, 1.5x backoff) lands on a container cold start — the 3-5s freeze. - Kernel half of the fix: the network worker (port 4001, the gameplay-socket owner) now answers
GET /connectionswith{ connections: <open gameplay-socket count> }(gameplay-connections-probe.ts). It lives on 4001 because that worker OWNS the socket table — the main container server's/health(port 4000) cannot see it — and 4001 is unreachable from the public internet (cf-edge forwards only gameplay-socket WS upgrades there), so the probe needs no auth. - cf-edge half (ships with cf-edge deploy, same change train):
GameContainer.onActivityExpirednow probes/connectionsbefore honoring an expiry. Open sockets → the stop is declined and the lib re-arms the clock (next probe onesleepAfterwindow out — one local HTTP request per 10 minutes, never on the frame path). Anything short of positive liveness evidence falls through to the default reap so idle containers never leak: zero connections, a 404 (pre-patch 5.0.0 network workers don't serve the path), no 4001 listener at all (pre-5.0 kernels), or a probe timeout (wedged kernel) all keep today's behavior. Until a kernel patch carrying this endpoint is pinned, rooms keep the old 10-minute reap — the fix completes when both halves are live. - Deliberate consequence to know about: a room with an abandoned-but-open tab (live TCP, nobody playing) now keeps its container running indefinitely instead of being reaped at 10 minutes. An open gameplay socket is the engine's definition of "someone is in the room"; if longer-lived containers turn out to matter for billing, the lever is counting only recently-active sockets, not reverting the liveness probe.
- Fail-on-parent suite
apps/cf-edge/test/game-container.test.ts(cf-edge's first vitest setup) drives the containers lib's REAL alarm loop under fake timers — simulated 10-minute windows, never wall-clock. On the parent commit the live-socket expiry SIGTERMs (the bug); with the fix it declines, re-arms, and still reaps once the room empties / the probe fails. Kernel-side probe matching is pinned ingameplay-connections-probe.test.ts, including never shadowing a WS upgrade. - Mantle wake-on-contact now requires an actual disturbance (
manifoldDisturbsSleeperinsolver/step.ts): a sleeping dynamic body wakes only when a manifold point is touching (pre-solve separation ≤ 0, where bias pushout acts) or the counterpart is closing faster thanSLEEP_LINEAR_VELOCITY(where the solver applies real impulse). Mere presence in the speculative band (0 < sep ≤ 2 cm) no longer wakes — mirroring the event book's no-hysteresis overlap rule (speculative points never counted as overlap either). - Why: resting configurations live inside the speculative band permanently — the character controller's padding gap is exactly
SPECULATIVE_DISTANCE(2 cm), and settled stack neighbors sit within slop. Proximity-waking made every sleeper adjacent to an awake body a period-16 asleep/awake oscillator whose phase flips on sub-epsilon pose differences. On predicting clients this was the dominant remaining mispredict class: a crate asleep on the server (PhysicsBodyState.status=1) stayed awake on the client for hundreds of counted ticks, micro-rotating ~0.001/tick, re-slept by every rollback restore and re-woken by the next step's wake pass. Probe-measured wake census in the crate-stack-shove grind: 12 of 13 contact wakes fired at separations 0.0085–0.0198 (speculative band), every one on the body's first asleep tick (sleepTicks=15). - The gate is a pure function of pose + velocity lanes (replicated state) — client and server gate identically; no new cross-tick state, nothing new on the wire.
- Grind evidence: crate-stack-shove (mantle) counted mismatch ticks 30/155 (two baseline runs, counter still climbing at session end) → 25/42 post-fix with the counter dead flat for the final ~23 samples of identical shove choreography; post-fix wake census shows zero speculative wakes (6 penetrating + 16 fast-closing, all legitimate). Stacks still topple and scatter identically; drive-vehicle mount/drive/dismount unaffected (its residual mismatches are the known steering-echo value class, no sleep-status rows).
- Physics-derived ObjectAPI reads quantize to their correction epsilon's grid at the read boundary (
tome/api/physics-read.ts). Server and client bodies legitimately drift below the mismatch-detector epsilons (corrections only fire above them), and raw reads leaked that sub-correction drift into behavior-script state:getVehicleSpeed()→ steering target →patchState({ currentSteering })flagged the exacttome/statecompare on every steer flip (105t mantle / 52t rapier in the car-thing acceptance ledger — the #2 volume class, engine-agnostic). Thetome/statecompare itself stays bit-exact; no compare-side fuzz. - Single source of truth for the grids: the mismatch-detector correction epsilons moved to
engine/prediction/epsilons.tsand are imported by the detector, the transform component correction thresholds, and the read boundary. Velocity reads (getVelocity,getObjectVelocity,getVehicleSpeed) snap toDEFAULT_VELOCITY_EPSILON(0.05 m/s); physics-driven pose reads (feetPosition/rotation/yawproperties, query/getPlayers/nearest/getControlTargetresult positions, query self-origins) snap to the pose correction grids (1 cm / derived quat-component grid with re-normalization);getWheelState.suspensionLength/.steeringsnap to the vehicle-config epsilon (1e-3);api.raycasthits against physics-driven bodies snap point/normal/distance; NPCmoveTo/fleedistances andcanSeeresults snap to the position grid. - The gate is
isPhysicsPoseDriven: dynamic bodies (rapier or mantle), character controllers, and children attached under either. Authored values never quantize — spec poses, kinematic script-driven bodies, local transforms, terrain/voxel/spline queries keep full precision, so authored set→get round-trips stay bit-exact. - The honest residual: a fixed grid cannot eliminate boundary straddles — two values within epsilon can sit one grid cell apart, with probability ≈ drift/grid per read (~1e-5 for the dominant 1-f32-ULP drift class at 1×ε grids). A straddle costs one tome/state correction blip, not an echo family. Pinned by
tome/api/__tests__/physics-read.test.ts, including an engineered worst-case midpoint test documenting the residual shape. getWheelState.rotation(wheel visual spin) stays raw: it is a render-only accumulator that drifts by radians across sides by design (compare-exempt in the detector); no grid can collapse it. Deriving replicated script state from it will echo — it is presentation-only.- Retained environment slot: equirect/sky-kind environment changes are texture value swaps on one engine-owned PMREM node per scene — environment flips and IBL texture arrivals no longer recompile the whole scene
- Uniform-class material values destructuralized: primitive tint, water material overrides, and model-batch color/metalness/roughness/emissive/opacity overrides ride retained uniforms and per-instance attributes instead of the structural batch signature — live recolors/tints no longer rebuild shaders or fragment instanced batches; sprite/text texture swaps stop bumping material.version
- Compile-queue hardening: build-window compile starts are bounded per frame and ride one shared async compile; asset-arrival and structural-sky scene sweeps coalesce through the existing 250ms window; sweeps never re-hide objects whose materials already compiled; new primitive lanes/pools/sprite batches compile hidden-until-warm instead of freezing the frame; scene capture and object preview stop forcing full recompiles; the CPU-bound frame report gains an honest "N materials compiled synchronously inside render frames" counter
- Visual instantiation is collect-budgeted: a single-frame content flood (mass spawn, place entry) is applied a few entities per frame under an adaptive ~6ms budget instead of freezing one frame for the whole flood, and the model handler packs batches once per tick instead of quadratically per attach; the frame report names the backlog while it drains
- 3d-rooms geometry scales with visible structure, not tile count: merged wall solids with hidden-face culling (~111× triangle reduction on a 64×64 layout) and colliders built from the merged boxes (482ms → 14ms create on the same layout); auto room lights cap at 256 keeping the largest rooms
- 3d-rooms layout parsing degrades instead of throwing: unknown glyphs read as empty space, blank/oversized layouts become empty/truncated terrain, malformed legend entries are tolerated — each with one deduped warning to Savi naming exactly what was wrong; an author typo can no longer wipe a place's terrain
- Decayed-input convergence: the input ack now carries the exact post-sanitize decayed axes the server applied on late-input ticks; the client rewrites its stored input history to match and resimulates — a network hiccup converges in one correction instead of booking a mismatch every decayed tick
- Pre-hydration honesty: a configured room whose spec fetch fails no longer boots the default playground as if it were the game — the world stays unhydrated (joins hold at Loading and retry) and the room re-fetches the real spec on capped backoff; run_script against such a room reports world-not-ready instead of an empty world
- Multiplayer client-realm parented spawns compose their World transforms at attach — child objects spawned into a hierarchy mid-session no longer render torn from their parent until the first movement
- God mode: material and property edits round-trip losslessly (flat map/tint edits no longer dissolve on the next tick); shapes placed from god mode default to static physics; mixed objects (primitive + audio/fx/light/particles) derive tuning chips for every family they carry; menu picks resolve by item identity so a shifting list can never apply to the wrong object; entering god mode no longer triggers the voice input (chorded Tab is never a voice press)
- Terrain water participates in lighting: the water emissive node honors emissiveIntensity (it was bypassed), and the terrain-liquid default of emissive=own-color at intensity 0 now contributes exactly zero — rivers and lakes stop self-glowing at night; authored glow (lava, magic) scales correctly
- /play/[updateSlug] rooms honor the slug pin instead of serving the latest publish
- Rapier character-controller contact debug labels read the obstacle normal correctly (floor contacts no longer logged as "ceiling")