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.43mo agoGroovyengine v4.33mo agoContinuumengine v4.23mo agoFoundationsengine v4.13mo agoGenesisengine v0.13mo ago
← All posts
← All posts

engine v5.2.24

Engine v5.2.24

August 14, 2026

A patch in the Lume line.

what's new

  • Players in other places can now receive state. The classic pattern — queue the party in the lobby, warp to the arena, then tell everyone their match — just works: patchObjectState(playerId, {...}) reaches a player anywhere in the room, and the new getPlayerPlace(playerId) tells you whether someone left the game (null) or is just in another place. No more heartbeats, shepherds, or ticket systems to work around invisible teammates.
  • Turning on pixelate (or chromatic aberration, depth of field, blur) for the first time in a session no longer flashes the screen black while the effect's shaders compile — the game keeps rendering un-graded for those frames and the effect lands as one clean switch.
  • Hitting big things no longer floods the screen with light: the built-in damage flash is calmer by default, sized so a red hit-pulse on a huge tree or wall stays a pulse instead of a full-screen flash. Combat on normal-size enemies still reads exactly like a hit.
  • flash() now takes intensity (0–1), so you can dial an object's hit flash from a subtle glow to the full white-out yourself.
  • Scattered decoration works in multiplayer again. Scatter beds authored into your game's spec (grass, rocks, forests — anything painted or declared with scatter) now appear in multiplayer rooms exactly like they do in solo play. They'd been silently empty in shared rooms; no change needed on your side.
  • Timestamps and big numbers in state are now exact. Storing api.seconds() (or any large number) in state via patchState/setState/direct assignment used to round it by up to ±64 seconds at today's clock scale — enough to break "how long since X" checks in ways that looked like a haunted clock. Stored numbers now come back exactly as written, everywhere, for every player.
  • Game UI that uses drawn art (scripts/tex-*.js images) no longer flashes a big translucent blue square while the art loads. The loading window now paints nothing at all — the art appears when it's ready, and that's the first thing you see.
  • Heightmap worlds can now have real underground. Set underground: true on your terrain (ask Savi, or api.patchTerrain({ underground: true })) and the engine stops treating players below the surface as having fallen through the world — no more being teleported back up out of your basement, tunnel, or subway line. The ground above stays exactly as solid to walk on; players still need a way down that you build (a pit, a shaft, a portal). Below the surface, floors are yours to author — the engine no longer provides a safety net there.
