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.24w agoConnectionengine v5.11mo agoFor Realengine v5.02mo agoAtelierengine v4.62mo agoSurface Tensionengine v4.52mo agoSolidengine v4.42mo agoGroovyengine v4.33mo agoContinuumengine v4.23mo agoFoundationsengine v4.13mo agoGenesisengine v0.13mo ago
← All posts
← All posts

engine v5.2.23

Engine v5.2.23

August 12, 2026

A patch in the Lume line.

what's new

  • Savi now files the features you keep wishing for with the Spawn team — a dream that comes back session after session gets written down once, in your words, instead of staying her private workaround.
  • If a script's event listeners ever get marked dead (after an error storm), registering a new listener now just works — the old "dead ears" verdict can't silence fresh code anymore.
  • Fixed the bug where every world could render as a blank white screen (the August 8 incident) — the change that caused it never shipped to live games and is now fully removed.
  • "I reported this to the team" is now true in every kind of room — bug reports from multiplayer relay rooms reach the team the same way they always have from other rooms.
  • When you summon Savi's builder body into a multiplayer room, she now arrives a polite conversational step away, facing you — never inside your face.
  • While a custom sky image is still generating or loading, the sky shows a pleasant placeholder color instead of going pitch black.
  • When a script sets a material property the engine doesn't understand (like three.js's map), the engine now says so once and suggests the right name — instead of silently ignoring it.
  • Scripts that import each other in a circle now get told exactly which files form the circle, instead of a generic compile error.
  • When Savi deletes objects that a behavior script spawns (minions, lamps, pickups), she now learns on the spot that the script will bring them back on the next room restart — so she edits the spawner script instead of leaving you a delete that silently undoes itself overnight.
  • Entering god mode (or hopping into a vehicle) while holding a movement key no longer leaves your character auto-running in that direction. Your game's own input handler gets one final "all keys released" frame when control moves away, so held-key state settles exactly like a real key release.
  • Shrinking an object with the built-in scale grip no longer flips into growing when you drag past its minimum — it holds at the minimum size until you drag back out. Growing feels exactly the same as before.
  • Going back to a place you'd left — lobby after a match, hub after a dungeon — now brings its scripts back listening. Menu buttons, referees, and anything else wired with api.on respond after the round trip instead of going silent until a page reload.
  • Swapping which script an object runs while nobody is in its place now takes full effect on the next visit — the old script's event handlers no longer linger on the object.
  • Objects parented to scaled objects (like pickup items on a scaled pile) no longer randomly grow — or jump position — when players move between places or the room re-syncs. They keep the exact size and offset you authored, every time.
  • You can freeze a character at an exact frame of an animation: mixer: { pose: { clip: 'Sit', speed: 0, time: 0.4 } } holds the 0.4-second pose instead of snapping to the clip's first frame — posed statues, card art, seated NPCs that sit HOW you posed them. Change time to re-pose; use it with normal speed to start a clip partway through.
  • Things you delete now STAY deleted after restarts. In multiplayer rooms, deleting an object from a script (or having Savi delete it) used to quietly un-happen the next time the room came back — the room's memory resurrected the ghost. Deletes are recorded for real now, no matter who or what does the deleting.
  • Editing your game from a phone in a multiplayer room now actually saves — before, those edits could silently vanish on reload.
  • The engine's helpful heads-ups (notifyDm messages and built-in warnings) now reach Savi from multiplayer rooms too — and if one fails to send, it says so in the logs instead of pretending it went through.
