engine v5.1.10
Engine v5.1.10
July 4, 2026
A patch in the Connection line.
what's new
- 2D games got a big placement upgrade: backgrounds and distant scenery can now be glued to the camera, so they always fill the screen — no more backdrops drifting away from the view, floating set pieces, or stretched skies. Repeating strips tile seamlessly across the whole view, parallax layers slide at their own speeds, and everything holds up at any screen shape from tall phones to ultrawide.
- Mid-ground scenery (trees, ruins, set dressing between the backdrop and the play area) now sits properly on the ground across its whole parallax drift instead of floating over dips or drowning in hills.
- onSoundEnd fires again: scripts that sequence dialogue or music off a sound actually finishing (instead of guessing with timers) work in real games, not just single-thread test harnesses.
- Building a place and walking players into it in the same script now works — and place-travel errors show up in your logs instead of vanishing.
- On phones, selection handles are now sized for thumbs — big enough to grab, never covering the object — and brush/draw tools have a visible ✕ to exit.
- Objects you attach to a parent in another place now go where the parent is, deleting a parent cleans up its attached objects everywhere, and Savi gets told when something can't be saved instead of it quietly breaking on the next load.
- Fixed a bug where one broken sprite could permanently turn off automatic sprite sizing and pixel-accurate collision shapes for every other sprite in the room.
- Spinning objects now tumble realistically instead of holding a fixed axis — long or flat objects thrown with spin will wobble and flip the way real ones do.
- Your game has a music system now:
api.music.play("cdn/music-….mp3", { fadeMs: 800 })starts or crossfades the room's soundtrack,stopfades it out,duckdips it under dialogue,now()tells you what's playing and where it is. Layers let you stack stems ({ layer: "drums" }). Players joining mid-song hear the right spot, and editing your UI never restarts the music. api.audio.duration(clip)gives you a clip's real length in seconds (null until it's known) — no more measuring rituals for timed sequences.playSoundfinally honorsbus:("Voice", "UI", …) so the right volume slider governs each sound, pluspriority:for lines that must not be culled.- Your game no longer creeps down to its worst-looking quality while you're actively building — loading hitches from edits don't count against your hardware anymore.
- Games that swap through lots of big 3D models (previews, galleries, dress-up rooms) no longer slowly fill graphics memory until rendering dies.
- Game controls and menus now stay clear of the iPhone notch and home bar.
- Editing big games is way snappier: script-heavy worlds apply Savi's changes in a fraction of the time they used to (a 3,000-object world went from ~2 seconds to ~0.3 per edit), and the engine no longer quietly does every edit twice.
- Saving game data just got a lot safer: the new
api.updateStoragereads your data, applies your change, and writes it back without ever overwriting someone else's save that landed in between — if two things touch the same save at once (double-click purchases, two rooms, an autosave racing a reward), both changes survive instead of one silently erasing the other. Perfect for coins, inventories, and gacha-style rewards that must land exactly once. - Save and load can no longer falsely report an empty save when storage is briefly unreachable — instead of your save looking blank (and risking a fresh session overwriting your real progress), the game now sees an honest "storage isn't ready yet" error it can retry.
- Worlds built by scripts that painted tons of individual voxels no longer freeze for many seconds when the room starts — and they stay snappy while you keep editing.
- Turning terrain off (or switching terrain types) now actually takes — no more ghost settings from the old terrain sticking around, and when a script gets rolled back, you're told everything it would have changed.
- Music patterns written in standard strudel vocabulary now either just work (
.lpq,.distort,.duck) or tell Savi exactly which vibe verb to use — no more cryptic "is not a function" crashes in your soundtrack. pbr: truenow works on custom geometry: sculpted and scripted meshes that paint textures withctx.albedo()get the same derived normal/roughness/metalness detail your textured boxes and terrain already enjoy — each textured face derives maps from its own texture, no extra authoring.- Editing big worlds got faster again: the engine now re-processes only the objects your edit actually touched — a one-object tweak in a 3,000-object world costs what it would in a 10-object world, and an edit that changes nothing is near-free.
- Worlds with heavy build scripts stay responsive: when lots of timers come due at once, the work spreads across ticks instead of freezing the room in one long stall — and any script that runs slow gets named in your logs (with how long it took and how to split it up) instead of freezing the game anonymously.
- Publishing from your phone works properly now: the cover screenshots Savi needs get taken on mobile too, and a game whose flythrough picture failed once will quietly retake it next time you visit instead of staying stuck.
- Edits that fail to save no longer disappear silently: when the server rejects a spec write (ownership race, oversized batch), the rejection now shows up in the game's logs and Savi gets told "N spec writes were rejected and did not save" — so "my changes keep disappearing after reload" becomes diagnosable in-session instead of a mystery. Writes from an object that was spawned an instant earlier no longer lose the race at all: the server briefly holds and retries them instead of dropping them.
›technical notes
- 2D camera-attach placement, wave 1 (#7449): new PositionSpec shape
feetPosition: { attach: "camera", x?, y?, depth? }— the object lives in the viewer's camera frame while the entity stays vanilla underneath (origin-parked feet anchor; networking, physics-sync, and AOI see an ordinary entity —aoi.tsbyte-identical). The renderer resolves placement against the presented camera (post-smoother, post-pixelSnap);sizeomitted = aspect-preserving frustum fill (crop, never stretch — pinned at 9:16 and 32:9); parallax is rewritten in the camera frame;repeatXrenders one viewport-spanning quad with wrapped UV phase;depthclamps into live near/far with a once-per-entity warn naming both numbers. Camera-frame ontology enforced on both sides of every forbidden pair: attach-onto-physics/parent refused, and physics/npc/parenting/api.move()onto attached entities refused with teaching errors (move is a warned no-op).query()filters attached entities on all three scan paths;getProperty("feetPosition")returns the attach form. Rider: one-argnotifyDmOnce(message)— the message is its own dedup key (the natural call shape previously burned the key and sent nothing). - 2D wave 2 (#7813): the legacy plate machinery dies (+44/−495) — backdrop clamp groups (
BackdropClampGroup,resolveBackdropClampGroups,backdropClampDeltaOf),isBackdropTextureId's clamp-eligibility role, 3-tilerepeatXgeometry + whole-tile re-centering for world sprites, and the pre-wave-1 orphancamera-attachments.ts. Mid-band world-sprite parallax survives asapplyStandaloneParallaxOffset(taught mid pieces keep their drift). Scheduled behavior deltas for legacy content only: filename-clampedbackdrop-*plates can out-travel painted coverage again; world-spriterepeatXstrips render one tile wide, and barerepeatX(no parallax) now batches. - 2d-mode skill (#7811): mid-band footing judgment replaced after zoo falsification — feet at-or-below the deepest floor the piece drifts across (light −0.5 tuck), replacing local-sink, whose footing was only true at one camera position (camera-attach made drifting mids the default case).
- The worker topology's decoded clip-duration relay is re-landed (ledger #802). #7088 taught the sim worker to record each
audio.clipDurationpost from the main-thread decoder into the local duration mirror and relay it to the server asengine.soundDuration; the #6891 client-auth merge clobbered that handler six days later, so the message dropped silently — the mirror never filled, the server's mirror stayedseconds:null, andonSoundEndnever fired in the worker topology (which is every production embed; non-worker simulation was removed from the client entry). The switch case, pre-mount buffer, and mount-time flush are restored against the current runtime-worker; the sound-end trust model (server accepts reports only for clips its own playSound referenced, healed by the cooldown-gated re-offer) is unchanged. - ObjectAPI script WRITE verbs (
setScript/replaceInScript/insertInScript) now rejectmemory/paths with a redirect error naming the working tool (str_replace_editor, which writes to savi's Supabase memory store — the PR #7146 funnel). The game spec replicates whole to every player who joins, so amemory/key inspec.scriptsstrands savi's private notes on the player-facing wire; the validator now matches the boundary #7146 already drew. Deliberate asymmetries (plan p-624fbe9e):deleteScriptstill acceptsmemory/keys (the stranded-app migration needs that cleanup path), andgetScript/listScriptsare unchanged (stranded apps stay readable until migrated). - New rate-limited server tripwire:
tome.spec.memory_scripts_on_wire(warn, at most once per 60s window) fires fromtome/spec-sync-serverwhen an outbound spec revision carriesmemory/script keys — meta carries key count, UTF-8 byte total of the stranded sources, and the spec revision; app/room identity rides the server logger context. Observation only: zero spec mutation, zero filtering, no replication/signature/wire change. - Ledger 796: container log lines now carry the real app id instead of
appid:unknown. cf-edge threads the app UUID into container start env asSPAWN_APP_ID— the same boot-immutable ride asSPAWN_ROOM_ID/SPAWN_ENGINE_HASH(#421/#584) — from the iframe?ctxpayload on prewarms and from the variant-credentials Supabase lookup (one embedded select, no extra request) on bare/roomsWS-first boots. The kernel logger already readsSPAWN_APP_IDinto its per-line context and DD intake tags, so every line — including the wedge-attribution lines (room.boot.wedged,worker.fatal) that fire before any late identity bind, and lines from old pinned engines — tags app + variant from the first boot line. Per-app container triage in Datadog works now. - Ledger 752: in client-owned multiplayer,
definePlace(X)→enterPlace(player, X)in one client-simulated invocation no longer silently strands the traveler. The two calls rode different lanes with inverted server ordering (SpecMutations folded at the tick-end drain,rail.enterPlaceexecuted at command dispatch −100); forwarded spec mutations now fold in a new server system at simulation −150 — before command dispatch — and the client flushes the tracked mutation batch onto the socket before EVERYrail.*command at the one enqueue seam all rails share (enqueueClientCommand), so program order holds end to end for emit/transferControl/voxelEdit/intent alike, present and future rails both. addBehavior/removeBehavioron a just-spawned entity whose spec mirror no-opped (spawn into a place the local spec does not carry) now still record the durable op when the pending spawn already left the tracker (e.g. shipped by a mid-wrap rail flush) — previously the ECS got patched and the durable behavior ref was silently lost on reload.unknown_sourcerail rejections (thespawn(npc); …rail on npc…one-invocation sibling race — the create rides the StateDeltas lane and can land after the Command) now also report into the behavior fault/log stream, attributed to the stamped source, instead of dying as an uncorrelatedcmd.err.definePlace/updatePlace/deletePlace/setDefaultPlaceoutside a persistence tracker now ride the same tracker-less persistence tail aspatchEngine/patchRouting: client-auth behaviors forward them source-stamped call-time (previously a bare call never reached the server at all), and the server/singleplayer authority enqueues them for its own drain.- Rail command handler throws (e.g.
enterPlace() failed: place … does not exist) now report into the behavior fault/log stream — runtime log (visible to getLogs) plus the DM notifier, attributed to the stamped source entity — before the dispatcher'scmd.errreply, which previously died uncorrelated on the client. - Ledger 810: god-mode handle gizmos now size from a 56px thumb target with a 44px iOS floor and an object-fraction cap, converting px→world through the existing
UiGlobalsResourceviewport mirror; theCOARSE_HANDLE_SCALEconstant is deleted. - Ledger 811: armed touch tools get a visible ✕ exit chip that routes the same
god:cancel-tooldesktop Escape sends. - Ledger 776:
spawn()with aparentnow resolves the child's place from the PARENT's live place instead of the spawner's — amaincontroller spawning{ parent: "skurp" }while skurp lives insparkminesno longer mints a cross-place parent/child split at birth (entity and persisted row both land in the parent's place). An explicitplacethat conflicts with the parent's is still honored but warns on the fault channel — it authors a split. - Ledger 777: the destroy cascade's row sweep is now doc-global across all three seams (kernel recorder
collectSpecSubtreeRowIdsForDestroy, kernel spec-mutation fold, kiln doc-apply) — a child row split into another place leaves the doc WITH its destroyed parent instead of upgrading to a permanent orphan. Parent refs bind by same-place-wins (a ref resolves to a row of that id in the child's own place first), so #184 cross-place duplicate ids never cascade into another place's same-named family. Seeding by the target's ROW id also lets place-namespaced runtime ids (placeId:objectId) collect their bare-id children. - Ledger 779: the persist gate (
isMutationPersistable) now refuses spawn rows whoseparentcan never have a row of its own (player/,camera,tome/exec,system/,_prefixes) — previously the row persisted and reloaded orphaned forever (the one-nameplate-per-player debris class). The live spawn is untouched; the refusal is taught via the normal fault channel instead of silently minting. - No data repair: pre-existing split/orphan rows in stored docs are untouched (separate creator-consent call).
- Ledger 816: a
DrawSpritewritten without atexture(neither the ObjectAPI sprite setter nor spec apply validates the field — e.g.obj.sprite = { opacity: 0.5 }on an entity with no existing sprite shipstexture: undefined) made the server-onlysprite/metadata-hydrationsystem throw a synchronous TypeError on every pass; under sustained sprite-write pressure the scheduler's 5-retry ladder completed and permanently disabled hydration for the rest of the process — every later sprite in the room lost auto-sizing and hull colliders until the container recycled. Hydration now skips texture-less sprites (nothing to hydrate from) instead of throwing. FatalSystemError's message claimed "server restart required" — false: the runtime worker catches it and the system is simply disabled for the process lifetime. Message and docs now say what actually happens.- Mantle: Box3D technique harvest (PR #7741). Adopted: gyroscopic torque via implicit NR-1 solve in the deterministic lane (pure arithmetic + quat rotate helpers, no transcendentals; scratch rebuilt per tick — nothing new outside the snapshot except contact-event prev-overlap sets, which ride the arena per the graduation clause). Rejected with measurements (docs/physics-native/box3d-harvest-notes.md): static-softness ζ=5 and central friction — both breached the heightfield-rest neighbor-sensitivity epsilon; cross-tick warm starting rejected on the snapshot veto. Cross-runtime golden hash chains identical V8↔JSC; 390/390 mantle tests both runtimes.
- Music API (plan p-9989dd7d): engine-owned music orchestration. New
TomeMusicStatecomponent on thetome/specentity (replicate:"always", reset-proof, carried in the reset snapshot — late-joiners receive music state in the same packet as the spec) holding per-layer{clip, volume, loop, startTick, fade, epoch}targets plus a global duck envelope. New ObjectAPI namespaceapi.music.{play, crossfade, stop, duck, now}(server-realm; baremusic.*in run_script) writes it; a newmusic-clientsystem applies it through the existing AudioTrack/voice pipeline (Music bus — user volume/mute govern structurally). Crossfades, same-clip in-place volume ramps, named layers (parallel stems), tick-anchored positions, and late-join seek are engine-owned; playback state never touches GameSpec asset refs (regression-pinned). LegacymusicShiftruntime untouched; per-player musicShift state suppresses the global default layer on that client while active. - Late-join seek plumbing lit up:
AudioEmitterValue.time→ prep now producesstartOffset; the renderer's buffered path start-offset bug fixed (source.start(when, offset)— was a delay, not a seek) with loop-length wrap; music layers force the buffered decode lane past the stream heuristic. api.audio.duration(ref)(server) +audio.duration(ref)(client hatch): sync clip length in seconds,nulluntil the fact mirrors (first decode/report; server reads mark the clip referenced so the fact converges). Rides the #7809 clip-duration relay.playSoundnow honorsbus:(named mixer bus, unknown coerces to "SFX") andpriority:(0–100 clamp) — previously silently ignored; defaults unchanged when absent.- Zoo Audio zone rebuilt on
music.*(trigger-volume room music, jukebox pad, genre-fusion stems pad, duck on the voice pad); the hand-rolled ~95-line ui.js director is deleted; a minimal hatch mount-lifecycle fixture remains. - audio skill net-shrinks 110→75 lines (7,849→6,778 chars): the taught director pattern is absorbed by the API; hatch teaching survives as the escape hatch.
tome.reconcile.parent_missingwarns once per row per boot instead of on every applySpec pass (ledger 776 warn-side, #7754 — was ~5.7k DD events/15d plus ~3/min creator-visible runtime-log spam that nudged Savi into re-fixing the same row). A changed parent value earns a fresh warn. The cross-place case gets honest text: when the parent exists as a row in another place, the warn says exactly that and suggests removing the row or moving it to the parent's place, instead of claiming the parent is missing.- Quality governor evidence hygiene (ledger 782, #7760): frames inside the compile/load grace window no longer count as shed evidence — the judged window flushes on every grace sample and steady samples push only after the grace check, so post-grace judgment starts clean. Was ~1 phantom rung shed per Savi edit / place-warp: healthy hardware ground to the quality floor over a 5–10 minute build session. A genuinely heavy world still sheds within the sustain (pinned). Dumps gain a session-cumulative governor transition ring (~last 32 shifts, timestamped with triggers) + landing state.
- Renderer model retain/release + keep-alive eviction (#7617): the model cache's
modelEvictionFrames: MAX_SAFE_INTEGERpin dies — visuals lease their LOD-resolved model ids (two lease slots, settled through every LOD transition) and release on detach; models ride the texture two-tier eviction (grace window measured from release, per-device keep-alive byte budgets 192/64/48MB, oldest-released first past budget).disposeModelnow disposes GLB-embedded textures —material.dispose()never freed them and they were the dominant leaked bytes. Fixes the Final Abyss WebGPU device-loss class (one preview entity cycling 28 bespoke 20–27MB GLBs → 1113MB texMem →createBufferfailing at 192 bytes → device death). Riders: device-lost latches the worker-silence watchdog (kills the contradictory follow-up report), andrenderer-device-lostcarries the GPU adapter description. - Ledger 809: kiln hosts measure
env(safe-area-inset-*)via a probe element and forward the values once per host across the iframe boundary; the kernel publishes--spawn-safe-area-*CSS vars on<html>with anenv()fallback (256px clamp, non-finite→0). God-mode overlays, touch controls, and the server-behind banner consume them. - Ledger 787:
room.server_behind.episodeno longer mixes units. The old headlineavgTickWorkMswas per PUMP FRAME (tickWorkMsTotal / framesLastSecond) whilephases[]/topSystems[]are per tick — and exactly when the log fires, frames coalesce up tomaxStepsPerFrame(8) ticks, so the headline read ~7× the attribution (a P3 burned 25 min on a phantom "87% unattributed"). The field is renamedavgFrameWorkMs(maxTickWorkMs→maxFrameWorkMs), and the log now also carries per-tickavgTickMs, the coalescing factorstepsPerFrame,unattributedTickMs(avgTickMs − Σ phases: jobQueue.poll/world.prune/ctx overhead outside phase brackets), and per-phaseresidualAvgTickMs(phase avg − Σ that phase's systems: commitTick/stager/flush). All computed at log time inbuildServerBehindEpisodeLog(server-behind-episode-log.ts) — zero hot-path additions. No in-repo DD config queries the old field names; saved DD queries onavgTickWorkMs/maxTickWorkMsneed the new names. - Spec→ECS apply cost collapse (plan p-a2af390c, #7853 + #7826): behavior compilation and signatures now cost O(unique script contents), not O(objects × applies) — a content-keyed compile cache with per-dep source-hash revalidation (failures cache too: a broken lib costs one failed compile per unique content instead of one per object per apply), and behavior signatures are
murmur3_128hashes over exactly the old inputs instead of embedded full sources (185.5MB retained signature chars → 2.2MB on a 3,352-object prod spec — the 3GB-container GC spiral). Grown-spec apply: 1,863ms → 298ms; broken-libs apply: 11,137ms → 506ms. onSpawn re-run semantics pinned identical by op-count tests. - Server spec pushes apply once (#7826): every push, boot, and reset applied the whole spec twice — the direct apply plus
tome/spec-sync-server's echo re-apply on the next tick (DD-attributed at 2,917ms avg / 20,420ms max per echo on 3,352-object prod worlds). The three lanes that tracked raw spec references inTomeSpecnow track the applied snapshot (reference identity is the codebase's echo-suppression contract); a FAILED apply deliberately keeps the raw reference so the sync pass retries. - New ObjectAPI verb
api.updateStorage(key, updater, callback?, { attempts? })— the engine-owned safe read-modify-write for storage. get → updater(current) → versioned (compare-and-set) write, automatically retried against the fresher value when another writer lands first (the conflict echoes the current head, so a rebase costs no extra read; default 4 attempts). The updater must be pure — it may run multiple times; returnundefinedto abort without writing. Callback results:{ ok: true, value, version, attempts },{ ok: true, aborted: true },{ ok: false, error }with codesstorage_conflict_exhausted(carries attempts + currentVersion) andstorage_cas_unsupported. Works in behaviors (callback), lifecycle/cron (awaitJobtoo), and player-context — the loop's legs ride the existing forwarded storage jobs, so theuser/<self>/…self-scoping applies unchanged. Custom spec jobs get the same primitive asenv.update(key, updater, { attempts? })andenv.getWithVersion(key). - Storage vocabulary is now version-aware:
storage:getresults additively carryversion;storage:setacceptsbaseVersion(absent = last-writer-wins as before;0= create-only expect-absent;N= compare-and-set) and returns{ ok: true, version }. A stale base fails witherror.code: "storage_conflict",retriable: true, anderror.details: { currentVersion, currentValue }— the existingif (!result.ok) returnscript idiom degrades to a safe SKIP, never a clobber. Identical on both storage lanes (job-pool rail and lifecycle/cron direct-SDK rail) and across the forwarded multiplayer wire (detailsnow rides job error payloads). storage:set'sttlSecondsis now actually honored on the job-pool rail (it was typed and accepted but dropped before reaching kiln); an expired document reads as absent.- Honesty fixes: a failed in-memory-rail storage read now propagates its error instead of being swallowed into
undefined(a failed get must be distinguishable from empty — the ledger 793 pattern, one rail over), and a compare-and-set write that the storage backend cannot honor (deploy-overlap window with a pre-CAS kiln) fails typed asstorage_cas_unsupportedinstead of silently degrading to last-writer-wins. - SDK surface (
@spawnco/server, ships in this build):documents.getWithVersion(name),documents.set(name, value, { baseVersion?, ttlSeconds? }) → { version }throwing typedDocumentConflictError { currentVersion, currentValue }on 409, anddocuments.update(name, updater, { attempts? })— the same CAS loop, awaitable. Requires the kiln documents-funnel deploy (version-aware GET/PUT); against an older kiln the new verbs fail typed rather than lie. - Ledger 793: room-scoped job-pool storage now fails honestly instead of lying. In the variant-change window (pool torn down and recreated before SDK identity resolves) — and any time a room pool has no working SDK —
storage:get/set/del/list/query/lock/unlockjobs previously fell back to per-thread in-memory storage and reportedok(reads asvalue: null, writes persisted nowhere), the exact failure shape that defeats creator-written save guards (_saveLoadOk-style flags armed on a blank read, then defaults overwrote the real save). All seven storage surfaces now fail witherror.code: "storage_unavailable"and a self-teaching message; the code rides both lanes verbatim (server-local and forwarded multiplayer). Dev/harness/test pools that intentionally run without a storage backend keep the quiet in-memory fallback unchanged (tri-statesdkIdentitydiscriminator: object = resolved, null = room-scoped unresolved → honest failure, absent = identity-less pool → in-memory by design). - Known limitation, declared: a worker spawned in the null-identity window keeps failing loudly until natural churn — proactive identity-refresh respawn is a named follow-up. Field specimen: a_dreamcatcher's Spawn Tactics progression wipes (dump 77cae01a).
- Ledger 790: the terrain-edit journal is now compacted at the durable-store seams. Accumulated single-cell
voxel-setcommands fold into dense per-chunkvoxel-stampcommands (the existing palette+RLE encoding) on document save and on document load, so the durable payload and every boot rehydrate are O(edited cells, chunk-bounded) instead of O(lifetime edit count). Semantics-preserving: identical replayed voxel state,appliedRevision/lastServerTimestampuntouched, nonzero-state cells keep their single-cell commands, and commands at or before the last region-shaped command keep their exact positions. Old uncompacted documents still load and self-heal (one compacted re-save on first boot). - Terrain-edit fold now re-serializes only chunks whose journal changed (per-chunk
appliedRevision/lastServerTimestampreuse against the previous store entry) instead of re-cloning the whole place per edit, and journal serialization dropped its per-command deep clones (commands are immutable engine-wide). The near-cap size check estimates payload bytes instead ofJSON.stringify-ing multi-MB documents on the sim thread. - Receipt on the prod-shaped 39k-command specimen (364 chunks): boot rehydrate 292ms → 38ms, single-edit fold+save 125ms → 1.1ms, saved document 39,289 commands / 5.2MB → 365 commands / 0.4MB (dev hardware; the prod container amplified the old cost to ~20s freezes).
patchTerrainthat changeskindnow replaces the terrain wholesale instead of deep-merging residue from the old kind (r-56, #7762; class precedent #7734's patchAtmosphere fix) — applied with the sharedswitchesDiscriminatedKindpredicate at all three folds (live ObjectAPI, kernel spec-mutation fold, kiln durable fold), sopatchTerrain({ kind: "off" }, "main")on a heightmap yields{ kind: "off" }exactly. Same-kind and kindless patches keep deep-merging. (patchCamerahas the same latent class — enumerated, deliberately not fixed here.)- Transactional run_script rollback now reports everything it discarded — the silent-swallow that actually ate the specimen's kind-switch (an unrelated patchPlayer schema error rolled back the script and the terrain verb vanished without a trace).
- Ledger 787: terrain-reanchor's per-anchor re-resolve no longer routes through the full creator-facing
createObjectAPI().setProperty("feetPosition")dispatch.updateTerrainAnchoredEntitycalls the feetPosition writer directly (terrain-height sample + anchor sync + transform write + owned-scatter-field translate) — the dispatch wrapper's pieces (ObjectAPI construction, sprite-warn doctrine, creator-write schema validation, tracker-gated mutation recording) are state-inert on engine re-apply paths. Parity with the old path (component state + pending dirty marks on twin worlds) is pinned byterrain-reanchor-parity.test.ts. - The reanchor system additionally samples the composed height BEFORE writing: when a chunk-version bump didn't actually move the ground under the anchor (sculpt touched the chunk but not the anchor's cell), the write is skipped entirely — component writes were value-deduped no-ops anyway, and the one non-deduped piece, the static physics body dispose+rebuild, is exactly what made per-tick field churn cost ~34ms/tick at 119 anchors on prod app 28642021 (
server_behindepisodes). The version gate itself was already per-chunk (composedFieldVersionAtsums the authored+runtime versions of the one chunk under the anchor), so no new versioning surface was needed. - Tome warn-class log lines no longer land in the Datadog error lane. Self-correcting
[Tome]warnings (mutation-warn rail — e.g. spawn()properties.parentauto-hoist — plus interpreter spec-normalization/terrain-mark-skip warns and the camera-state function filter) were written via bareconsole.warn, which is stderr on the server, and the container log tee ships every stderr line at status:error. They now route throughtomeLogger.warn(winston status:warn on the server; unchanged console.warn on the client and in worldless tests). Message text is unchanged, so text-grep dashboards keep matching. Genuine failure logs (tomeLogger.error, purchase-refusal warns) are untouched. - Vibe strudel-divergence (ledger 805, #7859): chain verbs whose DSP already exists are now honest aliases (
.lpq(r)→.resonance(r),.distort/.dist→.drive,.duck(on)→ the duck mod), and ~60 real strudel verbs vibe deliberately lacks (chop,vowel,off,sometimes,n,bank, …) throw teaching errors naming the nearest real verb on the existing fault rail — instead of a bare"…lpq is not a function"TypeError (87×/night in prod). A load-time guard refuses any table entry that would shadow a real chain verb; deliberately-divergent verbs (.rev= reverb send,.coarse= semitone transpose) keep vibe semantics, pinned by test. - Custom geometry supports
pbr: truefor materials (Jacob, #inventors 07-03). The gap:normalizeMaterialSpecderived Patina packed-variant map URLs frommaterial.texturealone, so custom geometry's primary texturing flow —ctx.albedo()textureRuns — silently dropped the request (whole-material{texture, pbr: true}already worked; the standalone bespoke lease binds all maps). The fix consumes the existing pipeline rather than mirroring it: the request now ridesDrawMaterialOverridesas a canonicalpbr: trueflag, and the bespoke run-value assembly (materialForTexture) derives each RUN texture's own maps via the samebuildFal1PbrMapUrl— normal/roughness/metalness/height, same URLs and transforms primitives ride. Non-derivable run sources (proceduralscripts/, pre-compressed.ktx2) keep the material's own maps. Derivation also keys off the material's effective texture (texturealias or flatmap— the reader round-trip shape god-mode writes), and the spec-asset preloader mirrors that."pbr"joinedSOLID_BATCH_OVERRIDE_KEYSso the carried flag never knocks a material off the batch lanes;pbrabsent/false stays byte-identical to before (pinned in bespoke-pbr-runs.test.ts, material-utils.test.ts, tome-primitives.test.ts). playSoundnow warns (once per entity+kind, mutation-warn rail → getLogs + ops trail) when a GIVENbus:orpriority:value is unusable — unknown/wrong-case bus names (coercion to "SFX" unchanged) and non-numeric priorities (default unchanged). Existing games passingbus: "voice"(wrong case) orpriority: "high"(string) keep exactly their old runtime behavior but start seeing the teaching warn in getLogs. Absent options remain silent; the dedupe key is value-free so per-frame varying bad values cannot fill the log ring or churn other warn classes' cooldown state.- Spec apply O(delta) close-out (plan p-a2af390c PR-C, #7919 — finishes 5.1.10's apply-cost collapse): (1) delta-0 short-circuit at the applySpec seam — a spec structurally equal to the applied generation skips the whole derivation skeleton, with retry vetoes so a short-circuit can never skip a retry (completed-apply binding via
TomeAppliedGenerationResource.specRef === GameSpecResource, degraded-lowering retries, compile-error re-report contract, scatter missing-children recovery, restore window, residency growth, instance-registry drift, bare-id owner-map changes in place-filtered worlds; reset drops the record explicitly). (2) Changed-set-first apply loop — per-object visits happen only for objects whose reconcile inputs changed (expanded-def structural inequality covering realm/parent beyond the four signatures, place moves, spec-authored hierarchy-pair changes, runtime-flagged scatter recovery), with the pinned full visit on replace; unchanged defs reuse the old signature object, killing the O(spec) re-stringify. (3) Structural sharing over the whole-spec resource clone — regions structurally equal to the applied doc reuse its engine-owned subtrees, deciding equality at each child's seam (an equal subtree is traversed once, allocates nothing); spec-row arrays get an id-keyed assist so inserts/deletes/reorders reuse every surviving row; the two in-apply writers of the stored doc (defaultSpawnPoint backfill, survived-destroy retry rows) are copy-on-write. (4) Newtome.applyepisode on the 787 instrumentation rail: structured per-apply completion log with duration, total/visited/skipped/spawned/destroyed, expand passes, degraded count, replace + shortCircuit flags, and caller lane. Hardening riders from the #7894 review:structurallyEqualSpecDocsdistinguishes array holes from storedundefined(the dangerous direction — a revision suppressed where master bumped); cyclic doc shapes are contained (a throw means "cannot prove equal" / plain deep clone, never a failed apply); the spline-orphan sweep runs on both paths so prediction-rollback strays don't survive the short-circuit. - Shared
__proto__guard on spec-doc clone walks (#7923, flagged by the adversarial review rider on #7919): creator-authored spec docs can carry own__proto__/constructor/prototypekeys (JSON.parseproduces them), and naivetarget[key] = valueclone walks turned an own__proto__data key into the clone's[[Prototype]]— silent data loss, phantom inherited state, and a prototype-pollution foothold. One shared guard (setSpecDocKey:Object.definePropertyre-key matchingstructuredClone's own-key semantics, so guarded clones stay structurally equal to their source — spec hashes and the #7919 delta-0 short-circuit ride that equality) now applies to every doc-shaped clone/copy walk insrc/tome:deepCloneUnknown,cloneValueReusingApplied,mergeRuntimeStateWithSpecDefaults,cloneValueForOverlay,mergeField/mergePlaceFields/mergePlaces,applySpecStateDefaults,toWireSafe, cameracloneRecord/mergeDeep, and material passthrough/params. Regression test per guarded walk plus anObject.prototypepollution canary;applySpecStateDefaultsgates onObject.hasOwn, notin. tome/timer-dispatchgains a per-tick wall-clock budget (25% of the tick interval, clamped 4–100ms — ~8.3ms at 30Hz; env knobsSPAWN_TIMER_DISPATCH_BUDGET_MS/SPAWN_TIMER_CALLBACK_WATCHDOG_MS), checked BETWEEN callbacks: due timers past the slice spill to the next tick instead of piling into one mega-tick (the DEADZONE 9.4s-tick class, ledger 829). Nothing is dropped or reordered by the budget — a spilled timer keeps its original due tick andrunInTicksfloors at 1, so arm order holds across the spill boundary by construction. A per-callback watchdog (25ms) writes one creator-legible line per slow callback naming script, owner, and elapsed ms — runtime log (getLogs) teaching runInTicks slicing, plustome.behavior.timer_slowon the DD rail — throttled per script+owner (30s cooldown, warn-key map hard-capped at 256 with warn-recency eviction). Spill breadcrumb +tome.timer_dispatch.spillcarry spilled/ran/budgetMs/elapsedMs andarmedTotal, the backlog gauge that makes the arm-rate-above-drain-rate regime visible before it becomes a memory problem. Honest scope: this is attribution + pile-up bounding, not a per-callback preemption — a single synchronous monster callback still wedges its own tick for its full duration (the cure is creator-side slicing, which the watchdog line teaches); a throw is not budget-exempt (wall time spent is spent).- An explicit
physics.collidershape override ("box"|"sphere"|"capsule") on a pierced primitive — awallwith holes,archway,squareArch,hollowCylinder,ring,torus— now warns (once per entity+warn class, runtime-log rail → getLogs) that it replaces the hole-respecting derived collider with a sealed shape viaderiveColliderFromBounds, naming the object, the override, and the remedy (remove the collider field). Collider behavior is unchanged — explicit overrides still win (sometimes a deliberate perf simplification); the seal just stops being silent (dump 2eba2de7: a doorway went solid with zero feedback). Dedupe keys are value-free per the #7943 review law (entity+warn class, never the offending value), and the existing collider-shape warns moved onto the same keying so one warn class firing no longer masks the other for the session. - Mobile screenshot famine, capture-source half (ledger 833; kiln gate half = #7945): the seo-preview capturer's undocumented
isMobileDevice()early-return is removed — the byte-identical screenshot pipeline (renderer-worker RPC → resize → uploadClip) was already proven on phones by the savi-note capturer, and the old predicate'smaxTouchPoints > 1check also starved touch-screen laptops. AndplanFlythroughCapturenow treats a world flythrough row with a NULL thumbnail as heal-on-next-visit instead of "already captured forever" — the 06-25 class where one failed thumbnail extraction permanently poisoned a game's press covers; the (variant, clipId, kind) upsert repairs the row in place, andshouldUploadFlythroughkeeps failed heals from uploading. - Spec-mutation forward rejections are no longer Datadog-only (ledger 831/g-77). Every drop site in
spec-mutation-drain.ts(not_owner,privilege_required,target_not_writable,batch_too_large,payload_too_large,boot_grace_overflow) now records aspec write dropped: <verdict>runtime-log entry — visible to Savi viagetLogs/run_script— content-key-throttled (30s, shared withreportRepeatingBehaviorError) so a ×26 burst is one entry. Creator-relevant classes (not_ownerfrom a creator client, boot-grace/size drops) additionally DM Savi throughTomeDmNotifierResourceunder the shared engine-error cooldown + window budget; visitorprivilege_requiredstays log-only (anti-forgery gate working as designed) andsuspended_sessionstays fully silent (takeover race, by design). - The just-spawned drain race is closed with a bounded hold-and-retry: a batch whose source authorization fails for a not-yet reason — source entity unknown (its create row can still be queued for next preUpdate when a socket flush straddles the drain phases) or unowned source in a place with no live host yet (host rows land at simulation −90, after the −150 ingest) — is held and re-judged for
SPEC_MUTATION_SOURCE_GRACE_TICKS(3) ticks before dropping loudly. Definite forgeries (source exists, owned by someone else) still drop immediately; nothing folds or persists on a hold, so the anti-forgery gate is unweakened. The hold is double-bounded (grace ticks +MAX_HELD_FORWARDED_SPEC_BATCHES), and the shutdown flush (drainTomeSpecMutationQueue(world, { final: true })) resolves holds deterministically: one last judge, then loud drop. A retried batch can persist after a later-arriving accept from the same sender for a different entity — benign under per-entity single-writer.