engine v5.2.33
Engine v5.2.33
August 22, 2026
A patch in the Lume line.
Fifteen cures in one: vehicles stop reloading scrambled, drags stop teleporting to the last save, Ctrl-Z survives reloads in multiplayer, half-joined sessions stop ghost-simulating live players, stale tabs can't cross players into your editing room, middle-click is reliable, grass reads green from both sides, things stay the right size entering new areas, game UI can save files, scripted terrain edits stop wiping paint, and slow machines stop tearing themselves down to 'recover' from their own lag.
what's new
- A one-off network hiccup while the engine downloads no longer kills a player's first open. If one piece of the engine fails to arrive, the game now quietly re-requests it once before showing an error — the class of "it just showed 'something went wrong' the first time I opened it" caused by a single flaky fetch heals itself.
- Vehicles and other built objects no longer come back scrambled after a reload — wheels, seats, and parts stay exactly where you put them, every session.
- Dragging an object no longer teleports it back to where it last saved. After a stretch of continuous dragging, grabbing an object could snap it to a stale position (roughly where the last autosave put it) and every drag after that kept teleporting. The drag now always starts from where the object actually is in your hand.
- Things stay the right size when you walk into a new area. In multiplayer rooms, objects with a size limit (
layout) that were built while your game was still electing a host could show up giant — a 10m watchtower rendering at 45m — and stay giant until the room restarted. Now the moment your session takes over simulating an area, everything in it snaps to its correct fitted size. - Scripted edits to painted terrain and scatter no longer make the rest of the paint vanish in the running game.
- Middle-click is now reliable. Binding the middle mouse button no longer fights the browser — bound middle-clicks can't trigger page autoscroll or other browser defaults — and a middle button held when pointer lock drops (Esc, opening chat) no longer sticks "down" until something else resets input. Actions bound to middle fire on press and release exactly like any key, every time.
- Falling out of the world while seated no longer welds you under the vehicle. When the engine's fall-catcher or terrain rescue moves a body back to safety, a rider still parented into a seat is unseated first and lands at the rescue point as a free body — before, the rescue position could be re-read as a seat offset and leave the player stuck under the cart until a script intervened. The unseat-before-rescue guard scripts wrote by hand is now the engine's own behavior.
- Custom crosshairs are yours now — the platform's white pointer dot no longer sits over your game's own crosshair. It stays hidden until a player actually moves the mouse in the overlay, which is the only time anyone needs it.
- Old open tabs can no longer cross your live game's players into your editing room. A tab left open from before a recent fix could reconnect players into the room you edit in — reading half-built work and manufacturing wipes. The server now refuses those stale connections at the door and the page reloads itself into the right room.
- Ctrl-Z now survives a reload in multiplayer worlds. Before, undoing a delete brought the object back on your screen but never re-saved it — it silently vanished the next time the world loaded (and if it lived in a side map, even a saved restore could teleport to the main map). Now an undone delete is really back: full object, behaviors and all, in the place it belonged.
- Ghost copies of players can no longer haunt multiplayer rooms while our builder reconnects. A half-booted background session used to be able to run stale copies of your game — old script versions writing old state onto live players' screens, in loops of up to three minutes. Now a session that hasn't finished joining simulates nothing at all: your world only ever has one live simulator per entity.
- "Which script is eating the tick?" is now one question, not days of ablation. In multiplayer rooms,
api.getPerformanceSnapshot()(Savi's perf read) now answers with the hosting player's actual per-script sim cost — cumulative ms per script file over the measured window — plus how many scripted entities ran vs. were schedule-skipped each tick. Before, that data lived on the hosting player's machine and the read came back empty. - On slow or busy computers, the game no longer tears itself down to "recover" from its own lag — the surprise "Connection lost. Reconnecting…" after a long freeze, and waves of textures failing and retrying, both came from the engine misreading its own slowness as failure. It now tells the difference and rides the lag out.
- Grass is no longer black from the far side. Grass blades and plant sprites used to render as black shapes when you looked at them from the side facing away from the sun's shading — fields showed dark patches that moved as the camera turned. Every blade now reads the same green from both sides, the way ground cover should.
- Your game UI can now save files to the player's device. Export buttons built the normal web way — make a Blob, point a download link at it, click it — now actually download: story text, save files, level exports, screenshots. Before this, the export button did nothing at all, with no error anywhere.
›technical notes
- A failed boot-critical dynamic import gets ONE cache-busted retry before boot death (tiger's onboarding dig 5b47a1e5, fix 1; 5.2.33 cargo). The client entry's engine-chunk imports (
worker-browser-host,client-sight-frameinloadCreateBrowserAppHost) now run throughimportBootChunk: on a module-fetch rejection (Chromium/Firefox/WebKit shapes matched by message), wait a 250–500 ms jitter, then re-import the failed URL with?retry=1appended — a request no poisoned intermediary cache (and no memoized module-map failure) has ever seen. WebKit's URL-less rejection falls back to re-invoking the same specifier once. A second failure rethrows with " (after retry)" appended, so the[kernel] Startup failedconsole line distinguishes retried-death from one-shot-death; evaluation errors (module loaded, then threw) are never retried. Exactly one retry, no loops — the burden of proof (the dig's receipts: transient edge 502s, ~10 events/3 sessions/day, same URL 200 seconds before and after, always a first open) lives on the helper. - Child transform ownership: a parented child's Local* is AUTHORED truth and the client
local-transform projection can no longer overwrite it. The projection's default flipped from
"re-derive Local from World for every parented child, minus four excused guards" to two explicit
jobs — SEED a Local lane whose row hasn't arrived yet (wire ordering), and OWN the lanes of
dynamic-bodied children, where physics writes the world pose and local is genuinely derived
(
physicsOwnsPose, one rule shared with the hierarchy solve). The god-mode-drag, stand-rider, and bone-subtree guards are deleted — each was one more authored-row case discovered the hard way, and the default now covers them (the stand-parent seam test flipped from negative control to positive pin). Root cause this kills: one tick of parent/child frame incoherence (a reload's journal-restored hull vs its spec-seeded children) was divided into the child's local offset and became permanent — vehicles reloaded scattered and rode rigidly-wrong forever (reload-scatter-repro.test.ts, red pre-fix with the exact predicted arithmetic). - A translate drag's x/z base is the press-time LIVE feet, never the spec row's (tucker's drag-teleport, walk thread 6:07pm 2026-08-20; 5.2.33 cargo, the 5.2.32 cut predates it). The translate pad and the spline
spline:movepad based their slide on the spec row's terrain-anchor x/z (authoredTerrainAnchorFeet→getObjectSpec). On a relay client the doc row only advances when a save's fold echoes back — release commits ride the accumulator → flush → kiln, never the local doc — so the row lags the hand by a whole save round trip, and every translate drag writes the terrain form{ x, z, y: { terrain } }, arming the anchor read. Once the first save echo landed (~3–5s into a drag chain), every later press captured the LAST SAVE's x/z as the slide base and the first drag frame teleported the object there + ray delta — degraded for the rest of the session ("EVERY time you drag and release it will like teleport … snaps back roughly to where the save was"). The authored anchor now supplies ONLY the vertical pin (offset + write form), which is all cal's Final Abyss burial fix ever needed from it; the commit seam's zero-move rule (GESTURE_LIVE_FEET_KEY) already keeps an idle touch from baking the f32 live base, so no ulp-smudge returns. Pinned bydrag-chain-release-commit.test.ts: the full relay-client loop (god-mode gestures + persist flush cadence + forward recency + simulated kiln fold/echo) runs a six-gesture drag chain with saves landing mid-drag — red on the old base (release at x=8 lands at 4: the stale anchor 2 + drag delta), green now, with every release's save minting and live/doc/durable converging on the drag's end. Named residual, same family, out of this pin's scope (self-healing in one round trip): a stale echo landing un-held after the recency horizon expires transiently reverts the live position until the client's own save echoes back. - Geometry fit re-solves on the hostship-gain edge (ledger 2566 — the 45.7m giants on every post-boot place expansion; live-verified in 46bfb1da's rig on master and 5.2.31). On a relay client the boot applySpec builds EVERY place's entities (client mode has no residency filter), and the fit drain runs before the election names this client host of anything — the observer gate consumes the TomeLayout dirty marks as bookkeeping (correct: no local GeometryScale write while another realm simulates). But nothing re-marked when hostship arrived: the header's promised rescue ("the first host's hydration seed re-marks and solves it") did not exist on the relay tier — the only seed runs at feature mount, long before hostship — so entities built while observing NEVER solved fit after the client became host, rendering at raw × authored scale (
local=3.808, geom=NULL, world=3.808for the whole session). The cure is the fit half of scatter-host-redrive's gain edge (ledger 2194's detector — one edge detector, two redrives, no second WeakMap family): on the edge where a place is GAINED,remarkGeometryFitForHostGainmarks every fit-bearing entity and the same tick's drain solves it. Over-marking is free —writeGeometryScaleidentity-skips values the replicated rows already answer, observer marks are consumed as bookkeeping next drain, and on the gain edge this realm IS the simulator, so the single-writer treaty holds. The false header sentence is corrected in the same diff. Pinned red-first ingeometry-fit-host-gain.test.ts(apply while hosting nothing → drain → GeometryScale correctly absent → TomePlaceHosts names the local client → tick → GeometryScale 0.2251). api.updatePlace's live application now applies{ merge: true, chunks, removedChunks }field deltas with the same incremental arithmetic both durable folds use (spec-mutation-fold'smergeFieldChunks, now shared by import) instead of installing the delta as the whole field — a partial-update-shaped write no longer wipes the live field while the stored copy stays correct (dig 5d20101b, ledger 2583).- Middle-mouse edges reach the ring (r-83fc347c dig on @jissi/blood-and-mead 660e9c3e — five Savi hardening rounds proved the script maximally defensive before the dig found both seams engine-side; 5.2.33 cargo). Two seams in
raw-capture.ts, red-first inmiddle-mouse.test.ts. (1) The r-c5a11032 claim extends to middle: a game binding middle —on: "Mouse1"or arequiredMouseButtonscombo modifier — claims the button's browser defaults for the session: preventDefault on pointerdown/up + compat mousedown/mouseup + auxclick (Windows-Chromium autoscroll engages off the unprevented compat MOUSEDOWN, auxclick carries its own defaults — paste, link-in-new-tab — and the pointer event's cancellation reaches neither), gated only on the claim, never on target or mode. Unbound middle keeps every browser default; the side-button claim rides unchanged under the renamedderiveClaimedMouseButtons/setClaimedMouseButtons. (2) The pointer-lock-loss desync closes:handlePointerLockChangeclearedheldButtons/mouseButtonsHeldon unlock without appending MOUSE_BUTTON_UP, so the sim'smouseButtonsDownkept the button until the next RESET — the physical release over UI died onreleaseMouseButtonIntent's!heldButtons.hasgate, a hold binding wedged (works/stops/works-again by construction — chat/blur un-wedged it), andsyncMouseButtonsFromMaskcouldn't heal it because the main-thread set was already empty. Held buttons now SURVIVE the unlock and the real release flows through the paths that already exist: the canvas-begun-drag carve-out (an up over UI passes while heldButtons has the button), the buttons-mask heal, pointercancel, and the blur/visibility RESETs. Deliberately neither alternative: a synthesized UP firesactiveOn:"keyup"bindings (processMouseButtonUppulses unconditionally — Esc toggling the lock would fire actions the player never released), and a silent RESET sweeps held movement keys whose keyups still arrive (lock loss keeps key focus). Rider: the docs generator'sInputActionDefprose line finally lists"mouse4" | "mouse5"— the r-c5a11032 tokens never joined it. Teaching-lane note (the spec is the creator's, not ours to edit): Savi's field patch comment in blood-and-mead'scarry.jsteaches a false engine model — "the engine honors one source per action" —buildDefaultBindingscompileskeysANDmouseANDgamepadeach to a live binding for the same action; every future dump read of that comment re-teaches the wrong shape until corrected. - Rescue-class world-lane writes unparent first (the OOB writer contract — golf-pack 2af2f101 flag 1 on #13317; 5.2.33 cargo). The two engine writers that teleport possibly-parented bodies — the out-of-bounds respawn (
respawnBodyAt,tome/systems/out-of-bounds.ts) and the terrain chunk-rescue (setRescuedFeetPosition,terrain-systems-shared.ts) — now detach the target's TomeParent edge immediately after the world write, so the detach rebases Local* to the rescued pose itself. A bare world write onto a still-parented rider never meant "rescued": the client local-transform projection divided it into a parent-relative Local* (the respawn point re-read as a seat offset — the Purr Putt dump f16eccb2 cat welded under the buggy), and under #13317's seed-missing ownership rule the same write becomes a silent no-op (the next hierarchy solve recomposes World from parent ⊗ local). Unparent-first is upstream of that divider, so the contract holds under both projections. The detach is one primitive, extracted from ObjectAPIsetParent(null)'s null branch intotome/hierarchy-detach.ts— one implementation behind the script lane and both rescue writers, resolve-tolerant for the engine-only worlds the chunk-rescue also runs in. - The platform's pointer-lock cursor dot (kiln's FakeCursor — host chrome's stand-in for the hardware cursor while the game iframe holds the lock) is now hidden-until-movement: a fresh overlay attach re-anchors it at viewport center INVISIBLE, and it renders only once the player actually mouses the overlay while visible-eligible (overlay open + locked). Motion means the player needs the pointer; lock-and-look crosshair games never move it, so their players never see a platform dot over the game's own reticle (savi platform-gap r-990382a0, app e742374d). No authored key — the earlier
camera.pointerLockDotdraft was withdrawn because authored-off would blind the overlay's only pointer in exactly the state it exists for; the default is now right structurally. The movement gate reads only the engine-relayed movement deltas (computed inside the engine's own accumulated cursor space), so the attach re-center and the unlocked snap-to-real-coordinates — re-anchors, not motion — can never fake a reveal, and the reveal rides the same call stack as the first qualifying move (no added latency). Lock loss and overlay close reset the gate: every re-anchor returns to hidden-until-movement. - The relay's cross-mode-room join refusal becomes a client reload verdict (#13303 residual; 5.2.33 cargo). The split-brain belt refuses a LIVE-surface join addressed at the dev-coordinate twin of its room (a prefix-less legacy id whose
live-form kiln's rooms.mode registry names a real live room) withRELAY_CLOSE.crossModeRoom(4412). The one honest producer of that address is a stale pre-13303 kiln PARENT bundle — fresh loads normalize at buildWebSocketUrl, and disconnect-rejoin remounts the iframe, never the parent that mints the URL — so nothing inside the iframe can heal it. The engine client maps 4412 as never-retry (room-runtime's close table: every redial re-asks the same stale parent for the same wrong URL), and the host answers with two moves:spawn:kernel:reload-verdictto the parent page (kiln's reload-verdict relay performs a fullwindow.location.reload(), loop-breaker bounded — the reload picks up the fixed bundle AND lands the session in the live room) and an error wall carryingterminalBootCondition: stale-bundle-reloadfor parents that predate the handler (boot-stall stands its remount ladder down; the copy names the manual reload). Pinned byroom-runtime.test.ts(4412 never retried, reported willReconnect:false) and relay-side bymarker-door.workerd-test.ts(refusal +relay.cross_mode_join_refused+ fail-open on an unregistered twin). - God-mode undo/redo commits on the relay tier (dig 32d6b57b — Cal's hollowmass, hm-pen-16).
undo()/redo()'s client branch was prediction-only by design: on the kernel tier the server world drains the same replicatedgod:undoinput and runs the authority branch, so the client's ECS-only apply is exactly a prediction. The relay tier has no server world — the relay-wire client IS the god-mode persist authority (isGodModePersistAuthority) — so the prediction was the only run and no structural undo ever reached the doc: every ctrl-z restore of a deleted object vanished on reload (game_specs receipts: the delete sweep landed at v21840, zero re-adds ever). Now a relay-wire client takes the authority branch — full spec-mirror bookkeeping plus the mutation enqueue, which leaves throughdrainTomeSpecMutationQueue's relay forward and kiln's fold, batched (undo of an N-row cascade is one enqueued batch, chunked by the existing 8192-count forward), and the restore carries the FULL captured row: behavior, state blob, tags, model. Kernel-tier clients keep the prediction-only branch byte-for-byte (a local enqueue would double-record against their server's run). Also fixed in the same seam: the authority branch's durable respawn mutation is now place-stamped (mirror ofdurableSpawnSpec/commitObjectIntoSpec) — the captured row is place-stripped by mirror convention and kiln's fold resolves a placeless NEW row todefaultPlace, so an undo (or redo of a spawn) in any other place restored the row into the wrong map on reload, on every tier. - Relay client worlds simulate NOTHING before Ready (ledger 2557's root-cause cure — the crownfall zombie-simulator dig 998860d4; 5.2.33 cargo). A wedged builder-body summon is a full engine client by construction, and its pre-Ready world holds only checkpoint/journal fossils — a host table naming its own stable clientId at a dead epoch, a suspension row still stamping a player who is live elsewhere. The simulate-when-unknown authority defaults let that world adopt hostship from the fossil table and (through the stale suspension row skipping the owned-by-client exclusion) a LIVE player's whole envelope: a stale simulator for up to its 180s watchdog window, on a summon loop, whose state writes fan to the room as ordinary keyed heads — seele's four divergent player copies. The cure flips the default at the two derivation sites:
getLocallyHostedPlacesanswers empty on a relay client world that has never applied its relay Ready grant (so the exclusion word can never read host-simulated — simulation now requires a POSITIVE authority fact: own envelope, local client-realm spawn, sim lease), andshouldSimulatePlaceHere's no-local-player mode-both lane is closed on the same worlds (that lane is the container/harness affordance). The Ready latch is the welcome-edge wire stamp (ClientRelayWireModeResource, written per determined Loaded welcome and kept across socket churn — a Ready world keeps its scope through reconnect outages); the realm test is the transport's live wire fact (ClientWireModeResource, moved to the netcode-transport leaf beside its type so control-utils can read it without a cycle through relay-world-sync). Kernel/container tiers, singleplayer, and bare harness worlds keep their semantics byte-for-byte; the legitimate suspension takeover is untouched (relay hostship is only AUTHORED at session-live, past Ready — the takeover test pins the post-Ready lane still granting). - Sim-cost attribution reaches the exec seat on player-hosted relay rooms (r-f67665bf — Savi's platform-gap from Nine Heavens 330e6aea; 5.2.33 cargo, the 5.2.32 cut predates it). Three pieces, all routing existing measurements: (1) behavior-update's timing gate widens to multiplayer client-auth tabs — under client-auth every update a tab runs is its own simulation duty, so the elapsed the watchdog machinery already shapes now feeds
SimAttributionResourcethere too (per-script rings, keyed by script file name; the budget watchdog stays authority-gated — attribution never parks anything). (2) The client perf report (engine.perfAttribution) carries per-script cumulative window ms (sim.top[].totalMs), the behavior-crossing census (behavior: totalEntities/updatedEntities/scheduleSkippedEvery/scheduleSkippedNear — behavior-update's existing per-tick debug resource, one get per report), andhostedPlaces(whose sim the rows attribute). (3) A relay lane (relay-perf-attribution.ts, the client-sight idiom):getPerformanceSnapshot()'s arm fans an EPHEMERALperfmeasure-requestto every member, each arms its own bounded reporter and remembers the asker, and reports return as DIRECTED EPHEMERAL frames consumed into the asker's local mirror through the same validate + rate-limit door the kernel command lane runs —engine.perfAttribution's relay lane flips "none" → "local" (consumed at egress, never journals, nothing sends while nobody asks). Kin 2422 (getClientHealth self-only on relay rooms) is the same lane shape — an always-on sibling would fanengine.clientHealththe same way. - Worker self-stall no longer reads as wire silence (dig 8e92f5ac: playtest-3/tophat — a 33.7s sim tick on a starved CPU minted close 4440 and remounted the iframe twice). The runtime worker's sim-health monitor now remembers its most recent blocking tick (
lastSelfStall), and the upload ack-liveness heal consults it beside the existing row-2515 page-thread hold: a self-stall of at least one heartbeat cadence that ended within the current bound means the measured ack silence was unobservable (our own loop couldn't send heartbeats or process acks), so the verdict HOLDs and re-arms instead of closing. A real ack death still condemns one full bound after the wake, judged on observable time only. - The texture bake watchdog discriminates starved from wedged before killing (same dig: three kill-all waves = 46 bake retry-failures on one starved session whose scripts baked clean minutes later). Zero progress across
BAKE_WATCHDOG_MSis now a PROBE, not a verdict: the transport pings the worker, and only a ping unanswered forBAKE_WATCHDOG_PING_GRACE_MS(10s) is a wedge — a starved worker answers late (the bake pumps one budget slice per macrotask, so its loop stays alive) and just gets a re-armed window. A genuine kill fails ONLY the wedged bake; queued bakes carry across to the fresh worker instead of mass-failing. - Back-face grass shades like the carpet again: the two-sided normal flip no longer touches terrain-sampled normals (dig f78408c1, jacob's dark-grass thread 1787353911; 5.2.33 rider). Grass/sprite decoration cards deliberately shade with the placement sampler's terrain up-normal (decorationSamplesTerrainNormal — the classic "grass shades like the ground" trick), but the decoration fragment applied its two-sided GEOMETRIC flip (
N *= select(1.0, -1.0, !front)) to every variant — so each back-facing blade pixel was lit with a normal pointing straight DOWN: zero sun at dot(N, L) ≤ 0, the ambient hemisphere at its darkest direction (the sky-v2 starter's SH probe IS the ambient), black blades salted through the meadow that swapped sides with look direction (tucker's view-dependence datum). Fixed in both lanes: the fixed lane forks at WGSL emission (variant.terrainNormal⇒ no flip; geometric variants keep it — compile-time, zero runtime cost), and the scripted lane — where the normal choice is per ITEM at runtime (item.fx.w; one shader serves the whole layer) — gates the assemble.ts flip in the fragment via the new vertex-pathkeepBackNormalWgslseam (frontFacing || (item.fx.w > 0.5)), with the item uniform made fragment-visible in the scripted group-3 layout. Red-first receipts: WGSL emission pins (decorations.test.ts, decorations-scripted.test.ts — red on master's emission), plus the new real-device gatescripts/verify-decor-backface(verify-terrain-jitter mold, production pipeline + card geometry + record ABI on the L4): on master's flip the back-face blade reads centerLuminance 0.0037 vs front 0.3857 (the black blade, on silicon); post-fix 0.3827 ≈ front (the carpet property), and the geometric control (0.278, lit only BY the flip) held in both worlds — the fork's other side keeps its viewer-facing flip. - Game UI can save files:
allow-downloadsgraduates onto the realm iframe sandbox (r-19dfda8a — Savi's platform gap, Story Builder ee0eeb9f; 5.2.33 cargo).REALM_IFRAME_SANDBOXlacked the token, so the sandboxed-downloads flag refused every authored export — Story Builder's Export window (Blob →URL.createObjectURL→a[download]→ click inside a real user click) SILENTLY no-opped: no throw, no event, no file, nothing script-observable (the silent-no-op family). The graduation is one token; the security shape is measured, not assumed, in the new browser-gate suitetests/browser/suites/realm-download.pw.ts(chromium + webkit): (1) under the byte-exact sandbox a real click's Blob export fires the browser's own download — user-visible, cancellable, riding the browser's download pipeline; (2) a control frame with the token stripped stays download-free on the same real click, proving the token is load-bearing and pinning master's exact pre-fix no-op (red-first receipt: on master's tokens the suite reds with a 15s download-event timeout off a real click); (3) a documented-verdict SENTINEL: a first NO-activation programmatic click currently fires the download in BOTH engines — the HTML "allowed to download" check makes gesture-gating UA-optional and neither engine takes the option — pinned so a flip in either direction re-opens the decision. The jsdom pin inrealm-manager.test.tsholds the exact token set (allow-scripts allow-modals allow-downloads) so every future sandbox edit is deliberate. Named residual (the filing's ask-2): a browser-refused download is UNOBSERVABLE from inside the frame — the click returns void, no error event exists, the refusal lives only in the browser's console — so no loud-failure machinery is buildable frame-side; the graduation itself removes the one refusal lane we controlled.