›technical notes
  • skills/debugging.md: recurring cross-session wishes are platform gaps — file once, tell them (ferhat dig 1f4b0ce8). One sentence added to the reportPlatformGap paragraph; no tool or API changes.
  • A fresh api.on registration refutes a standing dead-ear verdict (#12511, train item 15). clearBehaviorLivenessFaults (behavior-faults.ts) clears kind:"liveness" records — and only those — at api.on registration time, for both scopes that would silence the new ear (the registration's own script scope and the whole-entity scope). A live registration is direct evidence against "these ears are dead"; if OTHER ears on the entity are still stale, the next emit re-derives their verdict through the dispatch fault floor — nothing is lost, only re-proven. Pinned by script-edit-event-rebind tests.
  • REVERT of #12506 (sev fix-forward #12519, postmortem docs/postmortems/2026-08-08-white-worlds-sev.md). The 5.2.22 draft's <meta name="color-scheme" content="dark"> + html { color-scheme: dark } on the game documents made Chromium composite the WebGPU canvas opaque white (centerMeanLuma 251.5 → 149.1 on the strip arm; either declaration alone broke it; documentElement.style.colorScheme toggled it live). Both declarations are stripped from cf-edge's iframe HTML and the cf-kernel dev shell, and both documents carry inverse guard pins: the game document must not declare any color-scheme. Engine, relay (#12502), storage (#12500), kiln, and chat were exonerated with receipts.
  • reportEngineBug gets its relay lane (#12526, r-73bad3a1, ravi's dig 0c58bbb6 — tseele's Wickworks bug died as a memory note). New dispatch lane "relay-rpc": on a relay-wired client world the incident ships over the relay's identity-stamped RPC lanes (lane "incident") to kiln's intake. The send door mounts beside the GameDB io (netcode consumers seam, capability-not-wire-mode); RPC_RESULT receipts log verbatim, so a lost filing is loud, never silent (ledger 2089's law). Platform halves riding worker deploys, not this tarball: cf-relay forwards lane "incident" → kiln /api/relay/incident (relay service auth; room scope + reporter identity from the relay stamps, never the payload; per-variant rate floor), and the store leg is shared byte-for-byte with the kernel-tier SDK-keyed door so the sweep and incident-ack lane consume both room classes generically. Head diagnostics gain dumpRail: 'relay-rpc' for provenance. Deploy order honored: relay + kiln before kernels that dispatch the lane — an old relay answers 400 "unknown rpc lane", logged verbatim as a failed filing.
  • Savi body arrival position — a conversational offset, never the joining player's face (r-2dc06081 position half; annias' partner report via mavi). Every joiner self-spawns at the place's one deterministic spawn point with zero separation machinery, so a summoned body could materialize inside a player and a loitering body filled the next joiner's camera. The fix stays in the shell (arrival-position.ts, ArrivalAnnouncer idiom — decided once per process at the first poll that sees the body, retries ride the 2s poll): live foreign player within 3m → step ~3.25m in FRONT of their replicated facing and turn to face them; nobody close → step ~3.25m off the spawn along the body's own facing and turn around; candidates inside ANY player's personal space rotate around the anchor in 45° steps. Data plane is session.teleportSelf + optional yawRad (answered yawApplied: true); /healthz reports arrival: {state, detail}.
  • Loading equirect shows the fallback base color, never a black frame (#12508, train item 21 — the placeholder ladder's first rung for pending texture bakes). sky-store.ts: a pending equirect renders EQUIRECT_FALLBACK_COLOR instead of routing to SKY_KIND_NONE (whose store default [0,0,0] cleared the frame black); texture adoption on arrival is unchanged and pinned.
  • Warn-once on unknown three-vocab material props (#12513, ledger 2107). Every Mesh*NodeMaterial constructor returns a thin Proxy whose set trap warns once per prop name process-wide (curated map for three's texture-map props, close-name suggestion for typos, compact supported surface otherwise), then performs the assignment unchanged. Known props forward with no bookkeeping; only set is trapped, reads stay raw. The teaching rides the existing shader-vocabulary-unknown-symbol diagnostic rail (#1153's channel).
  • Circular-import failures get a structured verdict (#12513, ledger 2111). script-modules.ts mints CircularDependencyError (code circular_dependency + the cycle's closed file list) at all four cycle-detect sites; the exec engine stamps the code on the outcome, the host threads it onto ScriptExecResult, and it rides the existing code channel end to end (kiln exec route and run_script already forward it). The SP-forward reply leg passes compile-class verdicts through while still dropping forge-able forward-lane codes; clone-fallback preserves it on degraded replies.
  • run_script results carry spawn provenance for destroyed behavior-spawned entities (the seele resurrected-lamps class, dig b52f2842; engine resolver 35103eafd8, ravi r-ea36708d). ObjectAPI.destroy() on exec overlay worlds resolves the target's TomeSpawnedBy provenance AT DESTROY TIME (resolveSpawnProvenance, tome/ownership.ts — by settle the component is gone), deduped per spawning owner (a batch destroy of one spawner's brood is one row: {ownerEntityId, behaviorScripts, destroyedIds (sample), destroyedCount}), ungated by persist (a transient destroy misleads identically), and rides the TransactionLog to settle as ScriptExecResult.destroyedSpawnProvenance. Live-world behavior destroys (the projectile hot path) record nothing and pay ~nothing (WeakMap keyed on the overlay, same idiom as the property-write touches). cf-studio-chat composes the fact into run_script's note: live owner with scripts names the spawner and says the durable fix edits it; a dead owner gets the softer shape (owner id kept — the child's own component asserts it — scripts unknown); no provenance leaves the result untouched.
  • Release-on-control-transfer (ledger 2129, ronnie/enigmagames stuck-WASD). tomeInputApplier tracks the last resolved dispatch target per controller (TomeControlDispatchTargetsResource); when the resolved target changes, it dispatches one final zeroed InputState (empty targets behind the memoized axis/action proxy handlers — declared axes read 0, declared actions read false) to the PREVIOUS target's onInput before the frame routes to the new target. Mechanism this deletes: after setControlMapping(player → god rig) every subsequent frame — including the keyup's zeros — dispatched at the god rig, so the parked body's onInput never ran again and game-authored intent state latched at the last held direction integrated for the whole god session. The keyup was never lost (all raw-capture flushes exist; god entry even calls clearInputState) — the latch was game state only onInput can settle, so the fix delivers a release the game's own code interprets, never an engine write into authored state keys. Release mirrors the primary dispatch's per-side guards exactly (behavior resolvable, not faulted, !isClientAuthClientSimulatedEntity) so whichever side runs an entity's onInput delivers its release; template is the parked-body action fan-out in the same file.
  • enterGodMode velocity hand-zero deleted (commands/god-mode.ts). The old special case zeroed state.velocity at entry — it missed every intent-latching game and its zero was overwritten next tick by intent-driven acceleration. Subsumption is pinned by test: a velocity-latching player script settles to zero through the release frame.
  • Size-star shrink is monotone through the screen centre (ronnie/enigmagames scale flip, dump a9c143a1). The three scale gestures in default-editors.ts (fallback hover star, attachUniformSize, emitterSizeHandle) measured the drag as screenRadius(now) − screenRadius(press) — an UNSIGNED angular distance off the pivot's on-screen point. That measure bottoms out at zero as the cursor crosses the object's screen centre and rises again on the far side, so a long shrink drag visibly flipped into a grow mid-gesture ("shrinking past a certain point flips and starts growing"). The scale VALUE never went negative (the ×0.02 factor floor predates this) — the through-zero was in the drag measure. All three now press-freeze a radial axis via radiusDragSnapshot and read radiusDragSignedFromSnap — the same monotone signed-radial primitive the radius rings and NPC vision-range grips already use (radialDragPress/radialDragSigned in builtin-authoring.ts, whose own doc names this exact failure: "dragging toward the centre keeps shrinking, never flips back to grow"). Shared factor math lives in one helper (scaleDragFactor), floored at SCALE_FACTOR_FLOOR = 0.02 — the gesture can never zero an object or mirror it through a sign flip, and a drag on an already-mirrored (negative-scale) object preserves the sign because the factor multiplies.
  • Boundary, stated on purpose: only the interactive tool clamps. Authored scale through the ObjectAPI stays unclamped — scripts legitimately mirror with negative scale — pinned by test.
  • Place re-entry re-arms api.on listeners (ledger 2145, dump b3959a68 — BUG SWEEPER's dead SOLO/DUEL menu and lost PvE-ladder MMR). Two engine legs, both fixture-proven red before the fix:
    • The delta-0 short-circuit ate the re-entry apply. unloadPlace destroys a place's entities but left the applied-generation record's binding intact; a re-entry whose spec content, residency key, and instance-places key all match the recorded generation short-circuited the parked placeResident re-apply, and the place never re-materialized at all — empty skeleton, no state, no ears. unloadPlace now drops TomeAppliedGenerationResource ("the world already IS this spec" was just falsified), so the next apply full-passes and the spawn loop's hasEntity checks rebuild the place. Cost: one full pass per post-unload apply; the gate re-arms at that apply's completion.
    • Module-memory persistence defeated the once-guard wiring shape. Compiled behavior modules live in the content-keyed behaviorCache for the process lifetime, so a module-scope once-guard (if (!wired[api.id]) { wired[api.id] = true; api.on(...) } — the exact shape api.on's own run_script teaching warn prescribes) never re-wires on a fresh materialization: the guard is already true, and the subscriptions died with the destroy. Now api.on stamps armedInOnSpawn provenance (via the existing onSpawn-context probe), the place-unload teardown PARKS non-onSpawn-armed subscriptions (TomeParkedEventSubscriptionsResource, threaded as DestroyEntityWithHookOptions.parkReentrySubscriptions through the cascade), and every later materialization at the same id — spec-lane re-expansion or a re-run producer's deterministic-id api.spawn child — revives them. onSpawn-armed ears clear as always: their producer re-runs (RUN inline server-side, the DEFER drain on clients), and reviving them too would double-deliver. Parked rows whose script's edit generation moved drop at revive (the edited script's fresh module re-wires itself); the park dies with the world-reset sweep.
  • Deliberately NOT fixed here, named in ledger 2145: the relay-tier journal-refold lane (BUG SWEEPER's actual tier) re-materializes entities through replication ingest, which never runs spawnObject — the DEFER-park needs extending to ingest-materialized hook-bearing entities in unhosted places. And cross-place emits into an unhosted place (the rank-write hole) exist independent of the deafness.
  • Parked re-entry ears now check behavior membership at revive (adversarial review of #12599 / ledger 2145). The park's edit-generation check only catches CONTENT edits of the same script; reassigning behavior: ["referee.js"] → ["other.js"] while the place is unloaded bumps no generation (referee.js never changed), so the parked referee.js ear revived onto an object that no longer runs referee.js and its handler fired on the new def — red-proven in place-reentry-park-revive-guards.test.ts. reviveParkedEventSubscriptions now takes the materializing def's behavior refs (spec lane passes def.behavior, the api.spawn lane passes resolvedSpec.behavior) and drops parked rows whose arming script left the list. Safe for every script-reachable sub: api.on binds to the arming entity itself and scripts hold no foreign full API (api.spawn returns an id string), so a sub's scriptRef is always one of its own entity's behavior refs; ref-less engine-side subs are exempt.
  • Review pins landed alongside: double-armed (onSpawn + update-once-guard) stays at exactly two ears across the round trip; the park's diverging-id residue (module-scope counter id schemes strand one entry per round trip until world reset — deterministic script Math.random re-mints identical ids and revives cleanly, so it does NOT leak) is pinned honestly; the pre-existing reset-lane deafness (resetTomeWorld drops the park while the once-guard module survives the content-keyed cache — update-wired ears never re-arm after an explicit reset) is pinned as it.fails so the fix flips it loud.
  • Torn-frame gate in client local-transform projection (ledger 2135, enfeul's furniture-sized frag grenade, dig 3e3d04e9). projectEntity derives Local* as inv(parentWorld) ⊗ childWorld; when the parent's World* wasn't available, every lane manufactured an identity parent — a missing parent entity fell into the "root" branch (Local ← World verbatim) and a present-but-unsolved parent divided by identity. Either way the child's world scale baked into LocalScale as a durable AOI-replicated authored-truth write; the next hierarchy solve recomposed parent × local and the child grew by the parent's scale per torn frame (2.3 → 5.29 → 12.2). The position and rotation lanes had the same flaw — a bake there teleports/mis-rotates instead of growing. Now projectEntity resolves the parent frame once (readParentWorldFrameIntoScratch): a parent that is missing, or present with NONE of the three World* rows, is a torn frame — the whole projection is skipped, nothing is written, and the entity stays queued (torn carry-over in the system loop) so the derivation re-runs when the parent's frame lands. Structural, not a sixth guard: the wrong write can no longer be manufactured in this shape.
  • Partial parent frames still project, deliberately. A parent with SOME World* rows keeps identity/zero fallbacks for the missing aspects — the same convention solveChild composes with, so the division inverts the compose exactly (round-trip stable). This is a legitimate steady state, not a tear: physics write-back mints WorldFeetPosition/WorldRotation but never WorldScale, so an unscaled dynamic-body parent (car-weld rigs) carries pos+rot rows forever. Pinned by test so the gate never tightens to any-row-absent and freezes those children.
  • Guard census (the managing-not-eliminating check, ordered by the mint): none of the five existing torn-frame guards dies under this gate — each closes a tear where the parent's rows are PRESENT but values are stale, mixed-authority, or in another basis: singleplayer short-circuit (#154, child rows hold parent-local coords pre-solve), god-mode-drag skip (parent moved mid-drag, child stale), stand-rider skip (child last-physics vs parent fresh-ingest), bone-subtree skip (#308, World = bone ⊗ local basis), eager attach compose (#160, attach window). All kept; census recorded in the gate's doc block. Deleted instead: the identity-manufacture branches themselves — the per-lane parent reads and their fallbacks collapsed into the single gated frame read, and the dangling-parent-as-root branch is gone from all three lanes.
  • Mixer channels take time — the pose-freeze / seek primitive (seconds into the clip). speed: 0 was already legal but only ever showed frame 0; the raw three.js equivalent is two lines (action.paused = true; action.time = 0.4), so this is an API gap, not a pipeline. time is honored on create AND update: a layer is born at the seek, an UPDATED value re-seeks the playhead, and a re-sent equal value never disturbs playback (scripts re-derive and re-send channel state every tick — the no-op law). A startTick restart (one-shot re-fire) lands on time, not 0. Negative clamps to 0 at parse. Plumbed through the whole seam: spec type + Zod schema, parseMixerChannel, updateChannel merge + mixerChannelsEqual (time-only updates are never swallowed; time: null clears the stored seek), the per-character mixer (clip-state.ts — ActiveClipLayer.seekTime tracks the last-applied authored value), and the horde batch path (skinned-batch.ts — same law, and a seek is an event: it resamples immediately instead of waiting out an anim-LOD hold, so the batch↔clone seam agrees on the pose).
  • Rail-lane audit closure (#12674, tucker's commission; audit doc docs/plans/rail-lane-audit-2026-08-11.md): every sendable message gets a lane or a loud refusal. Four verified gaps from the 13-agent rail/lane census (215 rows, 144 names, 25 candidates adversarially verified), each re-proven still-real at HEAD before building. Kernel parity: nothing new rides the wire.
  • The resurrection cure — the ghost-destroy adjudicator widens past god-mode (rail.destroy residual; tide's destroy-authority adjudication, the journal-resurrection class). judgeRelayGhostDestroy drops its god-mode gate: ANY outside-invocation destroy — script, run_script/exec, god-mode alike — of a departed/unowned journaled entity tombs, or intent-routes to the live owner; sim-lease claim capture stands aside on relay (claims can never confirm there — lease.claim is a dead lane), so captured destroys stop silently reverting. This completes the covenant family whose god-mode face already rode this draft (a98bed581a): the class was entities outliving their own DESTRUCTION — script/exec destroys of hosted/unowned journal entities minted no tomb on every promoted engine, and the host-adoption restate (relay-world-restate.ts) re-journaled the ghosts as fresh keyed heads on every churn, which is why world resets healed for only days at a time.
  • spawn:kernel:spec-save settles on the mobile embed (was silent park). GameHost (/embed/play) had no listener — a permissioned creator's relay-room edits parked after 3×30s and were silently gone on reload. VariantViewer's settle flow extracted to core/kernel/spec-save-relay (one implementation, two mounts); appId-less hosts answer a typed terminal host_has_no_save_lane refusal instead of parking.
  • tome.dm.notify gets a lane (was none → {local, dmNotify}). Egress consumes into an identity-stamped RPC on cf-relay's new 'dm' lane → kiln /api/relay/dm-notify → the shared studio-chat notify_dm door (source pinned 'tome'). Rate window + once-key dedup kiln-side (Redis, fail-open, claim released on failed delivery); failed DMs land in the runtime log instead of pretending they sent. Platform halves ride worker deploys, not this tarball; an old relay answers 400 "unknown rpc lane", logged verbatim as a failed filing.
  • rail.interactClaim journals peer-visible grants (was none → peer). Peers register (sender × target) grants in a kernel-mirrored table (relay-interaction-grants.ts, TTL/cap drift-pinned) honored at exactly the kernel-authorized rails — relocated hook terrain edits, dabs, and grant-sourced enterPlace converge room-wide and fold for late joiners.