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 v6.0One PlaceSep 5, 2026engine 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

One Placeengine v6.03d agoLumeengine v5.22mo agoConnectionengine v5.12mo agoFor Realengine v5.03mo agoAtelierengine v4.63mo agoSurface Tensionengine v4.53mo agoSolidengine v4.43mo agoGroovyengine v4.33mo agoContinuumengine v4.24mo agoFoundationsengine v4.14mo agoGenesisengine v0.14mo ago
← All posts
← All posts

engine v5.2.36

Engine v5.2.36

August 29, 2026

A patch in the Lume line.

Clicking your game's buttons no longer steals the keyboard, models with shape keys can blink and smile, uploaded models are solid in multiplayer rooms, objects stop flickering back to old values when two players or Savi edit them at once, busy rooms no longer crash the browser tab, and a player's browser extensions are never mistaken for your game's messages.

what's new

  • Clicking a button in your game's UI no longer steals the keyboard: WASD keeps working through the click, a key you were holding is not dropped, and Space or Enter after clicking "buy" jumps or interacts instead of buying again. Tab opens chat even right after you clicked a game button.
  • Click-to-copy fields (invite codes, share links) no longer eat the arrow keys for the rest of the session. Real text boxes still take the keyboard while you type in them, and a click on the world hands it back.
  • Savi now gets a warning at write time when a game binds "hold Cmd" or "hold Ctrl" next to keys the browser owns (W, T, N, Q), and when a mouse-look game binds Escape — the browser takes the first Esc to release the pointer, so she is pointed at the engine's pointerLockLost signal instead.
  • Savi no longer reports a browser extension's chatter as a fault in your game. If a player has an extension that talks to every page, the game used to count those messages against itself and Savi could tell you your UI code was misbehaving. Those messages were never yours; they are now ignored.
  • Faces move now: models with shape keys (Blender's blend shapes — blinks, smiles, mouth shapes) play them. An animation that drives them in the file just works, and you can set them from a script — properties.morph = { Blink: 1 } closes the eyes, getMorphTargetNames() tells you what a model has. Uploaded a face and got a frozen stare before? Try it again.
  • Objects no longer flicker back to an old state when two things edit them at once. When a second client or Savi took over an object mid-change, the first client could quietly re-save its last value on top of the newer one a moment later — furnaces and conveyors would strobe, the room's traffic would climb, and a tab could run out of memory. The old value is now dropped the moment the newer one arrives.
  • Very busy multiplayer rooms no longer crash the game tab. When a room sends changes faster than a player's device can process them, the engine now keeps only the newest movement update for each mover instead of queueing every stale one — movement stays current, nothing that matters for the world's saved state is dropped, and the browser stops running out of memory and killing the game mid-session.
  • Games that paint lots of objects or particles with script-drawn textures run at full speed again. Animating those objects used to make the engine re-read the whole texture script on every change, every tick — busy scenes stuttered, and the engine's performance governor could switch the game's own effects off right in the middle of the action. The re-reading is gone; the effects stay on.
  • Uploaded and generated models are solid in multiplayer rooms now. A model with a mesh collider used to collide only in singleplayer — in a shared room, players and raycasts passed straight through it however solid it looked. Models now collide by their real shape everywhere.
  • A multiplayer room can no longer be frozen by another player's stale view of who runs what. One player's out-of-date sync data could quietly overwrite the room's idea of which player simulates each area — NPCs and machines stopped moving, buttons stopped answering, and a game's start flow could sit dead for joining players until the room healed itself minutes later. That poisoned hand-me-down is now refused at the door.
  • When you ask Savi whether something is solid, the answer now comes from the collision the engine actually built, not from the object's settings. A model whose collision never loaded used to read as solid while players fell straight through it; it now honestly answers that nothing is there yet, and the collider view draws markers only where collision really exists.
›technical notes
  • Authored UI keyboard ownership: one predicate, every door (the 08-28 input-feel audit, defaults A+B). The frame runtime stamps every authored-UI container with data-input-capture="ignore" meaning pointer plane (the overUi verdict), and isTextEntryTarget read the same attribute as keyboard ownership through closest(), so every authored element was a text-entry target: the frame withheld all keys while a clicked button held focus, posted textentryfocus on focusin into any authored element (raw-capture RESET wiped held movement at the click), left the Tab arm unreachable behind the withhold, and blurNonTextActiveElement (blur-after-action) never fired for buttons. isTextEntryTarget now answers only "can this element take text" — editable input/textarea, select, contenteditable, plus the explicit author opt-out data-holds-keyboard (HOLDS_KEYBOARD_ATTRIBUTE) — with readonly, disabled, and button-type inputs releasing; it is the predicate for the frame withhold, the focusin reset twin, raw-capture's isEditableElement (host editable gate + editable-focus RESET), the creator-shortcut suite, and the Tab machine's exemptions. The world-note verify plate carries data-holds-keyboard. Default B: the frame cancels mousedown on authored controls that are not keyboard owners (chrome-host's data-chrome-focus-guard decision generalized; native drag/resize members of the interactive grammar keep their default), so focus never parks on a button — keys keep flowing, Space/Enter cannot re-fire the last-clicked button, Tab has nothing authored to cycle to; keyboard owners keep native focus. The host's routeClick hands focus to the game surface on a UI click whose press took no focus, unless a realm frame holds focus (a Send button beside a chat field leaves the field focused). Pins re-rooted INSIDE the stamped container (the composition is what is pinned); real-browser scenarios in tests/browser/suites/input-feel-defaults.pw.ts.
  • Browser-reserved keys, the class (default C; widens 5.2.35's held-ctrl/W fault). collectInputBindingFaults consults a small table on the same write-time rail: held ctrl beside W/T/N, held cmd/win beside W/Q/T/N — one collapsed line per map per modifier naming every offender and every chord; tapped modifiers and maps without a partner stay silent. An Escape binding in a lock-preferring game (the spec camera resolved against the default place, specPrefersPointerLock) bills one line pointing at the engine-injected pointerLockLost action; callers (patchInputs, spec-sync) pass the lock posture as context.
  • game-ui skill: the "Text Fields Own the Keyboard" section is deleted — its "readonly included" clause was the homonym's symptom written as design, and the per-button blur() move is a workaround for a default the engine now carries.
  • Foreign window traffic is never billed to the game (ledger 2773). realm-manager.ts's window listener used to hand-mint {kind:null, reason:"bad-shape"} against the authored-realm drop budget for any message whose event.source was the realm iframe's contentWindow but decoded as none of hello / boot-error / progress. Browser extensions inject content scripts into every frame their URL rules match (all_frames), the sandboxed opaque-origin realm included, and the usual frame→parent bridge those scripts post is exactly that message — so one player's extension bought a game three authored realm protocol: dropped 50 messages (malformed/over-rate); latest (unknown kind) [bad-shape] confessions and Savi told the creator their game had a fault. Nothing the game is taught can produce that signature: the engine's frame code posts only the three decodable window kinds, and every authored message rides the closure-held MessagePort. The window channel is now decode-or-ignore; only the port feeds the drop budget (unchanged: bad shape / over-rate there is honestly the game's), and the source gate that ignores forged boots from other frames is unchanged. Pinned in the browser gate on the production frame bundle: a foreign postMessage from the frame's own window (headless twin) and a real MV3 content-script bridge loaded into headed Chromium (suites/headed/realm-foreign-extension.pw.ts) both bill 0 and confess nothing; the positive control (bad-shape-flood conformance scenario) still bills all 60 and confesses byte-exact with the port kind named.
  • Morph targets (Blender shape keys / GLB blend shapes) render — the parser carries them, the mixer plays them, a morph property drives them (ress 08-14, "could the forge head blink?"). gltf-parse.ts reads primitive.targets[] (POSITION delta required, NORMAL delta optional; quantized/meshopt targets decode through the normal accessor path) into ModelPrimitive.morphTargets with names on ModelMesh.targetNames (Blender's extras.targetNames, index strings otherwise) and default mesh.weights; the unconditional "shape keys skipped" warning (#12925) dies — only Draco-skipped primitives still name their lost keys. The clip weights path is now a real channel: clip-state.ts evaluates it as a fourth per-node lane (same layers, envelopes, masks, cumulative normalization over the mesh's default weights) into mixer.morphWeights[meshIndex]. GPU: registry.ts packs each shape-keyed primitive's deltas into ONE read-only storage buffer ([target][vertex]{dpos, dnrm}), and the static + character skinning shaders gain a hasMorph variant (skinning-wgsl.ts: group 3 bindings 1–2 = deltas + a per-visual MorphParams uniform; the vertex stage adds Σ w·delta to position/normal BEFORE the palette / instance matrix, keyed by vertex_index; casters and outline masks morph too). Only shape-keyed primitives ever set hasMorph — every other model's pipeline key and layout are byte-identical. Per visual (skinned-character.ts) the MAX_ACTIVE_MORPHS (8) largest |weight| targets bind each frame (the three-era MorphNode's cap). Shape-keyed models classify SKINNED (isStaticParsedModel false) — they ride the per-visual character runtime even without a skeleton (unskinned primitives draw as rigid children) — and are horde-ineligible (isPoseBakeable); the cull sphere grows by the asset's morphPad (largest delta × node scale). New draw/morph component + properties.morph ({ Blink: 1 } — name or index string → unclamped weight): named targets override the clip's weights per target, unnamed keep the clip's, null hands the face back; the setter teaches once in getLogs when a name matches no shape key of the loaded model. Discovery: getMorphTargetNames() / getObjectMorphTargetNames(id). Not in this slice: morph on the scripted-material (shade) skinned prelude — a scripted material on a shape-keyed part draws unmorphed; the horde/batched path never morphs by design.
  • A rest stamp can no longer re-journal a value the journal has already superseded (ledger 2749). The relay wire session rest-stamps its own remembered value (motionInFlight, written only by its own sends) as a journaled STATE final when session-local clocks say the key went quiet. Its only eviction lived on the session's own publish path (forgetJournaledMotion); nothing on the receive path touched the memory when a PEER journaled a newer value for the same (entity, component). Two clocks confused — session-local quiescence ("I stopped sending this") and journal append order ("newest wins for every joiner and refold"; cf-relay join.ts heads keep their original seq). One writer per key: sound. Two or more: A's stale final lands above B's correction, the head regresses, the corrector writes again, the re-write is the next stale final in someone's memory. Specimen (derrf's War Factory Tycoon dev room, 2026-08-29 ~05:30Z): three writers on the same keys + a flapping main-place seat → 746 journal rows/s (~94% restatement-class), ~24 stale stamps/s across ~90 ids, values strobing under the player, a 311,622-payload client ingress queue, the tab killed. New RelayWireSession.forgetPeerJournaledDelta(delta) — the receive-path twin of forgetJournaledMotion: per-key subtraction from motionInFlight (updates per component; creates and deletes per entity), the same components forgotten from the motion author chain (§11.3 — receivers no longer hold this chain's value), and deletes dropping the entity's churn streaks exactly as the own path does. Fed by the netcode ingress (netcode.ts ingestRelayPeerFrame) for STATE frames in every phase, through the new transport seam noteRelayPeerJournaledDelta (ClientRoomRuntime, the debug wrapper pass-through, the multi-client harness), filtered by the SAME direct-apply hooks the ingest consults so a row this client WITHHOLDS because it simulates the entity leaves the memory alone — the simulator's truth still rest-stamps. Not done, on purpose: EPHEMERAL peer rows do not evict (two continuous writers stopping within one round trip would leave both memories dead and nothing journaled), and a peer's journaled update does not drop this session's churn streak (a paused writer already re-earns by the gap rule; dropping a continuous writer's streak makes its next writes journal, those rows drop the other writer's streak, and two writers on one key settle into journaling every tick). One-writer behavior is unchanged. Pinned: session unit tests (the loop's shape, wall-clock sweep, per-component subtraction, post-correction re-write still stamps, peer delete/create, lone-writer control), the author-chain forget on the binary lane, and a two-real-stack harness run whose pre-cure receipt reads head before A sweep: {author: B, n: 100, seq: 19}; head after A sweep: {author: A, n: 6, seq: 21}.
  • The ingress depth rung: past a bound, motion sheds latest-wins per (author, stream) — the treadmill can no longer grow the client queue to a renderer kill (ledger 2749, kin 2774). Fix 2364's ingress heal is judged at the transport drain edge, and the netcode's budgeted drain never visits the transport while its carry backlog stands — so a treadmill (arrivals as fast as the budgeted drain) grew the queue without the age verdict ever being read: 92,645 → ~311k queued payloads across six meter lines, then Chrome killed the frame. Depth is now its own rung, and it SHEDS instead of healing: past RELAY_INGRESS_DEPTH_SHED_ROWS (16,384 = 8× the material-backlog floor, tens of seconds of the specimen's arrival rate, 5× under the first kill meter), motion-class payloads collapse latest-wins per (author, stream) — the cell term drops out of the coalescing key, so a mover walking the cell lattice stops stranding one row per abandoned cell. Motion loss IS restatement (the lane's contract); journal-bound and CONTROL payloads never shed — a dropped journal row is a permanent below-cursor hole. The latch dies wherever the index dies (drain, the compactors, the overflow heal), so a recovered queue returns to the per-cell law by itself; the entry edge warns once, and the ingest meter's panel line carries motionShed beside queueDepth.
  • Wire carriage for the relay's restatement admission belt (ledger 2749, derrf's earned-restatement loop). RelayWireKeyedRow gains base — the journal seq a rest-final believes it restates — with binary carriage ROW_FLAG_BASE minted only under an invite accepts ≥ RELAY_INGEST_ACCEPTS_RESTATE_BASE (4); valid only beside key, never beside tomb (a recreate is fresh authorship, fail closed). The belt itself lives relay-side and deploys fleet-wide independent of engine pins: appendBatch refuses a rest-final based behind a DIFFERENT author's newer head — never journaled, never fanned, counted as the new rowsStaleRestate traffic class — so the stale-final feedback loop dies at the door for writers that stamp base. At this version the kernel carries the vocabulary but does not yet stamp base on its rest sweep; base-less rows take today's admission byte-for-byte, and this version's own protection against the loop is the receive-path rest-stamp eviction (the rest-stamp-evicts-on-peer-journal note in this same version).
  • buildMaterialScriptEntry memoizes on scripts-record identity (ledger 2778). Per-tick sprite property writes re-hashed the whole texture-script source: writeDrawSprite enters ensureTextureScriptEntry first-line, the DrawSprite set hook re-ensures on every committed set, and each ensure paid the dependency walk plus the whole-source hash before its no-op hash compare — 80 script-textured particles animating size+opacity cost ~320 full-source hashes a tick (writer lane + hook lane), 40–75ms ticks against the 16.7ms budget, so the governor parked the game's own FX (Supervillain Simulator). Every scripts writer is copy-on-write (ObjectAPI setScript spreads a fresh record, the mutation fold owns the record, applySpec stores a sanitized clone) and the applySpec derive gate already keys on that identity, so the record's identity is the scripts-tree generation: memoize (scripts identity, ref) → entry at the buildMaterialScriptEntry choke point, and the settled path is two map lookups for every caller — sprite writes, material overrides, the tween dotted-channel lane, the set hooks, fx sinks, UI-seen refs, and the sync loops. A script save mints a new record and rebuilds touched refs once. Bench (sprite write storm, 80 particles × size+opacity per tick, 3.2KB source): median 4.618ms → 0.783ms per tick, entry builds per tick → 0.
  • Mesh colliders realize in relay rooms: every consumer falls through to the side-local cook (ledger 2786; live on every relay room since the 08-05 default-host flip, #12193). The authoritative collider pass runs only off-client, and a relay-hosted room has no server world (client-auth: every simulator is a client; cf-relay holds no engine world) — PhysicsColliderMesh/PhysicsColliderSource were never written, so every client cooked the real geometry into its side-local byAsset cache and then waited forever for a replication that structurally cannot arrive, while rapier skipped the entity silently every tick: raycasts answered null and characters walked through uploaded/generated models. Now every mesh-collider consumer falls through to the byAsset cook, keyed off the entity's own PhysicsBodyConfig, when NO component exists — cooking is deterministic from the asset bytes, so the local cook is the same geometry the server pass would have replicated. rapier: isMeshColliderReady, makeColliderDescs, computeFeetOffset (body-origin placement — without it the shape sinks a full feetOffset), resolveMeshFallbackLocalBounds (2D cuboids + collider view), buildColliderSignature (a late cook lands via the sync's atomic swap); mantle: resolveEffectiveMesh, buildSignature, the trimesh cook cache key, and zero-shape mesh bodies mark boundsFallback so the unresolved re-probe heals them the tick the cook lands. Component-present worlds (kernel tier, singleplayer) keep byte-identical semantics, the placeholder hold included. E2E receipt in a real relay room, two browsers: pre-fix rays passed through the statue and both characters walked through the wall; post-fix the host's raycast hits, both characters pin at the face, and the surviving client answers the identical hit after host handoff.
  • Derived coordination tables never arrive by wire on the relay tier (ledger 2524, the mirror poison). A peer row carrying tome/place-hosts, tome/sim-leases, or tome/suspended-sessions could replace a client's local mirror whole — the 08-29 specimen deleted the resident's own seat and wrote a terminal seat his election never spoke, and the adoption sweep and shouldSimulatePlaceHere read that mirror with no election in the loop: the occupied place ran simulated-by-nobody for 3+ minutes, its whole start flow dead for joiners. This executes the recorded one-writer design (place-hosts.ts: two writers exist, one per host tier, never both in one world; the tables are derived, and the registry gate keeps derived components out of every upload): the relay-tier peer ingest strips the three coordination-table components from peer rows beside stripPeerInvisibleRows, in every phase — journaled tome/spec heads redeliver at every join/place-drain catchup — with warnOnce for visibility. Kernel/container rooms keep their wire copies (there the server is the one writer and the wire is its lane), pinned by a room-delta-ingest test. Registry-fact keying deliberately rejected: clientAuthWrite "never" is broader (music/sound-loop rows carry it and legitimately arrive by wire), so the set is the three names with the registry gap named in a comment.
  • Solid verdicts and the collider overlay answer mesh colliders from realization, not intent (#13674; dig 2786). entitySolidState answered static-mesh from PhysicsBodyConfig.collider.kind alone, and the collider overlay drew a bounds marker for every mesh config — both broke ledger 2297's recorded definition (the live collider registration at query time, never the spec's intent) for mesh kinds: rapier builds no shape for an unresolved mesh, so a starved GLB collider read as solid everywhere while raycasts passed through. The new mesh-collider-realization read gates both surfaces: realized means a live handle is registered (rapier or mantle — covers the ledger-1642 stale-but-solid hold and mantle's bounds-fallback statics) or the body factory's own readiness gate passes (the pre-sync window; 2D places are always ready). Unrealized mesh configs answer the existing word none and draw no marker. No new vocabulary.