Spawn
playmakespawnjam
⌘K
visitorclaim namesign in
sign up
playmake
spawn / aboutwhat we're building

pinned

start herewhat spawn isfaqfrequently asked questionsthe betthe spawn bet

updates

engine v5.2LumeJul 10, 2026engine v5.1ConnectionJun 18, 2026engine v5.0For RealJun 4, 2026engine v4.6AtelierJun 1, 2026engine v4.5Surface TensionMay 22, 2026engine v4.4SolidMay 15, 2026engine v4.3GroovyMay 13, 2026engine v4.2ContinuumMay 9, 2026engine v4.1FoundationsMay 4, 2026engine v0.1GenesisApr 29, 2026

pinned

what spawn isstart herefrequently asked questionsfaqthe spawn betthe bet

updates

Lumeengine v5.21mo agoConnectionengine v5.12mo agoFor Realengine v5.02mo agoAtelierengine v4.62mo agoSurface Tensionengine v4.53mo agoSolidengine v4.43mo agoGroovyengine v4.33mo agoContinuumengine v4.23mo agoFoundationsengine v4.13mo agoGenesisengine v0.13mo ago
← All posts
← All posts

engine v5.2.34

Engine v5.2.34

August 22, 2026

A patch in the Lume line.

Six cures: deleted objects can never pop back after the save confirms, players' script-driven saves stop dying silently in relay rooms, tweaking one knob on a custom material no longer wipes the material, group placed pieces without anything moving (keepWorld), roads can vary width along the route, and old room-host images can't silently wedge new engine sessions.

what's new

  • Tweaking one knob on a custom material no longer breaks the whole material. Changing a single parameter on a script-built material (a glow's gain, a beam's density) used to silently throw away the script and every other parameter — the object snapped back to a plain solid-colored default, and it looked like your material was never there. Now a single-parameter tweak changes exactly that parameter and leaves the rest of the material alone.
  • Group objects that are already placed without anything moving: attachTo(rootId, { keepWorld: true }) adopts a piece under a parent while keeping it exactly where it stands — build a brazier or chandelier from placed pieces, parent them to one root, and move the whole thing as one object.
  • null
  • Saves and other database calls made by players' own behaviors were silently dying in relay-hosted rooms — on some games, all of them ("GameDB door forward has no lane on this wire"). Fixed: those calls now ride the relay's database lane and wait out reconnection blips instead of being dropped.
  • Roads can vary their width along the route. Give a road's width a list — one number per point, like [2, 8, 2] — and it narrows and widens smoothly between them: straights that open up, corners that pinch, a plaza that necks down into an alley, all on one spline. Edge lines, sidewalks, hedges, and lamp posts follow the changing edge, and driving/collision matches what you see. A single number still means one width for the whole road.
  • Deleted something and watched it pop back a few seconds later? Fixed. Your delete always saved — a laggy sync lane could briefly re-load the world from just before it and bring the object back. The engine now remembers what your last save contains and never rolls behind it.
