engine v4.1.0
Foundations
May 4, 2026
First big engine update. Faster, smoother, smarter — and a small mountain of fixes.
what's new
Spawn's first big engine update. Faster, smoother, smarter — and a small mountain of fixes.
- Huge engine perf upgrade. The whole renderer was rebuilt, terrain streams better, and big scenes draw faster and smoother.
- Savi's tools are way more reliable, and she can now see and poke at your game's UI — huge unlock for UI-heavy games (Discord-style chat games, dashboards, card games, etc.).
- Multiplayer feels much smoother — camera, controls, and effects (vignettes, particles, music swells) all stay in sync now instead of glitching during rollback.
- Hundreds of fixes — actually hundreds. Screen flashes, object juice, screenshots, voxel marks, query ordering, and a long tail of small things that used to glitch now just work.
- New animation stuff! Savi can now tween any property on any object — bounce a chest open, pulse a crystal, fade things in, flash red on hit. More animation features coming soon.
›technical notes
- Added channel-based animation mixer to ObjectAPI. Declarative setup via
api.setProperty("mixer", { <channelName>: { clip, weight?, duration?, speed?, loop?, blendIn?, direction?, mask? } }). Runtime control viaapi.updateChannel(name, opts)(passnullto clear) andapi.getChannel(name)returning{ clip, weight, elapsed, duration, finished }. Channels blend by weight with optional per-bone masks ({ from: <bone> }or{ bones: [...] }). Backed by newDrawMixerECS component (replicate: AOI). - Added anime.js-style property tween API:
api.animate(targetEntityId, { keyframes, duration, easing, delay, direction, loop })andapi.isAnimating(targetEntityId, dotPath?). Keyframes target dot-paths like"feetPosition.y","material.emissive","scale". Supports scalar tween values, value arrays ([a, b, c]evenly distributed), per-segment timing ([{ value, duration, easing }]), and relative deltas ("+=5"/"-=5"). Server-authoritative via newtween-evaluatorsystem (order 105, afterbehavior-update); state stored inTweenStatecomponent (replicate: never). - Extracted shared
easing.ts(easing curves + OKLCH color interpolation) used by both behavior builtins and the tween evaluator. - Added
parseMixerChannelshared validator used by both themixerproperty setter andupdateChannelruntime call. - Added
interpolationproperty (getter/setter) backed byDrawInterpolation;{ teleportThreshold: number }ornullto disable. - Note: the previously documented
animated3DCharacter.action: { clip }shorthand never had a real implementation (the setter silently dropped the field). Use the mixer instead — declare locomotion + action as separate channels. - Camera authority split: render worker is now sole owner of yaw/pitch, integrating mouse deltas directly and publishing back to sim via a new CameraAngles SharedArrayBuffer channel. Spring-arm smoothing and physics-collision raycast moved from renderer into
camera-behavior(sim), collapsing five per-frame orbit params into a singleorbitDistthe renderer lerps toward. Removes input-lag and stale WASD movement axes. - Renderer now also publishes its final pos+quat through the camera-angles SAB; sim consumes
rendererTransforminsidebuildPointerRay()so click rays match what's on screen at >60 Hz refresh. View-state pos/rot intentionally left on sim-frame semantics so script-sidecamPos*is unchanged. Unit-norm guard rejects zero-initialized SAB state. - Compiled behavior scripts:
Math.random()now delegates to a seeded RNG when one is installed (globalThis.__tomeSeededRng), making prediction/resimulation deterministic. NewsetTomeRng/clearTomeRnghelpers intome/resources.tsinstall the RNG into both the ECS resource and the global bridge consumed bycompiler.ts's deterministic-Math injection. Wired throughinterpreter.ts,input-applier.ts, andbehavior-update.ts. - TweenState now replicates with AOI + snap correction so animation tweens stay in sync across clients during rollback and resimulation.
- ECS event component system restored (reverts the entity-based event experiment):
queueEventAdd/drainEventAdds/injectTransientre-instated on the ctrl channel. Particle bursts (ParticlesBurstEvent) drained per tick and forwarded through the SAB render channel. Juice/audio dedup logic survives rollback. - Snap-frame application bypasses the prediction entity filter for
replicate:"owner"components. Server-authoritative owner state (e.g.TomePlayerJuiceState) was previously dropped on predicted entities, breaking vignette, letterbox, music, and other effects in multiplayer. - ECS query-utils: query results are now sorted on every path (not only the index path) so behavior scripts iterating queries see a stable order frame-to-frame and across server/client.
- New client-only
DrawVisibilityOverride(replicate: never) letscamera-behaviorhide the local player without clobbering the server'sDrawVisibility. Allrender_v2states honoroverrideLayerMaskwith fallback to serverlayerMask. SkipReplicationremoved from terrain streaming. Terrain entities now replicate normally and are protected from server despawn; event components on the client are fixed.- Camera API (
tome/api/camera-api.ts)getProperty/setProperty/lookAt/setRotationread and writeFeetPosition+Rotationdirectly instead of going through the now-removedInterpTransformblob. Script-visible shape and semantics are unchanged. - Replaced legacy renderer with
render_v2: newRendererFeatureV2(engine/render_v2/feature.ts) is the sole renderer wired byengine/client/engine-bootstrap.ts. Worker-side ECS sync, camera, smoothing, and decorations all flow throughengine/render_v2/*instead of the deletedengine/render/main,engine/render/extractors,engine/render/commands, andengine/render/worker/worker-prep.tspaths. - Removed legacy render pipeline:
RendererFeature,worker-renderer.ts,worker-prep.ts,render-worker-state-adapter.ts,transform-smoother.ts, the prep/extractor/command system, and ~75k lines of supporting tests/utilities are gone. - New SAB-backed
RenderChannel(render_v2/render-channel.ts) replaces postMessage ECS sync. Adds a string table with generation-based eviction, u32 frame-headernumOps, local-buffer overflow instead of dropping ops, growable arena/string-table SABs, and per-frame perf metrics + transport instrumentation. - Renderer now consumes ops directly — legacy intent conversion deleted.
AppearanceIntentValue/LightIntentare gone; per-propertyDraw*writes (DrawModel,DrawSprite,DrawText,DrawSign,DrawMaterialOverrides,DrawAnimation,DrawVisibilityOverride, etc.) are written directly by the interpreter and replicated as their own components. IndirectBatchedMeshprimitive batching for cubes/spheres/etc., with shadow fixes: per-instance shadow override materials, follow-camera shadow frustum, cast/receive flags honored on model meshes.- Troika text vendored in-tree under
render_v2/text/vendor/*. Forces the bundled Geist Pixel atlas, short-circuitsFontResolver.resolveFallbacks, and drops the per-specdata.fontpath so the renderer worker never races on the troika unicode CDN. Outline/highlight bleeding fixed. - Screenshot capture rewired for v2:
captureScreenshotis now exposed onRendererV2Handleand threaded back to Savi'sview_game_canvas_screenshottool; fixes a freeze and reprojects the selection beam to the crosshair on the render worker. - Renderer is now sole authority for camera yaw/pitch; spring-arm collision moved to sim, rendered transform sent back to sim each frame. New
camera-smoother.ts,orientation.ts, andrenderer-camera.tsunderrender_v2/camera/. - Entity smoother batches pos/rot ingest per tick, adds per-object interpolation component and flash effects. Transform/layout-scale now applied in render_v2 model and scene state.
- ECS-driven outlines and selection visuals moved into
render_v2. Skinned models, LOD, decorations, and water materials added. Voxel terrain pipeline rewritten on render_v2. - New
sprite-node-material.ts,EnvironmentCore,FogCore,LightsCore,PostProcessingCore,RendererPipeline,RendererAnimation, andterrain-decoration-service.tsunderrender_v2/. - New
run_ui_scriptSavi tool plus general client-RPC system (cf-studio-chat DO ↔ kiln ↔ iframe). Scoped to the active user's most-recent WebSocket; rejects responses from other tabs/connections. view_game_canvas_screenshotnow works under render_v2: wiredcaptureScreenshotthrough the worker RPC, captures inside the render frame (WebGPU texture lifetime), bumped size limit (512KB→2MB) and timeout (500ms→2s). Switched final encode fromtransferToImageBitmaptoconvertToBlobso taking a screenshot no longer freezes the renderer.- Trimmed
run_scriptandrun_ui_scripttool descriptions; removed duplicated API docs and don't-lists in favor of taste/footgun notes. run_scriptresult shape collapsed to{ newVersion, return, logs? }/{ ok: false, error, logs? }; surfaces all log levels with data args.- New wisp tools
terminate_wispandlist_active_wisps(inwisp.ts, no longer monkey-patched inserver.ts); 4-char hex wisp IDs; terminated wisps render as "Wisp terminated" in the footer. - Misc Savi-side polish:
grepmerges adjacent matches;inspect_versionssummary-only mode;get_game_pulseresolves userId at invocation;str_replace_editorview shows line numbers; debug gizmo renders text tool results as pre-wrapped text; interpreter preservesTomeTerrainAnchoron spec-driven position updates. - Screen flash routed through DOM UI sink/transport (was ECS-only, broken in worker).
- Object-motion juice now restores base position/scale on effect end.
- Voxel structure mark bounds derived from template
build()output. SPAWN_AGENT.mdis now the single source of truth forCLAUDE.md/AGENTS.md; generated byscripts/generate-agent-docs.ts. StaleAGENTS/engine/{component-mixin-timing,live-reload}.mdremoved.- Rewrote terrain streaming as non-replicated, client-driven. Chunk components flipped from
replicate: "aoi"toreplicate: "never"; clients now build their own chunks from the terrain definition instead of waiting on snap frames. Removes terrain entities from the replication budget entirely. - Split the monolithic
terrain/systems.ts(~4800 LOC) intoserver-terrain-system.ts,client-terrain-system.ts,streaming.ts, andterrain-systems-shared.ts. Server runs request/ingest/streaming/rescue; client runs its own streaming + ingest + collider flush + mark-liquid + rescue. - Added voxel terrain pipeline: per-layer textures, packed layer-texture sharing across LODs, vertex colors, async texture loading, and AO. New
terrain-tile-service.ts(render_v2) is the LOD/tile authority. - LOD transitions no longer use dithered noise — replaced with stable cross-fade so seams stop crawling at distance.
- Voxel chunk builds: numeric greedy meshing, boundary cache, SharedArrayBuffer result slots for zero-copy worker→main transfer, deadline raised to 2000ms, deadline-failure recovery, and stale-voxel-lookup fix.
- Server voxel builds capped to a 3×3 chunk authority window with retry backoff so cold starts and large worlds don't stall the tick.
- Unified chunk entity ID prefixes under
terrain/stream/…; cross-prefix despawn fix prevents leaked chunk entities. - 2D top-down places now skip terrain mesh/collider work entirely (heightmap startup gate + ocean shoreline restore).
- Material rebuild thrashing eliminated; weight-texture checkerboard artifact fixed; sphere terrain positioning corrected; stale colliders invalidated with fallback anchors.
- Removed
SkipReplicationcomponent; terrain entities now protected from generic despawn paths instead. - F3 Advanced panel wired through worker-host with chunk-mesh lifecycle tracking.
- Fixed
_f32ReferenceError when terrain generator scripts use float literals. - Script-facing API (
api.getTerrainHeight,getTerrainNormal,getTerrainMaterial,getVoxelMaterial,isVoxelSolid,raycastVoxel,setVoxel,setVoxelState) is unchanged. - Internal: split monolithic
AppearanceIntentECS component into per-aspectDraw*components (DrawModel,DrawPrimitive,DrawSprite,DrawText,DrawSign,DrawMaterial,DrawVisibility,DrawInterpolation,DrawLight,DrawAnimated3DCharacter,TomeLayout). PublicObjectAPI.getProperty/setPropertykeys (visible,model,primitive,material,sprite,text,sign,layout,animated3DCharacter,light) and their value shapes are preserved — scripts read and write the same things they did before. - New
interpolationobject property:{ teleportThreshold: number } | null(orfalseto disable). Controls per-entity render-tick smoothing; setsDrawInterpolation. - New
GameSpec.assets.metadatafield (Record<string, AssetMetadata>) — engine-managed cache of CDN model bounds. NewAssetMetadatatype andpatchAssetsScriptMutationkind for engine-internal writers. - Camera API (
createCameraAPI) reads/writesFeetPosition+Rotationdirectly instead ofInterpTransform. PublicgetProperty("feetPosition" | "rotation"),setProperty,lookAt,getControlTarget, andqueryshapes unchanged. queryWorldnow returns results sorted by entity id on both the index and full-scan paths (previously only the index path sorted). Iteration order in scripts that loopapi.query()is now deterministic across the two paths.- Type re-anchoring (no runtime change, no spec-shape change):
MaterialOverridesSpecnow derives from engineDrawMaterialOverrides;SignSpecandObjectProperties.textnow derive fromAppearanceSignValue/AppearanceTextValue;ObjectProperties.modelis spelledstring | { id: string; animation?: DrawAnimationValue };ObjectProperties.spriteis spelled inline with the same fields. - Schema cleanup: removed unused exports
RectangleSplineShapeSchema(useSplineShapeSchema) andTerrainMarkSchema(useHeightmapTerrainMarkSchema). Voxel structure markboundsis now optional and auto-derived frombuild()output when a generator is supplied.