engine v5.1.11
Engine v5.1.11
July 6, 2026
A patch in the Connection line.
what's new
- Savi can now invent animations for your characters — a swim stroke, a dance, a wing-flap — by writing them as motion code, no animation files needed. They play (and blend) just like regular animations, and edits take effect at the next loop so nothing pops mid-motion.
- Savi can now watch your game in motion, not just look at frozen screenshots — she grabs a short filmstrip of live frames, so she can actually judge whether the walk cycle slides, the jump feels floaty, or the boss swing lands on beat.
- Game managers can now be read from anywhere! Add
replicate: "world"to a manager object (like your game's phase or score tracker) and every player can see its state no matter which area they're in — no more copying state onto each player to work around it. - Scripted splines work again: paths that sweep a custom cross-section you describe in a script (walls, tubes, ribbons — any shape) now save properly instead of being rejected. Scripts can also scatter objects along the path — lamp posts down a walkway, pillars under a bridge — and they'll appear right where the path runs.
- Effects no longer silently switch to a lower-quality renderer under load — the engine fits many more effects at once (128 default-sized effects on desktop, up from ~12–16), and anything truly beyond budget waits its turn instead of changing how it looks. When an effect does have to wait or shrink, the engine now says so instead of staying silent.
- Fixed a bug where a big world could get stuck blinking — the whole scene vanishing and popping back every few seconds, forever. If a piece of scenery ever fails to reach a player now, the world stays put and the engine reports the problem instead of strobing (that one piece may need a reload to appear).
- Fixed a leak that could slowly fill up your graphics card during long sessions in big worlds — especially ones that rebuild the scene a lot. No more "my GPU crashed" after a long build session.
- Animated characters no longer show up pitch black in softly-lit scenes. If your character model looked fine near a lamp but turned into a silhouette everywhere else, that's fixed.
- Feet touch the ground: sprites with empty space baked below the art (most generated side-view creatures and props) used to float above the floor — now they stand exactly on it, and their colliders line up with what you see.
- Your ambient light now does what you asked: worlds that set the ambient color and brightness in the common flat form used to silently render a dim gray instead. Existing dark 2D worlds get the brightness their creator originally wrote as soon as they update to this engine version.
- Cameras tuned with a small smoothing value (like 0.15) now follow the way you meant — snappy, not floating seconds behind the player. The engine recognizes the common "fraction per frame" style and applies the matching follow speed.
- Generated shapes now render both sides — backwards-facing surfaces stop going invisible. When Savi writes procedural geometry (roofs, furniture, terrain details), a face wound the "wrong" way used to vanish when viewed from the intended side; now it just renders.
- Name tags sit where they belong on resized characters: giants no longer wear their names inside their heads, and tiny characters no longer have names hovering way above them.
- Rigs and builds made of many custom-geometry panels that share one material script (like skeletal character rigs) no longer freeze the game for seconds on first spawn — all panels now share one compiled material, each still rendering its own colors and atlas regions.
- Scripts that return odd values no longer lose their changes: if Savi's script ended by returning something that couldn't travel back in the reply (a live logs handle, a function), the engine used to throw away everything the script had built — "my changes keep disappearing." Now the changes land and only the return value is replaced with a note explaining what to return instead.
- Savi's script changes stop vanishing over reply problems, part two: a script that successfully changed your game but returned a value the reply couldn't carry (a circular structure, a BigInt) used to have the whole answer thrown away — Savi was told her changes failed, rebuilt the same thing by hand, and the saved game never got the changes. Now the changes always land and save; only the unreportable return value is replaced with a note telling Savi what happened.
- Editing scripts while your game is running is much more reliable now: event handlers always pick up your latest code — no more "my fix doesn't work until I restart the room", and no more handlers firing twice (or four times) after repeated edits.
- Live script edits can no longer leave an object secretly deaf to events: if an edit orphans an object's old event handlers, the object now tells you loudly — naming the event and the script, with the fix (re-save the script; if it stays deaf, restart rooms) — instead of your correct fix looking like a no-op.
- When a script accidentally uses the wrong import/export style, the error now explains exactly what to change instead of a cryptic one-liner — so Savi fixes it on the first try instead of guessing.
- World thumbnails and Savi's screenshots recover cleanly when the graphics engine stalls, instead of silently hanging forever.
- Your world's shelf image refreshes while you build — the picture on your worlds list updates every few minutes during a live session instead of freezing at the first screenshot.
- Savi learned from this week's play sessions: she now fully deletes things you ask her to remove (no invisible leftovers), makes "hesitant" NPCs pause to think instead of moving in slow motion, wires up saving correctly for spawned objects, and has worked examples on tap for combat waves, enemy AI, and damage zones.
- Enemies and props whose 3D models are still generating no longer get reported as permanently broken: the engine used to log hard "extraction error" failures while a model was simply mid-cook (so Savi and dashboards read your placeholder pills as abandoned), and now it correctly treats "still generating" as waiting — it keeps checking on schedule and the real model swaps in the moment generation finishes.
- Fixed a chronic multiplayer glitch family: after a reconnect or a shaky connection, players' body parts and other respawning objects could silently stop syncing to everyone else (the server kept rejecting them as "already exists"). They now heal in place automatically.
- Building in god mode no longer leaks editor gizmos into the shared world — friends will stop seeing stray rings and handles hovering around objects you were editing.
- Fixed the worst "my game is frozen for one player" bug: on a slow or congested connection, the server could drop the big world-snapshot message without noticing, and that player's world stopped updating forever — joins that never load, props that never spawn, refreshes that don't help — while everything looked connected. Both sides now notice within seconds and resend a fresh snapshot, so the worst case is a brief hiccup instead of a permanently frozen world.
- Sky clouds are back: worlds using the realistic sky's built-in cloud layer have been rendering an empty sky since engine 5.1.9 — the settings were right and saved all along; the engine was quietly throwing away the clouds' first paint while their shader warmed up, then never repainting. It now retries until the paint actually lands, so your clouds show up seconds into the session, every session. Night skies get the same fix for the milky way.
›technical notes
- Authored ambient light is honored in both spellings: the runtime read only the schema-nested
atmosphere.ambient.light.{color,intensity}, but the recurring authored shape is flat (ambient: {color,intensity}) — the truthy object spawned the light while both fields read undefined, silently rendering gray0xd0d0d0@ 0.5 instead of the authored values (two independently authored dark 2D games in one night: 0.9 × pale lavender and 0.6 × violet both shipped as 0.5 gray).resolveAmbientLightDef(atmosphere-sky-utils) resolves both shapes, nested winning per field, at both read sites (atmosphere-sync light spawn, explicit-intensity collection for the darkness convention). A once-per-room[spec-shape]warn (runtime log + DM, spec-shape-warnings.ts) keeps the drift visible when the flat spelling is used. - Side-scroller camera
smoothinggained a unit-guard: the field is an exponential follow rate in s^-1 (default 5), but the universal gamedev prior is the per-frame lerp alpha — a value in (0,1), which read as a rate gives a multi-second time constant ("camera follows me super slowly": 0.15 → τ = 6.7s). Values in (0,1) are never a sane rate and always a plausible alpha, soresolveSmoothingRateconverts them as a 60Hz alpha (rate = −ln(1−alpha)·60; 0.15 → ≈9.75 s^-1, within 3% of the skill example's 10 — the authored intent is recoverable exactly). Rates ≥ 1 and the ≤ 0 snap convention pass through untouched. A once-per-room[spec-shape]warn names the conversion. Not a 5.1.10 camera-attach regression: rate semantics are unchanged since #6713; the top-down camera shares the same rate vocabulary and is a candidate for the same guard as a follow-up. - Sprite metadata hydration consumes
anchor_uv(+view) from MCDN2dmetadata: unanchored side-view sprites getanchor: [0.5, 1 − anchor_uv.y]written server-side (replicated inline like the hydrated size, same push-classified prediction posture), putting the declared foot line at the entity's feet. Foot anchors only apply in the lower half of the image (anchor y-up < 0.5 — floating/centered subjects like bats keep the engine default) and only for non-topdown views; authored anchors beat hydrated ones with the same bookkeeping as auto sizes (revert on texture swap, camera-attach carve-out), and so do authored colliders: any body not carrying theautoCollider: "sprite"marker (collider2d spriteHull, explicit shapes) skips anchor hydration entirely — those hulls are a pure function of the spec anchor and are never rebuilt from hydrated state, so moving only the art would sink exactly the worlds that already fixed floating with an authored hull. An anchor hydrated before a collider became authored reverts on the next pass. The hull collider upgrade maps through the resolved anchor in the same pass, so the visible art and its convex hull move together — closing the tiger-corpus "collider of ground is above the ground" class, where collider math was exonerated to the pixel and the float was pure canvas padding (fox 29.3%, stump 19.1%). - Fixed the 5.1.10 whole-world blink loop: the #818 bespoke-geometry heal escalation (stream reset + fresh snapshot) had no retry bound, and each reset erased the renderer-side attempt ordinal it was gated on (the reset's despawns resolve the wait tracker's entries), so a wait whose geometry the recovery snapshot also failed to land re-armed the collapse every ~15s forever — on a large creator world the entire scene despawned and rebuilt on a loop indefinitely. The loop is also a GPU killer: each rebuild cycle leaks ~6–8 textures/programs/materials (ledger #878, the separable disposal-hygiene fix), so ~150 uncapped cycles crashed the graphics device in ~39 minutes; capped at 3 the leak is ~20 resources — unreachable as a crash. Escalation is now bounded by a sim-side epoch that survives the resets: at most 3 recovery snapshots per epoch with doubling backoff (~15s, +30s, +60s), then the heal stops re-resetting and surfaces a structured fault (
bespoke-geometry-heal-exhausted) on the engine-diagnostic rail — runtime log (getLogs), a one-time DM, and a server-console breadcrumb so the class is observable from dashboards (every prior signal in this loop was browser-side only). Plain live-value re-forwards stay first-line between and after collapses; an epoch decays after 120s of request silence, so later independent drops get a fresh heal budget. - The renderer-host
captureScreenshot()RPC gains the 15s timeout + pending-entry eviction its siblings already have (ledger 862, #8025). It was the one pending-map RPC inbrowser-init.tswith no timeout: a wedged render worker (e.g. a software-WebGPU adapter that boots to 'ready' but stalls the compositor) silently deadlocked every caller awaiting a screenshot — the seo-thumbnail, chat-screenshot, and savi-notes capturers. Now matches the sibling pattern (renderObjectPreview/captureSceneView): 15s timeout, evict, resolve null — the failure value all three callers already handle. No worker-side cancel lane needed: screenshot requests batch-capture the live frame, so a stale queued id costs nothing and a late result no-ops. - Behavior and lib-module compile failures from residual ES-module syntax now teach the two-realm rule instead of surfacing the raw parser message (ledger 876, #8074).
describeEsModuleSyntaxErrorexisted but was wired only into the terrain/geometry/spline generator paths; it's now in thecompileModuleandcompileBehaviorcatch blocks, so 'Unexpected keyword export' arrives with the fix: lib/ modules are CommonJS (module.exports); behavior scripts may export hook functions but not export lists. The detector positive-matches V8's exact module-syntax messages (not substring greps), so a runtimeJSON.parsefailure quoting the word 'export' can never be mislabeled as an ES-module error. Generator messages stay byte-identical. Error-message channel only — no compile semantics, module resolution, or cache changes. - require() failures name their probe path (#8081, message text only): 'Module not compiled' now appends the probe list the resolver walked (
mods/{mod}/lib/…,{mod}/lib/…,lib/…), truncated at the key it settled on, so the reader sees which probe should have hit without excavating the resolver; the runtime require error names the same namespace list as the scan error (builtin/*,lib/*,mods/*/lib/*). Resolution behavior unchanged. - fx-gpu arena reservation is now an ABSOLUTE per-population default (
FX_GPU_DEFAULT_MAX_PARTICLES= 8,192) instead of a 1/16-arena share bounded by a quarter of remaining free slots. The old free-share rule made exhaustion a reservation artifact: ~12–16 defaulted populations reserved ANY arena size to zero while almost nothing was alive (the toybox specimen: 12 effects → 786k of the 1M desktop arena reserved, authored-3000 requests halved to 1536/384 and then refused). Desktop (1M slots) now holds 128 concurrent defaulted populations, mobile (256k) 32; authoredmaxParticleskeeps winning over the default; tier capacities unchanged. - Genuine arena exhaustion (nothing evictable — everything resident is hot) now REFUSES the newest effect (arrival order) instead of silently rerouting it to the CPU particle renderer. The refused effect's entity stays valid, holds no arena allocations, renders nothing, and retries on every sync — plus immediately when a finished effect frees slots mid-frame — claiming space in arrival order. Nothing already alive is ever culled (the #7630 eviction invariant extends to refusal). The
gpuFxAvailability: "unavailable"device-loss branch is untouched; capability-bound routing (ribbon sinks, path sources, encoder caps) still lands on the CPU backend. - The arena-pressure tell (ledger #848): refusals (
fx-arena-refused), arena-tight clamps (fx-arena-pressure), and surviving capability CPU fallbacks (fx-cpu-fallback) now ride the engine-diagnostic bridge — the renderer-worker → page → parent path that reaches Datadog ([renderer]-prefixed console re-emits in browser-init, forwarded by client-error-forwarding) and Savi's getLogs + render-category DM pointer. One structured line each (population id, reason, sizes), rate-limited to 10 per room session at the emitter with a matching page-side budget. Previously these were renderer-worker console.warns that reached nothing (0 prod events in 7 days while local digs showed 12+ per room). GpuFxStatsgainsrefused(entities + reasons) andarena.refusedEffects;arena.exhaustionFallbacksis renamedexhaustionRefusals(it counts refusals now, not fallbacks). scripts/verify-fx-gpu-coupling's arena-pressure scenario pins the new refusal contract on a real WebGPU device.- Fixed a GPU resource leak on entity despawn: the renderer's teardown (
disposeObjectTreeinthree/entity-object.ts) disposed geometry and materials but stranded every texture bound to the material's slots (map, normalMap, roughnessMap, …). Every full scene rebuild — stale-backlog stream resets, bespoke-geometry heal escalations, place transitions, script-driven despawn/respawn — leaked the despawned entities' texture sets until the graphics device was lost (production dump: 1224 live GL textures / 210MB texMem on a 145-draw scene after ~150 reset cycles). Despawn now disposes owned texture slots; shared instances (asset-service cache, cached-model scenes, placeholder singletons) are ownership-marked at mint (materials/texture-ownership.ts) and skipped, so refcounted caches stay authoritative over their own lifetimes. - Verified and pinned with a residency regression test that the stream-reset row ("despawn whatever the reader holds") routes through the same disposal path as ordinary despawns, and that reset-survivor entities still reattach warm resources without a dispose-and-rebuild.
- Motion eye (#7977):
view_live_scenegains a burst param ({frames?, spanSeconds?}, defaults 4 frames / 1.5s) — N tiles of the LIVE scene captured at deterministic playout-tick offsets and composited into ONE image strip with per-tile tick stamps burned in. Grids quantize to multiples of the tick spacing, so two bursts taken at arbitrary times stay comparable frame-for-frame. A burst is one entry in the existing scene-view lane: viewport bursts grab the canvas bitmap (2D ortho games too); camera/frame bursts re-render from a pose locked at burst start with visibility re-collected. Wedge-abandon is skipped while a burst progresses (own stall deadline); timeline rebase aborts truthfully; overlapping bursts reject at enqueue; host-timeout cancel stops an in-flight burst. Strips skip the 768px downsize, encode at built size with a caption bar, quality-stepped under the ~1MiB WS message cap. Bursts fail loud with no client — motion never degrades to a stale still. - Nameplate/text anchor offsets now stretch with entity scale (#8051). The draw/text offset was applied rotated but unscaled — one world-unit constant for every body size, so scaled-up characters wore nameplates inside their heads and scaled-down ones carried them mid-body or floating. Text anchors now compose P + R·(S∘offset) — the exact composition
model.offsetalready uses and the frame world-bounds already assumed. Scale (1,1,1) reproduces the authored offset exactly, so default-size characters render identically; glyph size stays in world units (readable at any body scale) — only the anchor offset stretches. Zoo objects place gains a nameplate × scale exhibit row (½×/1×/2× capsules, same offset). - Loud fault floor for orphaned event subscriptions (ledger 826, plan p-64fd35d9 piece 1, stage 1 of 2): every script-edit landing path (applySpec's changed-script sweep, the client-auth fold lane) bumps a per-ref edit generation and marks the subs the old script version wired as stale; covered classes tear those down and re-register in the same apply. At dispatch, an entity whose name-matching subs for an event are entirely stale-generation (judged before the place filter — the incident's deaf ears were place-filtered stale subs) faults once per (entity, scriptRef) through the existing behavior-fault machinery, with re-save/restart cure guidance in the message. Zero added steady-state cost: all bookkeeping is edit-time. The behavior-error DM cap was raised 240 → 500 (matching the engine.diagnostic rail, ledger #235 leg 2) so the cure tail survives the DM's hook/script/entity prefix. Pairs with the stage-2 rebind sweep changeset — both are intended to ride the same release train.
- Unified live-edit rebind sweep (
rebindEditedBehaviorsininterpreter.ts): one pass over all liveTomeBehaviorRefentities replaces the three class-gated re-run blocks (updateObject'sbehaviorChangedbranch, the player-diffplayerOnSpawnre-run, the dynamic hot-reload loop +rerunOnSpawn). Closes the entity classes the old blocks missed (ledger 826 / THE TAPE incident): legacy row-backed entities withoutbehaviorRefs, player handler/timer stacking, behavior-removed defs, and preserved-stale compiled modules. - Fold-lane coverage:
foldSpecMutationsIntoWorldwith script-content changes now schedules a realapplySpecof the folded head (previously foldedsetScriptleft every live consumer on the old module until restart). - Teardown records subscription tombstones (
TomeSubscriptionTombstonesResource), cleared on re-registration and reaped on entity destroy + world reset (id reuse must not inherit them). Event dispatch faults an emit whose name-match for a tombstoned entity is EMPTY — the loud, class-truthful end state for subs wired outsideonSpawnbehind persisted guard state (a plain re-save does not re-arm them because the guard persists). - Dangling
runScheduleentries whose timers all fired are reaped on rebind (reapDanglingSchedules). __spawnCollectRendererDebugSnapshotaccepts snapshot options (#8080, apex client debug tools piece W1C):WorkerDebugSnapshotOptionsthreads through both collector install sites (pre-mount entry + worker-browser-host) togetWorkerDebugSnapshot, so an automated browser can request option-gated blocks ({ includePerformanceProfile: true }etc.) programmatically instead of scraping the F2 inspector's lastSnapshot. Options only select what the snapshot INCLUDES — arming stays on the inspector/worker-message path; optionless calls are byte-identical to before (pinned by test). Debug surface only.- New opt-in
replicate?: "place" | "world"on ObjectDef/ObjectSpec (default "place" = previous behavior; absent field = absent behavior).replicate: "world"makes a designated object (global managers: game phase, scores, wave state) readable from every place: theTomeWorldReplicatedmarker is synced from the def on spec apply and api.spawn; the server egress fill (addGlobalAoiEntities) adds flagged entities to every AOI bucket's visible set alongside the existing hardcoded global set; and the client spec filter (filterSpecForPlace) keeps flagged defs in every place so the applySpec reconcile no longer destroys the local replica on a place transition out of the manager's place. READ path only — the owning place's simulator still solely runs the object's behavior (no execution gate reads the flag). Reads of a replicate: "world" object are eventually consistent — a client sees the owning simulator's write after one replication hop (simulator → server apply, then the server's next tick's delta fan-out), typically ~1 network round-trip + 1 server tick behind; read-after-write is guaranteed only on the machine that simulates the object, and reads go null while the object's place is unloaded — put global managers in the default place, which never unloads. Zero flagged entities = byte-identical visible sets; a log-only soft cap (WORLD_REPLICATED_SOFT_CAP= 16) warns on excessive fan-out without clamping. - run_script replies degrade piecewise at the structured-clone boundary instead of discarding the staged mutation batch (ledger #835, P1 data-loss class). Previously, when an exec's reply defeated structured clone — a script returning the
getLogs()proxy, a function, a live handle — worker-main.ts/live-port.ts replaced the ENTIRE response withok:false, mutationCount:0, rolling back world changes that had staged successfully (24 hits/4h on one app; 15+ apps fleet-wide in one window). The newexec/clone-fallback.tsserializes the TransactionLog and the return payload independently: the batch crosses and merges, the return value alone is replaced with a note naming the offending shape ("value omitted; your world changes WERE applied"), andtome.exec.result_not_serializablekeeps the class warn-visible host-side withchangesAppliedtruth. Preserved semantics: a THROWING script still rolls back (transactional failure ≠ cosmetic reply failure); a TransactionLog that itself defeats clone stays an honest changes-NOT-applied error; an undeliverable degraded reply falls back to an all-scalar verdict. Mirrors the decoupling the JSON wire boundary already had (sanitizeScriptResultForWire). - run_script replies now degrade piecewise at the kernel's JSON boundaries too, closing the remainder of the ledger #835/#841 data-loss class. The clone-fallback fix covered the structured-clone transports, but a return value that survives structured clone can still defeat
JSON.stringify(circular refs, BigInt): the container exec route's reply serialization threw, the catch answered 500exec_failedwith the mutation batch dropped, and studio-chat — told failure — never persisted the batch to kiln while the live world kept the merged changes (told-failure stopped meaning not-applied). The route now passes results throughsanitizeScriptResultForWire(the same sanitizer the singleplayer wire already used): verdict, mutations, and logs always cross; only the return value degrades to a note. The runtime-worker RPC reply got the same decoupling (container/tome-exec-reply.ts) — a reply-only postMessage failure no longer reports an applied exec asok:falsewith the batch dropped. - Script-authored skeletal animation clips (#7986, p-d4228e95): a named function of time plays wherever a clip name goes.
updateChannelsamples it once (fn.duration×fn.fps) into keyframe tracks of rest-pose offsets, parks them in the spec (assets.authoredClips, identity<scriptRef>#<fnName>), derives a forwardedAuthoredClipscomponent (material-scripts pattern), and the renderer composes them per rig into real THREE.AnimationClips beside GLB clips — no per-frame creator JS. Lib exports keep their module identity via a compiler WeakMap; anonymous/inferred names reject loudly; raw keyframe tracks are accepted at the same play site (tracks#<name>). Edits re-sample under the same identity, and playing layers swap at their next loop boundary without resetting channel phase. Binding contract reports through the diagnostics rail (3 new codes: unbound tracks, non-joint targets, net-drifting Hips translation); skinless models no-op with a diagnostic. Horde gate: authored identities are complex mixers (clone path only). Zoo: authored-clip swimmer station in the IK zone. - Fast-follow (#7996): per-identity mint-velocity guard in the authored-clip fold — a function whose sampled output changes every call re-mints per tick (per-tick spec writes + full-spec re-replication + patchAssets churn); warns once per episode through the diagnostics rail, silent on identical-hash plays and genuine edit cadence.
- Skill teaching (#7998): the animation skill teaches clip-function authoring and the traps that make clip functions look right first try.
- Structural lease keying for bespoke-mesh scripted materials (ledger 879). The mesh handler's material RefStore leased by FULL material value, so N custom-geometry panels wearing one script with per-panel uniform params (atlas-region fences, tints) minted N material instances — N synchronous TSL→WGSL builds at ~50–90ms each. The victim shape (a 69-panel skeletal rig baked by
rig-bake.js+material-atlas-region.js) froze the render worker 3–6s on every first spawn, every re-spawn past the 2-minute idle retention, and every join with the rig saved in-world. - Scripted leases now share by structural family (script ref + live source hash + overrides + geometry traits — everything except the script's param values). A candidate value rides an existing variant only when its param KEY SET matches and every differing entry passes
canApplyScriptedMaterialParams— the scripted runtime's own build-derived classifier. Params the build baked (plain-JS consumption, structural reads, strings, falsy-at-build) split into their own lease: values that change codegen never share, so wrong-sharing is impossible by construction. 69 uniform-only panels → 1 material instance → 1 node build (~2 with an additive lane). - Per-mesh uniform values ride a per-object param bag (
setScriptedMaterialObjectParams): registered param uniforms carry anonObjectUpdatecallback reading the render object's bag, and three clones objectGroup bindings per RenderObject — one compiled material renders every panel with its own values. Objects without a bag (primitives, previews, warm meshes, model clones) are untouched: the callback returns undefined and the material-level value keeps applying. - Scripted param ticks no longer re-home/clone leases (the shareKey stays pinned to the built params); changing a value's param key set now rebuilds instead of silently inheriting the stale uniform of a removed key. Store retention now keeps a whole rig warm as ~2 idle leases instead of 69, so despawn/respawn and stream-reset recovery reattach without recompiling.
- Spline
kind:"scripted"mints, spawns, and persists again (ledger 855). The engine grew the kind in 2026-05 (#6496 — types.ts, scripted-spline module, custom-geometry skill) but@spawn/tome-schemasnever learned it, so the kiln persistence gate's in-area rule rejected every spawn/setProperty carrying one — authored-YES, runtime-NO on every installable engine, 0-in-11,052 corpus usage (breakage, not disuse).SplineKindSchemanow includes"scripted"andSplineSpecSchemacarriesscript/params/seedto match the engineSplineSpec. - Scripted-spline
children(ctx)is wired into the lowering:expandSplineObjectsnow distributes the returned ObjectSpecs along the path (ids namespaced${owner.id}__spline__…, taggedspline-generated, owner realm/audience/lifetime inherited — the standard generated-child lifecycle). The frameschildren(ctx)reads are world-space, so positions computed fromframe.pointare validfeetPositionvalues as-is. A profile() that lofts no mesh no longer gates children; a scripted spline may defineprofile(),geometry(), orchildren()(compile requirement widened accordingly). - Skills teaching wave — settled clauses and woken examples across the skill corpus:
- Four savi-quality clauses (#7988): cut = delete the source (flags/zeroes/kind:off leave corpses); deliberate/hesitant NPCs = decision cadence, never movement-speed nerfs; persistence routes by entity class (terrain/voxel edits auto-persist on persistent places; runtime-spawned entities are the class needing explicit wiring); camera felt-language routes to the builtin sensitivity knob.
- Sprite auto-flip exact contract + probe-survey playbook for atlas recooks (#8014): the one flip the engine owns (single clip named exactly walk/run/idle auto-mirrors to travel direction), and hidden-probe
getChannelduration polling to verify a recook landed per facing. - 2d-mode skill (#7960, tiger corpus P1+P2): a light the character carries is a point light parented to the player (mood lives in the world; readability rides the player); far strips are sized to the window, not the world, with the feet anchor below the LOWEST floor so no dip opens a backdrop gap.
- Six dormant bare-
@skillexamples woken into live skill routing (#7995, #7999): wave-manager, arena-player, damage-zone → combat; enemy-ai → npc; mountain + river terrain → heightmap-terrain. Bare@skillis now a generation error naming the file, const, and working grammar, so examples can't silently sleep again. patchState/patchObjectStatedocs teach the deep-merge law + the real delete idiom (#7953): top-levelundefineddeletes; nestedundefinedassigns (the key survives); the real nested delete isdelete getState().inv.swordthrough the tracked proxy. Two kernel tests pin the semantics.
- Fixed skinned GLBs rendering black in ambient-only lit scenes (ledger 875, #8070). Three's WebGPU codegen drops the ambient+hemisphere contribution for SkinnedMesh draws whose material has
vertexColorsenabled, and every Meshy-style skinned export carries a solid-whiteCOLOR_0— so characters rendered black wherever the scene had no direct light. Fix at the per-character clone site (prepareMutableSkinnedTree): when a SkinnedMesh geometry'sCOLOR_0is uniform/near-uniform white — or absent — vertex colors are disabled on the cloned material via the loader's existing sanitize vocabulary (disableVertexColorsOnMaterial). Lossless for the hunted class; genuinely tinted skinned characters keep their vertex colors; the loaded source asset is never mutated. - Stale-client diagnosability (#8058), debug + CI surface only — no renderer behavior changes. The F2 inspector's Client section gains the engine hash (play-origin pathname), the persisted adaptive-quality landing read side-effect-free (rungId, boot cuts, lease age, fingerprint verdict), the boot-APPLIED landing recorded at the browser-init load site (applied-vs-stored), and a quality-reset button routing through the existing
RendererHandle.resetAdaptiveQualityentry via the inspector dispatch channel. - Publish guard against hash-prefix overwrite:
refuse-client-prefix-overwrite.shdoes one head-object existence check before any client-bytes upload and hard-refuses naming the hash (client bytes are addressed by the server tarball's hash — overwriting poisons immutable caches). Fail-closed: only a confirmed 404/NotFound passes; network errors/throttles/bad creds refuse distinctly.--force-republishwarns and continues for byte-identical retries. - One winding convention for savi-authored geometry (jacob's call, 07-04; #7975 review follow-through). Deleted the emission-time index reversal in
ctx.tri/ctx.quad(tome/geometry-context.ts) — the scripted path silently had the OPPOSITE visible side from rawkind:"custom"buffers because the reversal canceled the renderer's uniform CW→CCW flip (buildBespokeGeometry), which stays and now applies one convention to every bespoke source. Paired with it, opaque bespoke-mesh material leases default toTHREE.DoubleSide(renderer/three/meshes/index.tscreateMaterialLease), matching the batched bespoke lanes (always DoubleSide) and making the convention change visibility-adding rather than visibility-flipping. Scoped to opaque: transparent + DoubleSide + !forceSinglePass makes three render two passes (BackSide + FrontSide, 2x draws), so transparent meshes — explicit or viactx.colorvertex alpha — keep FrontSide; an authoredsideoverride always wins. Measured at 162-prop village density (measurement 6c884bb2): cost below noise floor, pipelines/draws identical (side is baked into the shader cache key — the default swaps which pipeline compiles, never adds one), shadow side follows and stays invisible. - The seo-preview capturer re-shoots every 4 minutes while a session stays live (visible tab only), instead of once per page session — world shelf images now track the build as it happens. Re-captures reuse the stable
seo-thumbnailclip id so kiln's clips/register upsert (variant_id, clip_id, kind) replaces the variant's one seo-preview row instead of inserting a timestamped row per shot; register bumpscreated_aton re-register so newest-clip consumers surface the refresh. Each re-capture runs the existing asset-quiescence gate and dark-frame retry; a re-capture that stays dark after retries keeps the previous good shot. - glb-bounds jobs classify MagicCDN "202: still generating" as a DEFERRED outcome instead of a job execution error (ledger 887, #8095). The deferral machinery was already correct (the job tags
httpStatus: 202andBoundsPrefetchFeaturere-asks on the exponential→slow cadence until the asset lands — ledger 643); the bug was classification at the harness boundary: the outcome vocabulary was done/error only, so every probe against a cooking model error-logged a fullBoundsExtractErrorstack withretriable:false— an expected wait dressed as permanent failure (13× in 5 minutes on one prod app, reading in DD as bounds extraction abandoned while the enemy models were simply mid-generation; creator report: enemies stuck as pill placeholders). Nowglb-bounds-job.tstags the 202 errordeferred: true(still ends the run — in-worker second-scale retries can never outwait a 5–10 min cold cook),harness.tssettles it at debug with wire code"deferred"(one compact line, no stack), and the client jobs path applies the same classification. Real failures keep the exact ERROR path; the 5s→5min re-ask schedule,TransientJobErrorin-worker retries, and 401/403/400 park semantics are untouched. Pinned end-to-end: a 202 settles deferred with one debug line, and the re-asked job extracts bounds when the fetch later returns 200. - Owner-idempotent creates at the client-auth drain (
state-delta-apply.ts), closing the chronicnetcode.state_delta.row_rejected"create row id already exists" class (ledger 884 family 2, ~358/day prod-wide). Client-auth ids are deterministic (parent-qualified derived children — rig bones, blueprint children), and their delete rows are easily lost (outbox prune on socket churn, non-Streaming ingress drops, flood-budget oldest-drop), leaving a stale server copy every same-id respawn collided with forever. When a create row's id already exists AND the sender could legally have deleted+recreated it (owner resolves to the sender, or unowned remainder of the sender's live hosted place — exactly the delete gate's authority), the create now applies as an UPDATE (the drain-side mirror of the client'sconvertAdoptedServerCreatesToUpdates). Spawn-tier components are dropped from the converted row so a collision can never re-attribute an existing entity's chain; foreign collisions keep rejecting loudly. Conversions are counted (idempotentCreates) and logged rate-limited asnetcode.state_delta.create_applied_as_update. - God-mode tooling out of replication:
ensureHandleEntity/ the outline + connector twin spawns now stampTomeRealm:"client"+TomeSpawnedBy: tome/client-spawn, making handles first-class client-realm local spawns. Previously they carried only the anchor's copiedPlaceMembership, so a hosting creator's upload predicates (isHostSimulatedEntity: explicit place, owner walk unowned) claimed them and published god-mode gizmos as canonical unowned place entities — fanned out to every client as gizmo ghosts wearing the anchor's name, and re-hovers after a lost delete rejected as<anchor>/handle/…create-collisions. With the stamps, the upload view's existing local-plane filter excludes them andisClientRealmLocalSpawnkeeps them out of the hosted remainder — no predicate special-casing. - Patch-level fix for the prod silent-delivery-loss mechanism (ledger 884, families 1+3 — the highest-severity client-visibility bug): Bun's
ws.send()returns 0 when a frame is DROPPED under backpressure, the network worker discarded that return, and the sim — whose SAB-side send had "succeeded" — marked the frame delivered, advancedlastSentTickand the dictionary watermark on the lie. When the dropped frame was a reset snapshot the client sat inAwaitingResetforever, discarding every steady-state delta while heartbeats kept the socket looking healthy: a permanent, per-client, totally silent world freeze (Balmora join-freeze, Enigma props-not-spawning), with reconnects deterministically re-dropping the same oversized reset. Two independent halves, either of which now breaks the wedge:- Server truth: a dropped
wire.sendfeeds back from the network worker as awire.send_failedingress event; netcode flips that connection toNeedsReset(invariant, stated in code: the sim never leaves a connection marked delivered-to when the transport dropped its frame). The retry rides a 1s hold — the socket buffer that just dropped a frame is full, so no snapshot per tick is assembled into it — and the client's ownProjectionState Activerequest clears the hold early. Thews_backpressurecontainer log now splits outwsSendDropped(send()===0) from queued-late (−1), and each flip logsnetcode.egress.ws_delivery_failed(rate-limited per connection). - Client self-heal (survives the coming DO-relay transition regardless of what drops a reset): while
AwaitingReset, discarded deltas and elapsed time are counted; past a threshold (5s doubling to 30s, or 64 discards after a 2s floor) the client re-requests the reset.decodePayload → nullon a StateDelta stops being a silent drop too: it counts and requests a fresh baseline (unknown/non-delta payloads never do — forward compatibility). Fresh connections now startAwaitingResetinstead of ingesting pre-baseline deltas blind, which arms the same self-heal for the dropped-JOIN-reset flavor. Every self-heal surfaces as a page-realm[worker-browser-host] projection self-healwarn (rides the console-forwarding lane into kiln browser logs with game context) plusprojection.{selfHeals,awaitingResetDiscards,undecodablePayloads}counters in the websocket perf-diagnostics window.
- Server truth: a dropped
- Fixed the 5.1.9/5.1.10 cloudless-sky regression: sky-v2 integrated clouds (
atmosphere.cloudsonkind:"realistic"/"rayleigh"skies) rendered nothing — dial-independent, error-free, spec intact. Mechanism: the cloud coverage-field bake (sky-cloud-field.ts) renders its window bands through the fork's nonblocking compilation (vendor patches 0005/0007), which silently SKIPS draws while the bake material's node graph or GPU pipeline is cold — but the bake's planner counted skipped bands as baked and committed the window anyway (sampleInvWidthset over a zeroed back target), mapping every sky pixel onto coverage 0. No re-bake until the wind-drift accumulator crosses the re-center margin — a world with calm/slow wind never gets one, soclouds.enabled: truerendered a permanently empty sky with zero errors anywhere. The one-shot-producer contract (compilationSkipCountaround the draw — the exact mechanism patch 0007 added, which #7343 wired into the environment capture, PMREM, look-pass, and post-processing) was missing from the two sky bakes. Now: a skipped band rewinds the pending bake to that band and retries next frame; the window flips only after every band's draw actually submitted. Already-baked bands are not re-rendered on retry (the back target persists), and the committed sampler mapping never moves while a re-bake is owed — the pre-fix "no clouds, never garbage" invariant holds throughout. - Same contract applied to the night bake (
sky-night.ts): the dirty flag was consumed before the draw, so a skipped bake served a black milky way until the next authoring change. The flag is now consumed only when the draw submitted. - Both bake quads and materials are named (
skyCloudFieldBakeQuad,skyNightBakeQuad), so a genuinely wedged compile now surfaces through the compile-skip monitor'srenderer-object-draw-stalleddiagnostic (the retry renders every frame while owed) instead of going silent after a bad commit. - Verification honesty: the planner/commit semantics are pinned CPU-side (skip-simulating renderer double in
sky-cloud-field.test.ts+sky-night.test.ts); headless suites cannot certify GPU output. Device walk: boot akind:"realistic"sky withclouds: { enabled: true, density: 0.6, opacity: 0.85, altitude: 150, speed: 2 }(the garden repro dials) and confirm clouds appear within a few frames of the sky activating — on 5.1.9/5.1.10 that sky stays a pure cloudless gradient for the whole session.