›technical notes
  • Dotted writes into a scripted material merge instead of gutting it: setObjectProperty(id, "material.params.X", v) now preserves the script ref and every sibling param (r-3fb1c3c8, ravi's dig 4f5f3ff8, dump e3b929c6; 5.2.34 rider). The dotted machinery already merged correctly — buildDottedPropertyMerge deep-sets into the property reader's flat echo, script included — but normalizeMaterialSpec only recognized the AUTHORED scripted spelling ({kind:"scripted", script:"scripts/x.js"}); the shape the engine itself emits — the reader's echo and the persisted mirror, {key:"Scripted", script:{ref, params}} — fell to the standard branch, where script and params are interpreted flat keys the override collector drops. So every dotted material write on a scripted material handed the writer a value it silently gutted to {key, params:{X}}: script ref gone, all sibling params gone, and a script-less key:"Scripted" renders as the Std/PBR default. That is exactly the merge properties.ts's flat-echo comment promises the echo exists to serve ("Readers feed dot-path writers … dotted setObjectProperty merges"), and the same lossless round-trip contract material-utils.ts pins for standard materials (ledger #173). It hit Savi three times in one session across spellings (material.params.* twice, material.script.params.*) — dump e3b929c6 carries five shaft-n-* prisms persisted as {"key":"Scripted","params":{"gain":1.5}} after the v116 receipts show the full 8-param scripted material: jacob's solid violet slabs. Two changes, one mechanism: (1) normalizeMaterialSpec round-trips both scripted spellings — object-shaped script.ref accepted, top-level params folded over script-carried params (the top level is always the newer write, since dot-path writers deep-set the spec spelling there) — which also repairs the tween-evaluator and god-mode gesture-commit lanes that re-apply the flat read-back through the same normalizer; (2) the dotted grammar canonicalizes material.params.* → material.script.params.* when the current material carries an object script (the model.materials → parts move), so the merge lands beside its siblings, the mirrored spec stays in the one persisted shape, and getProperty("material.params.X") answers what the write landed (read/write parity, both halves share canonicalScriptedMaterialSubPath). Whole-material assignment is untouched — replace stays its meaning. Red-first: the three dotted spellings, the v116→v119 persisted-spec fixture (full material before, dotted write, full material after minus only the addressed leaf), the whole-replace pin, and the mirrored-shape reload path (applySpec re-applying {key:"Scripted", script:{ref, params}}), all red on the old normalizer. Not touched here: a gutted legacy {key:"Scripted", params} with no ref anywhere still folds key-only (no ref to recover), and the b2 half of the dig (reversed smoothstep edges emitted verbatim to WGSL) stays open.
  • The merge contract is now pinned for EVERY dotted subtree, not just material (jacob, in-thread 1787363264: "that foot gun is insane. remove it for all cases not just this one"). The general contract: a dotted setObjectProperty("<parent>.<leaf>", v) changes ONLY the addressed leaf of the parent's echo — every sibling the reader echoes survives — while whole-subtree assignment stays a replace. New census suite (dotted-write-merge-census.test.ts) probes 17 families generically (read echo → dotted-write one leaf → read again → after must equal before with only that leaf deep-set): feetPosition, scale, primitive, material (echo-space), text, sprite, sign, light, audio, particles, fx, physics, visibleRange, jiggle, layout, npc, mixer — plus the families pinned in their own suites (rotation euler leaves, model parts/materials, animated3DCharacter part materials). Census verdict: buildDottedPropertyMerge merges by construction — clone the reader's echo, deep-set the leaf, hand the whole parent to the writer — so the replace-instead-of-merge class only exists where a writer's normalizer refuses the reader's echo and drops what it doesn't recognize; the scripted material was the one live instance in the registry, and the census pins each family against that asymmetry returning. Named bound on the census: attachment (writes ride the parent verb), scatter (template arrays — dotted indices are not part of the taught grammar), ik (channel values carry no sibling records), spline (writer runs the preview pipeline a bare test world doesn't host).
  • setParent/attachTo grow a keepWorld: true option (r-0752ba7b, jacob's brazier dig 3acd7d3f §2) — the world-preserving adopt, three.js attach() semantics. Unspecified position/rotation/scale now default to the local values that keep the child's current world pose under the new parent (rotation/scale invert the parent frame up front; the position leg converts through resolveWorldFeetPositionWrite after the link lands, so pivot folds and socket/part anchors peel exactly as the hierarchy solve composes them). The snap default is unchanged — the equipment idiom and the spec parent write lane both rely on it. Frames with no honest conversion (bone attachments, camera subtrees, ~zero parent scale axes) refuse loudly before any mutation.
  • The private-transport port's admission bar fails OPEN on an absent generation stamp — the port-protocol skew class dies structurally (dig d75f7599, 2557's second lane). The worker's PrivateClientTransport gate (row 2515, 37f6a0c080) was fail-closed on ANY mismatch — if (message.generation !== this.dialGeneration) return; — but the headless room-host shell BAKES the bridge into the container image and DOWNLOADS the worker per room pin, so the two halves of a protocol designed as one bundle can ship from different builds. The pinned b5f063f48e-era image's pre-stamp bridge posted {type:"open"} with no generation; 5.2.31+ workers read that as undefined !== 1 and silently dropped every open/message/messageBatch/close — open never flipped, the hello could never leave, the 4408 close never reached the reconnect ladder, and the summons parked 180s to exit 6. 24 apps wedged for 4 days; no engine pin could fix it (every post-2515 worker carried the bar, and the stale bridge couldn't answer it). The cure per the dig's fix-shape: generation !== undefined && generation !== this.dialGeneration (staleGeneration) — an absent stamp proves the far side predates stamping and is delivered; a mismatched stamp is still a superseded dial's late flush and still drops. The 2515 protection stands whole: in browsers both halves ride one bundle so absence never occurs, and the stale flushes the stamp exists to kill only ever arrive STAMPED from a post-2515 bridge. Red-first: unstamped open/message/messageBatch/close from a hand-posted legacy bridge reach the client (red on the fail-closed bar), a mismatched stamp still drops, a matching stamp passes (room-transport-bridge.test.ts). The protocol doc now names the reader-leniency law: every wire-shape change owes the question "what does the OTHER era's half do with this message?". Ships with the trigger half of the same PR: container-build.yml's auto-advance path filter (and container-image-drift.yml's lineage twin, lockstep) now watches the baked↔downloaded port-protocol seam roots, so a protocol change recooks the image instead of drifting under the pin.
  • Player-behavior api.sql on relay-hosted singleplayer rooms no longer dies at the relay egress' dead-lane drop — the forward gate is tri-state honest (ledger 2438 P1, medivhus's Cardhouse; field kin: the Grind95 save write lost in a host-flap window, 08-19). The forwarded GameDB door's eligibility gate (tome/api/game-db-forward.ts isGameDbForwardEligible) read the 2-state live wire sniff (clientWireMode !== "relay"), and the sniff's "undetermined" hold — the first-frame window AND every between-sockets redial (rejoin, suspend/overflow re-bootstrap, host-election flap) — stayed forward-ELIGIBLE by documented design. Every behavior api.sql issued in a hold enqueued a SqlRequest the egress deferred, and the welcome proving relay dropped the whole backlog typed at the dead-lane drop (game-db-forward.ts rejectDroppedForwardedGameDbCall — "GameDB door forward has no lane on this wire"). On Cardhouse (singleplayer pin, relay-hosted dev room-1) the window read permanently open across three engine versions: 100% of player-behavior SQL dropped while the DB sat provisioned and healthy (migrate_applied=1134), so the creator's all-truth-in-SQL game ran stateless for players. The cure per the row's own prescription: the gate now reads clientRelayWireTruth (the pin-04da3c48 pattern netcode's travel-authority forks already use) — the live sniff when determinate, else the welcome-edge stamp (ClientRelayWireModeResource). A stamped-relay world in the hold falls through to the relay lane, whose posture is composable there (the relay identity survives socket bounces) and whose pre-send park (ledger 2381) holds the call replay-safe until the session opens — hold what can be held. Fresh worlds whose wire never proved relay (stamp absent) and kernel-stamped reconnects keep today's path exactly; the egress dead-lane drop stays as defense-in-depth and now fires only on a fresh world's pre-first-welcome window or a resolution regression. The security boundary is unmoved: relay wires still carry no control-channel door, and the diverted calls ride the relay player lane's existing digest-and-verified-identity argument (or the room_host lane on a credentialed seat). Red-first in game-db-forward.test.ts: the between-sockets fixture (stamp relay, sniff undetermined) resolves the relay lane, parks, and SENDS when the wire opens — nothing touches the dead control lane; the kernel-stamped hold keeps the forward lane.
  • Road width arrays are honored per-point — the schema's existing promise, kept instead of averaged (ravi's vocabulary datum from the #13374 teaching-child; 5.2.34 rider). SplineSpec.width has always typed number | number[], but resolveRoadVisualWidth (spline.ts:1142) silently averaged an array to one scalar — Savi writes width: [2, 8, 2], gets a uniform road, no error (the #13371 build was fooled exactly this way; ravi's fix-forward taught around it in world-composition.md, now reverted). The road surface now lofts a per-frame half-width: width[i] rides points[i] (clamped to the last entry, like rolls) and interpolates linearly by arc distance between them, through the same buildPerPointDistanceSamples parameterization the rolls channel uses — one mechanism, two per-point channels. Roadside features track the varying edge (edge lines, sidewalks, hedges, and lamp posts all take their lateral offset from widthAt(distance)), and the collider is definitionally the same mesh (custom-primitive physics reuses the surface's BespokeGeometry), so collision follows the taper — the lie can't move. Scalar and uniform-array roads are byte-stable: a cross-revision dump of a full-roadside scalar road (16 generated entities, 8 geometries, curving 3D centerline) diffed byte-identical against master. Invalid array entries fall back to the average of the valid ones; an all-invalid array keeps DEFAULT_ROAD_WIDTH — the behavior arrays always had. Red-first pins in spline-road-per-point-width.test.ts: [2, 8, 2] narrow-wide-narrow at the authored points plus an interpolated midpoint, scalar ≡ uniform array, edge lines hugging the varying edge, and the collider-is-the-mesh agreement.
  • A confirmed spec save is now a head claim — the stale-refold resurrection class dies (ledger 2599 P1, cal's god-mode resurrections: thread 1540752985330819262, dig 1580caec). On relay rooms, the spec-save confirm (kiln:spec-save-result ok) splices the batch out of pending custody (relay-spec-save.ts onResult) — the instant the fold-rebase overlay stops protecting the write — but DISCARDED result.version: nothing recorded "my write is folded into every durable version ≥ N". A client on the F3 heartbeat-beacon fallback lane (relay-join.ts onHeartbeatPointer — which re-states the RELAY's checkpoint, one version behind the client's own just-confirmed write by construction, and cal's ONLY spec lane: 31/31 SPEC frames missed on the ring) would then fetch and fold the PRE-write doc, and applySpec's spawn pass re-materializes any spec row with no live entity (interpreter.ts: !world.hasEntity → spawn). Tree deleted at v21943; the beacon folded v21942; tree back ~3s later — on tape twice in the ECS capsule (two #delete:click destroys of the same id, 3s apart, zero inputs between). The cure per the row: the ok verdict's saved version now feeds the SAME spec-claim door SPEC frames and the F3 beacon share (RelaySpecSaveScheduler.onConfirmedSavedVersion → netcode → transport relayNoteSpecSaveConfirmed → relayJoin hooks → RelayJoinCoordinator.onSpecSaveConfirmed → armSpecClaim), so the client never folds a doc older than its own confirmed write: an older beacon can never displace the claim, an in-flight older fetch discards on resolve (the existing supersede check), and once the confirmed version folds the adopted head covers everything below it forever. Claim lifetime is untouched — room-lineage scope, survives socket bounces, dies at resetForNewRoom — and at-or-below-head confirms are covered no-ops, so the ordinary lane (own SPEC frame already folded the post-write doc) fetches nothing. Red-first against a real tome world (real fold-rebase, real applySpec spawn pass): the tape (delete confirms at vN, the beacon names vN−1 — the entity must not respawn), the crossed sub-ordering (pre-write fetch in flight when the confirm lands — resolve discarded), folds above the claim passing untouched, and resetForNewRoom clearing the claim (relay-spec-confirm-refold.test.ts; scheduler-side verdict pins in relay-spec-save.test.ts).