›technical notes
  • Cross-place player mail (r-6f782b46, dig bf02c5e4 — the party-hoppers class-delete). patchObjectState on a target that misses the local resolve but is a room-resident session avatar now routes as per-top-level-key guarded LWW set/unset intents over the existing rail.intent lane — the server validates, sequences, and forwards to the entity's simulator (the target player's own machine), whose basis guard adjudicates and answers the sender's pending ref, byte-identical at the server gate to the same patch made in the same place. No local optimistic preview exists (there is no local copy), so nothing registers a revert; inside a behavior invocation the intents join the invocation's causal chain (basis included), outside one they ride the outside-invocation act lane with an empty basis (the explicit-act destroy's precedent). The local-resolve-hit path is byte-unchanged — the new lane runs only where the old code warned into the void.
  • The gate is a new replicated coordination table, tome/room-roster (TomeRoomRoster, riding tome/spec beside TomePlaceHosts: globally AOI-visible, reset-proof, clientAuthWrite: "never"): clientId → { playerId, placeId } for every session avatar the server world holds, published by the new netcode/room-roster-publish server system (simulation −85, change-gated writes). Absent in singleplayer (never written — mode byte-identical) and on relay rooms (no server writer yet — the mail lane and roster reads degrade to pre-roster behavior there; a relay-side election-fold writer is the named follow-up seam).
  • getPlayerPlace(id?) — the distinct-verdict presence read: place id from local truth when the entity is present (any entity, any mode), from the roster when it is an avatar in an unreplicated place, null when it is nowhere in the room. This is the honest branch point the old getObjectState === null conflation forced games to reconstruct with heartbeat scars.
  • Pins: cross-place-player-mail-e2e.test.ts (two-place fixture through real netcode — delivery in- and outside-invocation, roster/room-truth reads including session-end, gone-target keeps the warn with nothing on the wire, detached-simulator refusal verdict with no blind server apply, and the wire-shape authority pin: what leaves the writer is one ordinary guarded rail.intent).
  • The session's first pixelate/CA/DoF/blur look no longer flashes black (dig 9489e3d8 specimen 3). The first two-hop look flipped the composite's target format canvas → intermediate, and the retained-variant hold is keyed BY format — so the flip was the one seam it structurally couldn't cover: cold composite clears the intermediate black, cold present falls to a cold copy, and declareCopy's only cold move is clearing the canvas black. Two rungs, both landed: (1) seam pre-warm — the first active-look frame warms the copy fallback (post/look/copy/<canvasFormat>) and the neutral intermediate-format composite via PipelineCache.warm() (a look-less session compiles neither — the structural OFF); (2) hold-last-presenter — the topology does not flip until the incoming pipelines are ready: on the entry edge the chain asks LookPass.presentReady() (the sync twin of the capture rig's prepare(); asking kicks every compile) plus the intermediate-format composite handle, and while any is pending the frame keeps the last presenter — composite straight to canvas. Held frames release nothing, so the incoming scripted build survives the hold (a rebuild per held frame would mint fresh WGSL + pipeline keys and never converge — pinned by test). Deliberately NOT hold-last-frame: a stored image re-presenting departed light is the exact class limiter v2 eliminated.
  • flash() gains intensity (dig 9489e3d8, the flash-authoring-defaults alignment). api.flash(id, { intensity }) names the peak emissive boost 0–1; absent = the historical 0.9 (FLASH_BASE_OPACITY), byte-identical wire (additive optional on the flash juice event; invalid input no-ops to default per verb etiquette; >1 clamps). The primitive previously had no amplitude knob at all while screenFlash did — the raw-three.js cost of "flash this mesh gently" is one uniform, so this is a property, not a pipeline. Under reduce-flashing the dim lane only ever lowers further (min(authored, dimRise)). Pins: object-api-vfx-args.test.ts (wire: absent/clamp/invalid), juice-client.test.ts (authored amplitude renders).
  • combat.damage()'s bundled flash drops to 0.45. The builtin's hit juice is a default nobody explicitly asked for, so it carries the platform's care (game-feel: light is the loudest channel): at the primitive's 0.9 a whole-mesh emissive flash on a viewport-filling target IS a full-screen flash per hit — the Game Hour tree-chop specimen (2026-08-10), where a survival crafter shipped a screen-flash-per-cut its own creator called a bug, and the class Drake hotfixed out of his game on day one. Pin: builtin-modules.test.ts (0.45 on the loud path).
  • The taught defaults align with the light-carries-the-platform's-care law (#12360) where the vocabulary actually emits: the screenFlash @tomeapi doc no longer says "the default reads right for per-hit feedback" — it now names screen wash as a rare-emphasis beat and routes per-hit feedback to the calmer deck (entity flash at low intensity, hitstop, particles); combat.md's melee recipe teaches calm flash intensity on big meshes and that screenFlash never rides a connect. api-reference/TomeAPI regen carries both.

Model-lane liveness door + honest loading/generating verbs

Savi could declare a model "dead" with no HTTP receipt (a raycast miss was enough) and re-mint it — 26 wrong-sized vine copies in one prod session while v1 served ready the whole time. The model lane's existing readiness probe already answered the question; its receipt was being discarded.

  • The probe's answer is now latched per asset: alive (probe ready or a 2xx payload) · dead-by-authority (a real authority 404, via the shared provenance idiom) · weather (no answer — explicitly NOT a death).
  • The scene note Savi reads says it plainly: a receipted-alive asset carries "do NOT regenerate or rename"; silence reads "no answer is not proof of death"; only a receipted death says dead.
  • "Still generating" now requires the server's own 202 receipt — cold-cache fetches read loading, counted separately, so a 1GB zone refill no longer masquerades as mass regeneration.
  • Relay-tier spec scatter minting (ledger 2194, conviction 4f6aa889). All four spec-path scatter mint sites (interpreter.ts refreshTerrainScatter / spawnObjectFromDef / updateObject scatterChanged + field-refresh arms) gated getWorldMode(world) === "server" — a kernel-tier-era rule ("clients receive them via replication") that no world satisfies on the relay tier, whose simulating seat is an elected CLIENT host. The gate is now shouldMintSpecScatter: server world OR not a client-auth observer of the bed — the anchor path's own predicate (isClientAuthObserverEntity), false on server worlds and singleplayer by construction, so those tiers are unchanged; unelected multiplayer clients still refuse via the no-hosted-place exclusion (no pre-election dupe storm).
  • The mint moment for fresh rooms: tome/scatter-host-redrive (new client system, simulation −80). The bootstrap applySpec lands BEFORE the election, so the gate swap alone never fires on a fresh relay room. The system watches resolved self-hostship (getLocallyHostedPlaces — identity-cached, one reference compare per steady-state tick) and on a gained place runs redriveHostGainedScatter: ADOPT-FIRST — beds whose children are already recorded (the bed's replicated TomeState.scatterChildren; TomeScatter is replicate:never and deliberately not the key) are adopted untouched, only bare beds mint. Host handoffs therefore never double-mint and never resurrect deliberately destroyed children.
  • Wire/journal pin: host-minted children are STATE creates — the SuppressMutationRecordingResource wrap suppresses spec-changeset recording and spec mirroring only, not replication; children journal as live keyed heads (e:<id>:@c) and fold into observers and late joiners like any hosted-remainder spawn.
  • Pins: scatter-relay-host-mint.test.ts (zoo corpus on the relay client-host topology: elected host mints at apply, observer refuses, singleplayer control, gain-edge re-drive, adopt-first, spec-document-never-mutated) and relay-scatter-host-mint.e2e.test.ts (full relay wire: host mints → journal heads → observer folds the identical id set → host departs → successor elected → adopts, no double-mint; fresh-room apply-before-election boot order mints on the gain edge).
  • State numbers carry full f64 end-to-end (ledger 2184, priority evidence row 2196, incident 110f42ab). Since #12554 armed the relay room clock, api.seconds() is EPOCH-SCALE (~1.786e9), and the state pipeline narrowed every stored number to f32 — twice: at the write funnel (quantizeStateValue → Math.fround) and on the room wire (shaped-object-map SHAPED_OBJECT_FIELD_F32, a 4-byte float lane). Math.fround's ULP at 1.786e9 is 128, so a stored timestamp landed up to ±64 SECONDS off and a correct authored window (now - gapSince > 2.5) misfired ~64s of every 128s window regardless of restamping cadence. #12684 cured one face (god-mode's tick stamps, moved to the session-scale local tick); this removes the class.
  • The write funnel stores what the script wrote. quantizeStateValue/quantizeTrackedStateWriteValue are now sanitizeStateValue/sanitizeTrackedStateWriteValue (JSON-safety walk only — nonfinite→null+teach, Map/Set/Date conversion, function/symbol drops all unchanged); quantizeSimplePatch is copySimplePatch (detach-copy, no narrowing).
  • The room wire's shaped-object-map lane classifies numbers as SHAPED_OBJECT_FIELD_F64 and vec3 records as SHAPED_OBJECT_FIELD_VEC3_F64 (8-byte doubles; +4 bytes per changed numeric state field — the transform/pose hot lanes have their own codecs and are untouched). The legacy F32/VEC3_F32 kinds stay decodable and writable forever (a shape def minted by an old peer via defineFromWire still parses) but classify never mints them again. Every other lane the bag rides was already lossless: JSON component codec, generic/OPAQUE nested values, relay register FIELD_F64, intent command payloads, binary-object codec.
  • Write-through intents drop their frounds (intent-protocol.ts + intent-context.ts): the inc delta is exact f64 (next - prev — the frounded delta collapsed to 0 for any step < ~64 on an epoch-scale value, silently dropping the intent), basis/revert comparison (clientAuthIntentValuesMatch) is exact numeric equality (sound because both realms now hold identical bits), and applied set values detach-copy without narrowing (cloneIntentValue).
  • Unchanged on purpose: transform/pose f32 quantization (quantizePositionLike et al. — render-tolerant, own codecs), physics read grids, collider-scale buckets, the sandbox float-literal/Math f32 regime (cross-engine determinism for script MATH, not storage), and the event-echo memo's fround-at-hash-time canonicalization (idempotent, consistent on both legs).
  • Pins: state-f64-epoch-precision.test.ts (production client-auth wire both directions: top-level scalar, vec3 field, nested object, array — all bit-exact at 1786580200.125; seele's window shape never misfires; all three wire pins verified failing on the pre-fix tree), shaped-object-map.test.ts classify + legacy-kind vocabulary, client-auth-intents.test.ts epoch-scale RMW inc.
  • The game-UI img lane's loading placeholder is now actually transparent (ledger 2202, dump cd8cc346, Kingsplow). TEXTURE_SCRIPT_PLACEHOLDER_SRC was labeled "1×1 transparent PNG" but its bytes decoded to RGBA(0,0,255,127) — half-opaque pure blue (PNG filter type 1 on a 1×1 RGBA scanline [1, 0,0,255,127] reconstructs to the raw bytes; there is no left neighbor to subtract). Every <img src="scripts/tex-*.js"> awaiting its first bake wore that pixel stretched to its styled width at 1:1 intrinsic aspect — a wordmark styled width:min(84vw,940px) painted a ~940px half-opaque blue SQUARE over the game for the whole bake window (~500ms per panel on the reporter's rig; repeated per multi-element panel — the photosensitivity shape enfeul reported, over a HUD Savi engineered to never flash). The constant is replaced with a decode-verified fully transparent pixel (RGBA 0,0,0,0), and the colocated test now inflates the actual IDAT scanline and asserts alpha 0 — the label can never drift from the bytes again. Same lying constant retired from kiln's OG-image test fixture and the assets README example.
  • terrain.underground: true (heightmap kind) declares below-surface space as play area and disarms the chunk-rescue there (r-1a763818, ledger 2225). On a heightmap, under-surface air is unrepresentable, so TerrainChunkRescueSystem has always read "feet > 0.1 m below the sampled surface" as fallen-through-the-world and teleported the body back up — which also ejected players from deliberate under-terrain builds (DJ Def1le's under-terrain train line; chukinice's diggable voxel world made the terrain-kind asymmetry creator-visible the same day). The flag inverts exactly that one reading, on both realms (the predicting client reproduces the server's non-rescue — a server-only disarm would be the up/down warp loop in reverse). Everything else is untouched: heightfield colliders (the surface stays solid from above), the client collider parity probe, the server anchor watchdog, repeated-fire remediation (downstream of a fire that no longer happens), and the voxel entombed rescue. The spec read costs nothing on the standing-on-ground path — it is only evaluated for a body the surface sample already judged sunk. patchTerrain accepts the key via the schema-derived allowlist; the heightmap-terrain skill teaches it next to the walkable-pit recipe.