Repository navigation
Releases: lsgmasa33/modoki-engine
Release list
v0.7.3
A large release: 926 commits across 71 issues since v0.7.2. The headline is nested prefab identity, a rework of how prefab and scene files store nested edits. It is paired with a new gameplay recorder, a round of undo, Play and preview safety fixes, and a cleanup of the agent (MCP) tool surface, which includes breaking renames.
✨ Features
Nested prefabs keep their identity
Every prefab member and node now carries a stable, stored identity, and nested edits are saved on the member they belong to. Previously, edits were keyed by position in the hierarchy.
- New file formats: prefab v5 → v7 and scene v16 → v17. Existing files migrate on load. Older editors refuse a newer file rather than silently corrupting it, and the prefab writers no longer stamp a format version unconditionally.
- Moves, field overrides, added tags, added/removed nodes and removed traits on nested instances are saved per member and per node. Editing one object no longer pins its untouched siblings, and later template changes still reach everything you didn't edit.
- Apply / Revert name members by identity, so they survive renames, re-parenting and rebuilds across two template versions. Apply from a nested instance keeps values the outer row shadows. The Apply dialog and the Inspector's override list show only an instance's own edits.
- Template reference nodes (a prefab placed inside a prefab template) now save their own moves, rows and references correctly, and keep orphaned rows.
- A member whose template node is gone is kept and named, never silently dropped (orphan rows survive Refresh and rebuilds at any depth).
- Duplicating a scene, promoting an instance, and Create Prefab in the same session all preserve member identity. Previously, a member moved in a just-created prefab was lost on reload.
- A rigged re-import no longer discards the prefab fields it doesn't compute.
Gameplay recorder
- Record a take in the GameView, then render it to video. Stop opens a render-options dialog, and a non-modal card shows render progress with cancel. You can also render from the CLI with
npm run record. - Replays are deterministic: fixed-timestep capture, a pinned RNG seed, and scene loads that cost zero take time.
- Each render is checked against the take's recorded events, per event type with numeric tolerance. Boot-time and app-lifetime events are handled so a correct replay doesn't read as diverged.
- A render names any assets that changed since the take was recorded, and survives a Vite page reload during boot.
- The GameView preview now lays out at the full device size.
Audio
- A new audio-health trace reports when the music bed is lost.
- Projects can opt in to reloading the page when audio is dead (running but frozen).
- Fullscreen ads hold the engine's audio and release it safely. A refused media play is recorded instead of swallowed.
Native plugins
- AppLovin MAX (
capacitor-applovin-max) meets the engine's ad contract: UMP consent, adaptive banners, and bounded preloads. Every parked call settles, including loads during MAX's expiry reload and repeat initializes. - In-app purchases (Android): every Billing listener settles its call, so a routine service disconnect can no longer hang a purchase or block later ones. The products/entitlements race on a shared array is fixed.
- game-debug (iOS):
startServeranswers exactly once on every path, and a failed start still registers recovery.requestReviewcan't crash. - AppsFlyer: the ATT request waits for the app to be active, and an already-answered status no longer blocks ads from starting.
- Boot diagnostics: the boot timeline records what the OS was doing, and the stall log says so.
🤖 Agent / MCP tooling
scene_query→physics_queryon both servers.- One word per concept across the surface (
path/type/kind/t/time). - An aim that names more than one of entity / selector / label /
{x,y}is refused as AMBIGUOUS instead of being resolved by precedence. - Action tools (for example
play_control) reply with the fields they changed, not the whole editor state.
Improvements
- String-encoded arguments (
"12","true", a JSON object) are decoded rather than refused, including game tool calls. Out-of-range numbers are refused by the schema instead of being clamped. - Tool descriptions are about 10 KB shorter, with repeated schema prose and history removed.
get_scene_state'strait=selects carriers, and targeted reads are capped at 20, with the cap disclosed.list_actions/device_introspectreturn bare names, andname=<substr>returns param schemas.- Reading a journal no longer clears it (a
sinceCap/nextSeqbaseline replaces clear). - Device ↔ editor parity: one console reader for both,
levelas a threshold,sinceas a sequence cursor andsinceMsas the clock. There is adevice_wait_fortwin that waits on entities and console output, anddevice_duplicate_entityaccepts the same entity alias. - Native-log windows are honoured on both platforms, and refusals name the next move.
- Entity aims sample the part of the rect the surface actually draws, and 2D bounds report their draw rect. A covering game UI node is named as its entity rather than as dock chrome.
- Handle routes share the aimed-route refusal contract.
- The play op reports a refused Play instead of
ok: true.modoki_dndreports a drop that opened a modal aspendingModal. save_allnames every file in asset-root form.- Strict-schema refusals name the unknown key.
- On Android, trusted CDP touches pick the app's page by identity, so taps reach the game.
🐛 Fixes
Undo, Play and preview
- A Timeline/Animation preview session and Play never coexist. Play, Stop and scene loads end the preview through one path, and they share one authored snapshot and restore.
- A preview puts back the audio mix, PlayerPrefs and quality tier its actions changed.
- A scrub, a Play press, or undo/redo during Stop's restore is refused until the restore lands. Leaving a preview cancels a session begin that is still pending.
- Every writer of the live world asks one question, "is the world authored?", so save-all during a restore can't write the posed state.
- Clip undo no longer opens a preview with no panel to exit it, and record mode bakes every recorded value, not just the first.
- Pausing the Animation ▶ freezes playback instead of leaving it advancing.
- Undo of an Apply made in the prefab editor, or in an untitled scene, now rebuilds the live instances, not just the prefab file.
- Opening a scene or prefab waits for an undo in flight, and the unsaved-changes prompt reads the post-undo state.
- A refused or non-reverting toolbar Play/Stop shows a warning toast with the reason.
Editor
- SceneView gizmo: aiming, picking and dragging use one camera and draw area, and axis handles are hit where they are drawn.
- Save As no longer pairs an
/assets/copy with an/@fs/source. JSON asset routes never overwrite an asset of a different kind. - The save dialog, the Inspector's Apply/Revert, and the hot-reload banner buttons are properly named (accessibility and automation).
- Quitting the editor commits renderer storage first, so the remembered layout and scene survive a stop.
editor:stopno longer kills the quit sequence with a double SIGTERM.
Build and dev server
- The editor no longer boots a game degraded after a dependency changes: each project's lockfile now keys Vite's dep-optimizer cache.
- A game's packages are pre-bundled at startup (cold dev boot no longer reloads mid-boot), and an inert packaged pre-bundle entry that warned on every boot is gone.
build.debugBuildoff removescapacitor-game-debugfrom the native build entirely, and re-enabling it restores it where it was.- A freshly scaffolded project builds, and the starter template is typechecked on every run.
Windows
- Open / Reveal in Explorer works with
&, spaces and paths the shell can't open. It no longer runs part of a file name as a command, opens Documents, hangs on a modal, or reports failure on success. - Build steps no longer go through a shell (argv everywhere,
.cmd/.batrun with escaping), so%VAR%in a path no longer expands.
⚡ Performance
- pixi.js 8.20.1 → 8.21.0, which carries the upstream fixes for two known memory leaks. The engine's
unload()workaround is removed. - Prefab identity resolution builds its world-wide lookup once per resolver instead of per query.
Full Changelog: v0.7.2...v0.7.3
v0.7.2
Six days since v0.7.1: 1,854 commits, 1,060 of them in the engine and editor.
This release has two themes. The first is nested prefabs that survive a save. Moving, deleting, duplicating and applying members inside nested prefab instances now round-trips through save and reload. Before, those edits could come back, vanish, or leave two entities sharing one GUID. The second is not losing work quietly. Every action that replaces the open world now asks about unsaved edits. A failed asset load is classified and remembered instead of being refetched every frame.
Dependencies
- three.js 0.184.0 → 0.185.1. The r185 iOS first-launch black screen is fixed: every async shader compile on a renderer now takes the same lock. r186 was tried and rolled back because its GTAO cannot compile under MSAA. The peer range is now
>=0.183.0 <0.186.0. - PixiJS, Rapier and Electron are unchanged.
- Pinned asset toolchain:
npm run toolchain:install -- toktx msdf-atlas-genprovisions pinned builds of toktx, ktx and msdf-atlas-gen. Asset conversion no longer uses whatever copy is on PATH, and ffmpeg/ffprobe are pinned the same way.
⚠️ Upgrading from 0.7.1
- Every entity has a GUID, including ones spawned without
EntityAttributes. Older scene entries get a stable derived GUID, so the first save may rewrite older scene files once. - Scene format v15. Scenes now save the structure inside nested prefab instances (v14) and prefab member moves (v15).
- New scenes are
<name>.scene.jsonat every entry point, not<name>.json. - Unsaved work is guarded everywhere. Opening a scene from the Assets panel now asks Save / Discard / Cancel, and so does every other action that replaces the world.
modoki_prefabedit-exit refuses while prefab edits are unsaved. - Reparenting across scenes asks first. Dropping or pasting an entity under a parent in another scene is a prompted scene move. The agent
reparent_entityneedsmoveToScene: true,apply-scene-opsrefuses a cross-sceneparentIdwrite, and a prefab member cannot be split out of its instance. - Resource entities are not tree nodes. Parenting, creating under, pasting under or duplicating a resource entity is refused.
loadSceneno longer rejects when a scene manager fails to start after the swap. The world is replaced and the failure is reported instartupErrors.entityRef()returns null for a dead handle.OnZone/OnCollisionexit callbacks gainselfRef/otherReffor the entity that just left.- Agent UI dispatch refuses unreachable controls. It refuses when no control bound to the action is on screen, or when every one is covered by a modal.
- Prefab link edits: a generic trait edit can no longer add, remove or write
PrefabInstance. Use Detach Prefab to cut a link. Nesting a prefab inside an instance of itself is refused. - Create and Save As ask before replacing an existing file. Replace keeps the file's GUID, and an agent create refuses an existing asset.
- MCP changes:
- the
open_*_editortools takedisplayName; input_watchtakesmaxPresses;- list replies use
returnedCount/totalCount, andentityCountis gone; - replies name entities by GUID, and the journal no longer aliases numeric ids: a despawn carries the same ref as its spawn;
- an entity aim takes exactly one of
guid,nameorid, and{id}works only for an entity without a GUID; - an unknown key in a create-entity spec is refused instead of ignored;
write_assetrefuses a path that does not exist, andwrite_asset_metarefuses when the asset is missing;select_sprite_slice,set_collider_editandset_gizmovalidate their input, editor-bound ops refuse when their editor is closed, anddiscard_asset_editsrefuses a path with nothing pending.
- the
- iOS text interaction is off by default in the web view, which removes the double-tap magnifier. It turns back on while a text field has focus.
audioCache.durationSecandvideoCache.durationSecare no longer committed to sidecars. They are measured on each machine.
✨ New
- Prefab member moves:
- a member moved inside its own instance keeps its identity and is saved;
- moves show in the Apply dialog under a new Moved category, and Apply re-parents the row in the prefab;
- Apply fixes member references in every file that uses the prefab, and undo puts those files back.
- Nested prefabs you drag stay linked:
- a prefab instance dropped inside another instance keeps its link and becomes a nested instance;
- an owned nested prefab dragged out stays linked to its own prefab.
- New
capacitor-modoki-systemplugin:openAppSettings;requestReview, which asks the OS for its rate-this-app prompt;system.openUrl, which opens an authored https link in the system browser;copyToClipboard, which also works on the web.
- Ads and attribution helpers are engine modules now: the ad lifecycle, interstitial pacing (with a No Ads gate), the pre-ad No Ads offer and mid-level break flow, and the AppsFlyer attribution lifecycle.
- Support ID:
supportIdgives the account uid when the player is signed in, and the app-instance ID otherwise. A ready-made Player ID row view uses it. AudioSource.shuffleStartstarts a shuffled playlist on a random clip.UIElement.imageAlignchooses which edge of a cropped image or video backdrop stays in view.NPRPostFX.emissivePassthrough: emissive surfaces glow and bloom under NPR instead of turning dark grey. It defaults to 1; set it to 0 for the old look.- Game time waits while the loading overlay is up, and the overlay stays up while a shader compile is holding frames, so a cutscene doesn't start behind it.
- Android notification icon: set
app.notificationIconSourceand the build generates the small icon at every density, so reminders no longer show the generic "i". - Multitasking device presets in the Game view: Split Screen 360×380, Window 700×450 and Window 1024×560.
- Inspector GUID in the entity header: it shows whether the GUID is durable, runtime-only (gone on reload) or unsaved. Click to copy it.
- New engine APIs:
requestPrefab, one retry-safe way to spawn prefabs at runtime;worldScoped, module state keyed by the world;EntityTable,packedOfandisPackedAlive, per-entity storage that notices a recycled entity id;parseAssetJsonand the load-failure types, so a game's own loader can classify its failures.
- Modal dialogs block the editor underneath, so shortcuts like Delete and ⌘Z no longer fire behind them.
- iOS saves stay out of iCloud and Finder backups.
🐛 Fixes
- Prefabs & nested instances:
- deleted or moved members inside an owned nested instance no longer come back on reload;
- two instances no longer share one GUID: a second nested instance at a row is no longer mistaken for the row's own, and a node added in a nested row gets a fresh GUID per instance;
- a user-added nested instance is no longer dropped on save;
- a deleted row is no longer brought back;
- references between a prefab's own members point inside each instance, not at the source entity;
- references to nodes added inside a prefab template survive save and reload, and duplicating an instance keeps them working;
- removing a prefab member in a scene takes its nested instance's members with it instead of leaving orphans;
- Apply to Prefab and prefab refresh keep a scene's nested edits and override changes, and don't duplicate added nodes;
- refreshing, reverting or applying a prefab instance that lives in a base scene keeps it in that scene;
- Create Prefab no longer flattens a nested instance when its cache is cold;
- Create Prefab no longer writes nested instances twice, and it leaves out transient runtime entities and reports what it left out;
- undoing Create Prefab after Play→Stop keeps a nested instance's link;
- Detach Prefab, unpack and deleting an owner no longer make moved members vanish on reload;
- dropping an instance under its own member no longer makes it vanish;
- a child parented to a prefab instance in an older scene no longer lands on an unrelated entity;
- saving no longer writes a live entity id into added prefab nodes;
- editing a pooled prefab no longer blanks its
UIEntriesview.
- Scenes & saving:
- duplicating a scene file gives it its own entity GUIDs, and references to prefab members follow the copy;
- duplicating or pasting an entity subtree re-points references inside the copy;
- agent Save As writes a real copy with a fresh scene ID;
- a base scene kept across a level swap keeps its unsaved state, prefab link and markers;
- after
discardUnsaved, undo no longer brings back discarded entities; - a scene manager that fails to start no longer half-unloads the scene.
- Editor:
- after an entity is destroyed and its id reused, the SceneView, Hierarchy, Animator and Director follow the right entity: no stale gizmos, collapse state or selection;
- clicking a label drawn over a sprite in the SceneView selects the label;
- Inspector number and name fields no longer rewrite themselves or drop characters when the window lacks focus;
- the Inspector shows a value not yet measured on this machine as missing, with a Re-import hint, instead of 0;
- Assets panel: the sprite filter lists sprites, a slice-name search finds its slice, and F2 and folder drag work on texture rows;
- moving or renaming a texture that is open in the Sprite or 9-slice editor keeps its unsaved edits, and a save that finishes after a texture swap no longer closes the new texture;
- Save and Browse dialogs are sheets of the editor window, so paste works and the editor doesn't freeze;
- Skin editor: a slow operation no longer lands on a different rig;
- a re-imported model, skinned GLB,
.mesh.json, audio clip or font ref...
v0.7.1
Six days since v0.7.0: 770 commits that touch the engine, the editor or the demos, out of 2,259 in total.
This release has two themes. The first is identity that outlives its owner. koota reuses entity ids, so a respawned entity could pick up a destroyed entity's particles, audio voice, UI state, screen bounds or 3D pick target. Every per-entity cache was moved onto a single shape that checks the entity is still alive. The second is agent tooling that fails loudly. The MCP tools stopped turning an unknown input into a plausible action, and stopped reporting a refusal as a success.
Dependencies
- three.js stays at 0.184.0. r185 black-screens iOS on the first launch after a clean install. The package peer range is now
>=0.183.0 <0.185.0, and a test enforces it. - PixiJS and Rapier are unchanged for this release.
- Security: all 10 open Dependabot alerts are cleared (sharp, js-yaml, vitest, and hono in both MCP servers).
⚠️ Upgrading from 0.7.0
GameDefinition.UIComponentis removed. The default game UI layer always mounts.- The
capacitor-litert-lmplugin is removed, together with@mediapipe/tasks-genai. The packaged editor's CSP no longer allows remote origins inscript-src/worker-src. - OTA publishing now deletes old bundle versions beyond
ota.retainVersions(default 5). It always keeps the live version and anything under an hour old. Publishing also needs a git checkout and refuses a dirty tree. - UI actions: a param that arrives as
''is now treated as absent. Built-in engine actions report a refusal asok:falseinstead ofdispatched:true. - Scene load waits for Rapier and for sprite-animation, rig, animset and particle definitions, so the first frame is posed and has physics. The cost: loading takes longer in scenes that use them (about 130–175 ms more for physics on a Galaxy A23).
- Build failures: a failed icon or splash generation now fails the build instead of shipping stale art.
- Listener errors: an exception thrown by an engine listener is caught and reported instead of propagating.
- Crashlytics:
journalErrornow reports to Crashlytics in every build. findEntityis typed (Entity | null), so code that leaned on it beinganymay need type fixes.- Packaged editor profile: the profile moved to a per-install, per-project folder. Editor settings such as the CDP opt-out, ports and tokens are carried over.
- Updates: "Check for Updates" now asks before downloading.
✨ New
- Reserved edge bands: mark a strip such as an ad banner with
UIAnchor.reservesEdge. A dialog root markedclearsReservedEdgesthen stays clear of it and of the safe area. - Adaptive design height on
Canvas2D(maxReferenceHeight, opt-in): tall phones use the extra vertical space instead of letterboxing. UIElement.minTapSizeenlarges a control's tap zone without moving its artwork. Real interactive content underneath still wins.- GTAO cost controls:
AmbientOcclusionPostFX.resolutionScaleandsamplesare authorable. On a Galaxy S22, scale 0.5 took postfx-demo from 28 to 60 fps. - UI overflow warning: in editor and debug builds, text that paints outside its box is reported. It appears as a
[UIOverflow]console warning, in the journal and indiagnose. refuseAction(): a UIAction handler can refuse, anddispatch_actionreports it.- New
engine.directoraction: play, pause, seek and change the speed of a cutscene from UI bindings. - OTA:
- publish a shell's sub-games from the editor (Project Settings → OTA → Sub-games) or from MCP;
- each manifest records the commit that built it;
- a corrupt embedded manifest is reported instead of being treated as missing.
- Playable ads:
asset-keep.jsontakes per-targetplayablekeep/drop rules;- the end card is the call to action;
- the time cap starts when the ad is viewable.
- New engine APIs:
measureText2D- the
ScreenBandlayout solver resolveCanvas2DHostensurePhysicsReadyentryIndexOf- testing helpers:
tapTargetFloor,inOrder,scratchDir
- Early monetisation modules, promoted from a shipping game and still evolving:
- the store shelf (
ShelfSession) - a daily calendar
- a login bonus
- the
capacitor-applovin-maxplugin
- the store shelf (
- Demos: all six demos have an app icon.
🐛 Fixes
- 2D text was invisible on every iOS 16 device. The MTSDF atlas was decoded premultiplied. Verified on an iPhone 8.
- Recycled entity ids: a respawned entity no longer inherits a destroyed entity's state. This covered particles, audio voices, skinned clones, text animation, UI DOM and hooks, prefab override marks, physics controls, video decoders, timeline registries, screen bounds and 3D picking.
- Physics from the first tick: Play and step used to run their first ticks with no 2D physics while Rapier's WASM was still loading.
- No pre-pose flash: the first frame after a scene load is posed.
- 2D textures:
- the "destroyed while still bound" WebGPU errors are gone;
- textures survive a scene swap instead of being destroyed and decoded again;
- a wedged 2D canvas recovers;
- a re-imported sprite, font, model or audio file shows its new bytes in the editor.
- Scene swaps survive a throwing listener, instead of aborting halfway.
- Packaged editor:
- "Check for Updates" always answers, including when an update exists;
- "Restart Now" no longer races the macOS updater;
- a startup failure exits (and leaves a log) instead of hanging;
- main-process dialogs no longer freeze the app.
- Preview and undo safety:
- saving inside a preview no longer loses one of its writes;
- undo, Play and scene edits are refused or scoped correctly during scrubbing and preview restores;
- a scene or prefab reload that arrives during Play is deferred to Stop instead of being dropped;
- editing a
.prefab.jsonoutside the editor now reloads it.
- Scene view:
- the UI preview picks the 2D entity under the cursor;
- 2D gizmo, group and vertex drags released outside the canvas commit;
- a lost graphics context recovers instead of leaving the panel black;
- collider-only view mode announces itself, and the Type filter badge names the active types.
- Editor panels:
- Sprite and Nine-Slice editors: resize follows the pointer and no longer collapses a slice.
- Physics Layers: the matrix stays symmetric after adding a layer.
- Undo history: undo labels name the entity.
- Assets and Hierarchy: re-selecting reveals the row again.
- Running game: it no longer steals presses on editor panels.
- Skin weight painting and 2D edits reach the Game view while stopped.
.meta.json: write failures are shown instead of swallowed.- Asset move/rename: reference repair no longer skips when a runtime tab is open.
- UI runtime:
- a single-axis scroll view no longer traps swipes on the other axis;
- a quick double page step moves two pages;
- pooled rows read nested prefab roots correctly, and refuse a vw/vh-sized root instead of guessing.
- Timeline:
- a seek while paused re-poses the scene;
- seeking onto the end fires the end event once;
- a slaved sub-director refuses transport commands instead of pretending;
- an un-slaved sub-director restarts correctly.
- Data robustness:
- keys such as
constructoror__proto__in scene or config data no longer crash anything or pollute objects; audio.setBusVolumechecks the bus name before writing.
- keys such as
- Builds:
- the CLI native build generates icons from
project.config.json; - an empty
iconSourcefalls back to the bundled icon; - the iOS heal strips an unused Facebook SDK from Firebase Auth;
- opening a project waits for a build that holds it.
- the CLI native build generates icons from
- Crash reporting: on iOS, JavaScript errors reach Crashlytics with their message, and each report gets its own grouping.
- Toolchain: removing tools refuses a junctioned folder instead of reporting it removed, and works after a toolchain folder rename.
- Demos: forest-camp's D-pad buttons no longer overlap on notched phones, and its progress bars are the right size.
⚡ Performance
- Boot: the shader prewarm skips work that does nothing under a post-FX stack, cutting its main-thread stall by a third on a Galaxy A23 (289 → 193 ms).
- GTAO: see the cost controls under New.
- Level reloads: a scene swap no longer destroys and re-decodes 2D textures.
🤖 Claude Code / MCP
- New
modoki_wait_for: waits until an editor, entity, console or chrome condition holds, instead of a fixed sleep. It also works as amodoki_batchstep. This is the only new tool; none were renamed or removed. - Editor chrome is readable and aimable by label:
modoki_handles {editor:'chrome'}reads live inputs, checkboxes, tabs and mixed multi-select state;- tap, drag, pointer, hover, scroll and focus accept a
labelaim.
- iOS over USB:
device_connect {useUsb:true}, through go-ios. - Errors say what happened:
- a refusal carries its real error code, not a generic
REFUSED_BY_OPor a timeout that looks like a dead editor; - an unknown value is refused with the valid options. This covers mouse buttons, pointer actions, modifiers, key names and journal levels;
modoki_evalreports op refusals correctly.
- a refusal carries its real error code, not a generic
- Quieter correctness:
modoki_type_textcan type a newline;modoki_dndreports asset-editor drops that did land;modoki.importloads the app's own module instance;get_scene_statereportsguid:nullfor guid-less entities, and a new scene's starter entities get real GUIDs;scene-mutateandasset-writerefuse when they would destroy unsaved editor changes.
- Device leases: ending a lease always stops WebDriverAgent, and one holder can no longer release another's phone.
Full Changelog: https://github.co...
v0.7.0
Ten days since v0.6.0 — 538 feat/fix/perf commits in what this snapshot ships, out of 986 touching
the published tree.
This is a durability release, not a feature release. Two classes dominate it.
The first is state outliving the thing it belonged to: a session torn down but still writing, a
scene load that lost a race and overwrote the winner anyway, a guard that survived the operation it
was guarding. The second is an authored value that reads back as set and does nothing — a field
the Inspector shows, the scene stores, and no code ever reads. Both are quiet by nature: nothing
throws, the wrong value simply persists or the right one silently fails to apply. Most were found by
sweeping for the shape rather than from a report.
Dependencies
- three.js stays at 0.184.0. The 0.185.1 bump was made during this cycle and then reverted: it
black-screens every iOS device on the first launch after a clean install, on both the WebGPU and
the WebGL2 paths. Android is unaffected. The pin is exact so it cannot drift back. - PixiJS 8.19.0 → 8.20.1
- Rapier stays pinned at 0.19.3. 0.20.0 was bumped and reverted: it panics when you remove a
collider something is standing on. Root-caused upstream as a CCD cache bug, so the pin is
deliberate and documented rather than drift.
New capabilities
- 2D masking — a
Mask2Dtrait that genuinely clips a PixiJS subtree. - Multi-touch gestures — a pan / pinch / tap layer, scoped to the game's own DOM surface.
UIElement.autoFitText— shrink-only font fitting, converged by measurement.- A global input lock, gated on the action completing rather than on a timer.
- A container can swallow a tap without having to pretend to be a button.
- Three Android tablet presets in the editor.
- PixiJS geometry bytes and geometry churn are now counted in the profiler.
Authored values that did nothing
The engine's own rule is that an unwired field is a lie with a tooltip. This release went looking for
the lies:
- Four
UIElementfields read alive and applied nothing;textAligndid nothing on the path most
text takes;zIndexwas shadowed with no validator mention. UIAnchordiscarded all four authored margins in silence.- Six of the 14 fields a pooled row pins were overwritten without a word.
- An entry prefab root's
%size was pinned aspx— the unit was read and dropped. - Spacing faked with whitespace, which the DOM collapses.
Alongside it, a long correctness pass on the layout denominator itself: percentage sizing under
uiScale, safe-area percentages against a root with no layout box, rotated scenes, and
AutoFitText's fit denominator mixing screen px with layout px (up to 67% wrong).
Lifecycle and liveness
Sessions, scenes and realms now let go of their state when they end.
- A torn-down IAP session stops listening, and can no longer write entitlements into the next
game. A parkedpurchase()is bounded, released on a webview reload, and settled through one
unpark(). - A superseded scene load no longer overwrites the winner's path, undo stack and journal. A load
racingunloadAllcannot claim a success it did not get, and a post-swap tail cannot leave
activeScenePathpointing at a torn-down scene. - Navigation records history at the world swap, not in the continuation — a failed scene swap
used to corrupt the back stack. - A realm-shutdown registry means native listeners die with the realm.
- One
withTimeoutwith the abandoned operation's disposition required, replacing five defects
that shared one shape: a timeout that abandons without cancelling. - Roughly a dozen unguarded deferrals across the runtime and editor, one of them silent data loss.
- Video once-guards no longer outlive the playback they guarded.
Resource leaks
A sustained sweep, mostly GPU-side, and several were leaks where the release path existed but
nothing reached it:
- A compiled
WebGLProgramleaked on every text rebuild. - The environment PMREM leaked a whole generator per scene swap, with no reachable
dispose. - Six further GPU/DOM release defects, plus a guard to catch the next one.
- GPU-loss detection lived in one global slot, so a second viewport disarmed the first; every
renderer construction site now wires it. - A rebuilt 2D renderer replayed the dead one's instructions.
- The engine survives a jetsammed WebKit GPU process and instruments what killed it.
Refusing rather than overwriting
Formats that could not be read used to be overwritten in place. Now they refuse:
- A too-new scene refuses, and the guard moved ahead of the mutators.
- A corrupt
.mesh.jsonno longer mints a fresh GUID over itself; particle and atlas assets refuse
instead of overwriting what they could not parse. - The verdict is split from the disposition — the previous behaviour destroyed the asset, not just
its unreadable bytes. - A v3 scene no longer skips its first three migration rungs.
Two version fields were being written and read by nothing — the asset sidecar's (written at 14
sites) and PlayerPrefs' save-format version. Both are now load-bearing or gone. Unknown keys
survive normalization one level down, and asset writes keep their trailing newline.
Failures that used to be silent
- A play request the browser refused is reported as
@video.blocked. - A rejected
PlayerPrefsDELETE is no longer invisible; a failed flush is re-queued and retried
on the pause edge. - A refused
.meta.jsonpark tells the human, not only the console. - A missing asset file stops reporting itself as corrupt JSON.
- Store failures are named rather than swallowed.
- Ten "warn-once" sets that had degraded into warn-never.
Editor
The Inspector's asset views were autosaving to disk on every keystroke; those writes are now
parked, with the compare-and-swap moved onto the parked write. The Scene inspector's baseScene
field and the atlas view had the same defect. An older editor no longer deletes a config section a
newer branch added, and a panel crash message is selectable.
iOS and safe area
safeArea became a push signal rather than a read-triggered poll — its forced layout had been
stalling the iPhone 8's touch-scroll compositor. The audio gesture-unlock no longer disarms itself,
which had silenced the game until relaunch after an interruption. Reload-on-resume after a long
background.
OTA
Anti-rollback via a monotonic release sequence. A bundle's manifest is chained into the signed
release, so a signed release commits to bundle contents rather than a version pointer. Mandatory
updates are sticky and clearing one is a deliberate act. Superseded version folders are reclaimed
without one bundle deleting another's. The publish CLI now enforces the identity guards that
previously existed only in the editor route.
Build and release
Release snapshots are cut from a dedicated release_X_Y_Z branch and the publisher refuses to
publish from anything else — the tree that ships is now the tree that was tested. One
canonicalPath/samePath replaces the hand-rolled path-identity recipes that had drifted apart.
The native build fails when node_modules holds a previous tarball's bytes.
Work under games/ is not part of the published engine and is not described here.
Full Changelog: v0.6.0...v0.7.0
v0.6.0
✨ Features
- A real release build path on both platforms — signed AAB on Android and an iOS
archive/export flow, plus authored splash and icon variants and a web boot splash (the Android
splash path was previously silently falling back to the system splash — now actually drawn). - A native-config heal — auto build number derived from git commit count, an appId/appName heal
across native project files, and adisabledIfaffordance in Project Settings for fields that
don't apply to the current platform. - Editor: Project Settings split into tabs with an editable quality-tier matrix; an inline help (i)
affordance with image previews and drop-to-set on path/asset fields; several view/session toggles
now survive a reload; the Skin editor's weight-paint brush no longer tunnels on a fast stroke;
SceneView UI-vs-2D click routing is now reconciled by real paint order instead of layer heuristics;
a 2DParticleEmitterentity gets a SceneView selection gizmo. - New authorable
UIElementfields:scale, andscrollbarStyle(auto/tinted/hidden) with
scrollbarThumbColor/scrollbarTrackColorfor theming a scroll view's bar. AudioSource.playlist+crossfadeSecwalks a clip bank with crossfading.- MCP/agent tooling: six new tools —
modoki_animation_view_mode,modoki_game_view_device(s),
modoki_select_sprite_slice,modoki_open_skin_editor,modoki_set_skin_mode.
🐛 Fixes
- A debug time-scale of 0 could previously ship with a scene frozen — fixed so pausing in the editor
can no longer leak into a build. - A scroll view's live viewport measurement, and a UI scroll request, were each being written onto
an authored data field instead of a runtime-only one — both split out so authoring and runtime
state stop fighting each other. - A prefab's
versionfield is now the FORMAT version rather than a function of its content — a
content-derived bump had been making some prefabs load empty. - The quality-tier matrix's
textureMaxSizecap was authored in Project Settings but the build
never actually emitted it, and tier fields rejected decimal values. - Electron:
PlayerPrefsnow forces its localStorage commit instead of waiting for a clean
shutdown, so a hard quit no longer loses player data. - Six asset-loader caches (animation clips, anim sets, particles, 2D rigs, sprite anims, sprite
materials) were silently returning nothing for a guid missing from the manifest with no warning;
a related idle-render gate wasn't re-checking refs after an asset delete/re-import. A generated MCP
tool schema also emitted a bare unresolved$reffor a schema reused across sibling params. - Zone2D enter/exit could miscount or misattribute events when a despawn+respawn reused the same
entity index. - Particles: a fresh GPU particle pool could be drawn before its init compute landed, and a fresh
emitter could render before its texture was ready — both now gate on the real readiness signal
instead of a frame count; a one-shot warning on a sub-pixel 2D effect was false-positiving on a
real textured effect. - An
AudioSourceplaylist could go silent for the rest of a session if a crossfade window was
missed — a finished clip now always advances. - The loading overlay now hides on a real render-readiness signal instead of a frame count, fixing a
permanent black-screen race and a depth-of-field pass that could blow up its light cap. - A recycled entity index could inherit a dead entity's leftover video decoder; a
VideoTexture's
lifetime now follows the element that owns it, not the material. - A light-masked video screen was silently ignoring its mask; engine material "extras" now survive
every clone route. - Rendering: a light-mask selection now has hysteresis so a near-tie stops flapping; a post-FX
stack's stage quads are precompiled so a wrapped render-to-texture pass becomes compilable. - iOS: a native
wifiIPv4read could crash on a NULL address; the native-config heal now rewrites a
SceneDelegatethat bypasses the app's own view controller (which had been silently killing the
on-device debug bridge). - Hardened an editor backend route against a path-traversal gap in Project Settings path handling.
- A pooled prefab that never finished caching now warns instead of retrying forever.
- Electron: CDP status now reports the state Chromium actually observed, not the env var that
requested it. - Install/build robustness: plugin dists are now stamped at postinstall so nothing rebuilds mid-run;
ensureProjectDepsheals a stale workspace symlink, not just missingnode_modules; the Windows
installer no longer grants an unnecessary.vite-tempACL; the vendored-plugin freshness guard now
opens the tarball instead of trusting its name; a newnpm run test:nativescript runs the
iOS/Android plugin replays.
Full Changelog: v0.5.2...v0.6.0
v0.5.2
Twelve days and roughly 1,000 commits since v0.4.1. The headline is that Modoki now adapts to the device it runs on — a measured quality-tier system, a real scroll surface for UI, and in-app purchases that survive a crash mid-transaction — alongside a serious pass on live asset re-import correctness and several data-loss bugs in the editor.
⚠️ Breaking changes
- MCP: the
forceparam onload_scene/new_scene/ prefab edit-open is renameddiscardUnsaved— the old name is refused at the tool boundary (a raw wire call /modoki_evalstill honours it). Thesaveparam is removed from 13 tools; passing it under a strict schema is now refused. UIElement.fontFamilynow holds a font-asset GUID rather than a CSS family name — a literal family name in that field stops resolving.build.modules.nprand.gpuParticlesare removed from Engine Modules — no source file ever branched on either, so they did nothing when toggled off; the rows disappear from the panel.- A prefab instance's added children are now written compacted — a one-time on-disk diff the next time each project saves.
- The app root is now full-bleed and safe-area insets are applied per-element rather than to the whole root — a layout that relied on the old root-level inset will shift.
✨ Features
Quality tiers — the engine measures the phone and degrades itself. A device is classified by what its GPU is, refined by a boot-time probe that paces itself against the GPU rather than the display, and the verdict sharpens across launches instead of trusting one pass. Tiers now reach the 2D layer, the frame cap, and texture resolution — a low-end phone stops downloading flagship-resolution textures over the wire — and each tier's content is authored per project rather than baked into a code table. Measured on real hardware: a mid-range Android went from 9.24 ms to 3.80 ms of GPU time once the tier's light caps stopped being decorative.
Scrollable UI, and UI layout in general. UIScrollView — the engine previously had no scroll surface at all — plus UIEntries for recycled lists (5,000 entries across 9 pooled entities), scrollToEntry in entry units, wheel and scrollbar support, and a UIToggle control. GroupAlpha fades a whole 2D subtree, which a flat PixiJS tree could not do. Alongside the scroll work: UIElement.rotation, fontSize/letterSpacing now take a real unit (so text scales responsively instead of being fixed-pixel), and a container element can carry its own font again.
Animation — Free Smooth tangents. A new curve-tangent mode where a hand-dragged tangent survives a re-key and stays mirrored, in both the runtime evaluator and the Curves editor view.
In-app purchases. A first-party store bridge (capacitor-modoki-iap) that never auto-finishes a transaction, a crash-safe state machine, and diagnosable failures. Verified against the real App Store and Play sandboxes on device.
App versioning. app.version / app.buildNumber are now managed fields in Project Settings, healed into versionName/versionCode (Android) and MARKETING_VERSION/CURRENT_PROJECT_VERSION (iOS) with a never-lower guard — previously nothing in the engine managed a version and every project shipped the scaffolder's versionCode 1, which both stores silently refuse to accept twice.
Audio. A new AudioSettings resource trait with an Inspector-editable sfxVoiceLimit: one-shot sound effects are now voice-capped and stolen oldest-first with a short fade (music, entity-owned sources, and the UI bus are exempt) — previously an unbounded pile of one-shots would sum and hard-clip. Journal events fire on every voice transition, so audio is now something a test or QA case can assert on.
Touch input. The engine had no way to walk on a phone; it does now, via a new TouchControl trait wired through the UI layer.
Agent/game tooling. registerAgentTool lets a game register its own strictly-schema'd tools onto the MCP surface, reachable on a connected phone via device_game_tools/device_game_tool_call. The core agent surface grew by six tools (validate_prefab, unused_assets, write_asset_meta, duplicate_asset, move_asset, create_folder), plus PlayerPrefs read/write, all six physics scene queries, and clip posing. New device-side crash/log triage: device_crash_reports (iOS .ips crash + jetsam) and device_native_logs reading the device's own syslog rather than only the app's.
Editor. Device presets carry safe-area insets, so the preview stops lying about a notch. Asset panels park their writes and Cmd+S is the save — including inside a preview envelope, which now exits, saves and resumes. Find References inverts the tree-shaker's walk to show what points at an asset. The default layout is the owner's arrangement. iOS devices on iOS ≤16 can now be deployed to directly from the Build menu, without the previous manual Xcode ⌘R handoff.
Profiling and crash reporting. A boot-phase timeline, a desktop pipeline probe, a prewarm that reports what it could not resolve, and crash shapes JS cannot reach — ANR, uncaught Java, a real signal crash — plus a JS-reachable globalErrors capture path for a shipped build, and iOS crash reports are now correctly symbolicated (the dSYM upload phase was armed in the wrong build configuration).
🐛 Fixes
Live asset re-import — a GPU use-after-free class, and a silently-stale class. The most consequential fix area this release. Re-importing a texture, material, or HDR environment while a live material still referenced it could destroy the GPU resource out from under the renderer ("Destroyed texture … used in a submit", reported twice; measured 4 frames drawing a disposed material) — material clones (tint/prop-override/light-mask variants) and even one SceneView panel unmounting could trigger it process-wide. Separately, several re-imports silently did nothing: a standalone texture re-import had no effect, a re-imported font kept drawing its old MSDF atlas until reload and its DOM FontFace never updated (Text2D and DOM text disagreeing on screen), UIElement.imageSrc kept the stale URL, and a project's model postprocessor never ran on any editor re-import at all. A shared environment's showAsBackground toggle could also never be turned back off.
Physics: a collision callback read the PREVIOUS frame's transform and velocity. Any contact handler inspecting position or velocity during a collision — bounce, damage, knockback — was computing against stale data, in both the 2D and 3D physics systems.
Editor data loss. The postprocessor dropdown could destroy a model's GUID while a failed .meta.json write was reported as saved; the 9-slice editor's Save could race the Inspector's re-read and lose an edit on a backdrop click; a failed save elsewhere used to close its dialog and drop the edit anyway; snapshotEntity dropped every field a custom Inspector section owned, so undo could silently blank them; and the sanctioned way to stop this clone's editor could force-kill a healthy one and drop its prefs. Also: 12 undo/redo sites treated a failed filesystem write as success (a prefab created from an entity could look fine all session and vanish on the next scene load), a throwing undo closure could strand an action on neither the undo nor redo stack, and an asset undo could "restore" nothing while the real files sat in the OS trash. normalizeAnimationClip/Timeline/SpriteAnim could erase an unnamed field on the next Cmd+S. A failed model-import write could still register the asset, leaving a GUID pointing at a file that doesn't exist. The scale gizmo could compound uncontrollably on a mesh-less entity (a light's scale measured 1 → 8,959 → 87,721 → 858,883 across three drags), with undo restoring the damage.
PlayerPrefs.flush() did not actually fsync — a documented durability guarantee was false; corrected in both the runtime and its docs.
Every game's boot ran twice — a GameShell effect depended on state it itself set, so system/service registration, attribution init, PlayerPrefs.init, and scene load all ran a second time. Idempotent almost everywhere, but a native SDK could show two consent prompts on one launch.
A scale of exactly 0 anywhere in the parent chain destroyed rotation too, not just size — a consequence of how three.js decomposes a singular matrix, now worked around; the same corrected transform reaches physics, audio, and game systems.
Rendering: the 2D canvas pool could destroy the GPU context behind a still-visible canvas (a live view going blank on reuse) and leaked a context on every destroy; restoring a shared video material's texture map to null could kill the whole 3D renderer, not just that material. The boot-time shader prewarm previously armed zero shadow-casting objects, compiling a materially different pipeline than the first real frame needs — fixed, and measured on a Galaxy A23: worst-case first-frame stall dropped from ~1,500 ms to ~550 ms.
Crash reporting: the client-side rate limiter could charge its burst window before deduping, so a warn flood could silence every crash report for the rest of the session.
2D rig editing: deleting a bone could rewrite every other bone's weights; an asset's authored texture type never reached the runtime, so per-texture import settings were silently ignored.
Native/build: building from a packaged editor no longer breaks the editor's own code signature (it was writing Vite's temp config, a scoped tsconfig, and backend state inside the signed application — packaged state now lives in a machine-wide location instead). Builds work from an admin-elevated install (C:\Program Files), which previously failed every Build press with EPERM. A packaged editor no longer kills its own dev server on the first native build. A corrupted shared toolchain manifest could make the installer wipe node_modules and silently drop ...
v0.4.1
A hotfix for a serious bug in 0.4.0. If you are on 0.4.0, update. 0.4.0 has been marked pre-release and its Windows build should not be used.
🐛 Fixes
The editor served the WRONG project after every switch but the first. Open project A, then B: the editor reported B, the title said B, and the renderer loaded A's game code and A's assets — so edits were saved against A's tree. The only visible symptom was a console error blaming server.fs.allow, which was not the cause.
The chain: stopping the dev server resolved when the kill was issued rather than when the process exited, so the old Vite still held the port when its replacement spawned. The replacement died on --strictPort, the stale server kept answering, and a late exit event from the dead predecessor cleared the bookkeeping that described its live replacement — leaving an untracked server that outlived the editor and held the port into the next launch too.
It is now enforced by identity rather than timing, in three layers that each close a hole the others don't:
- a superseded child can no longer clear state describing its replacement;
- stopping the server waits for the process to actually exit, so the port is free before the respawn;
- the server answering on the port must be the one we spawned — checked before spawning and again while waiting for it to come up.
A dev server this editor leaked onto the port (from a crash, or the state bug above) is now reclaimed at startup instead of quietly left running — it was still watching the repo and rewriting asset sidecars under a project nobody had open. A server belonging to another install or another live editor is refused and named, never taken.
The renderer's error message no longer asserts a cause it can't know: it asks the dev server who it is and names the other project.
Windows-only, both fixed: Stop Editor was a complete no-op that reported success, and the Open Recent submenu was unreachable from agent tooling (its labels are native paths, and they were being parsed as menu paths).
The same class of bug on the other side: on macOS, Open Recent was likewise unaddressable — a mac recent-project label is an absolute POSIX path, so it contained the very separator used to parse it, and the menu refused the exact path it had just advertised. And the force-kill path could still return before the process had released its port, which after the above change made the editor refuse to start on a port its own dying server still held.
📝 Other notable
- 0.4.0's Windows regression was introduced by a #185 change in that release. macOS 0.4.0 and all of 0.3.8 are unaffected by the wrong-project bug, though the two macOS defects above are older.
Full Changelog: v0.4.0...v0.4.1
v0.4.0
⚠️ Withdrawn on Windows — marked pre-releaseWindows builds of 0.4.0 have a serious bug: after the first project you open, every
subsequent Open Project silently loads the previous project's code. A stale Vite server
keeps port 5173, the new one dies on startup, and the editor then reports one project while
serving another — so edits can be saved against the wrong project's tree.This release is marked pre-release so the auto-updater holds installs at v0.3.8, which is
unaffected. Windows users should stay on or roll back to v0.3.8 until 0.4.1.macOS is not affected — the regression is in the Windows-only process-kill path.
A fix is in progress and will ship as 0.4.1.
A large release — a whole new media subsystem, variable fonts, and a sustained push on low-end device performance.
✨ Features
Video is a first-class asset. .mp4 registers as an asset kind with its own converter, settings source-of-truth and Inspector. A VideoPlayer can drive a 3D screen, a 2D PixiJS sprite, a UI node, or a fullscreen cutscene overlay that can't softlock on skip. Clips play from the bundle or download from a remote host into a resumable LRU cache; playback couples to timeScale, emits events, and answers to declarative video.* actions. A Timeline video track lets a Director sequence a cutscene. The new video-demo project tours every surface.
Variable fonts. The bake can finally pick a weight — axis instancing at build time, and a dynamic path that honours the sidecar's axes, size, pxRange and charset. The Font Inspector offers a font's real variation axes and stops offering what it can't honour. Every bundled engine font is now GUID-bearing.
Quality tiers and a boot ramp probe. A device capability probe keyed per platform, a tier resolver with a change policy, and the tier exposed in Project Settings with a live preview in the debug menu. At boot, a ramp probe measures throughput and caches its verdict; iOS skips measuring entirely and resolves from the model id. Validated against five real phones, which said the population is three bands.
A frame-time profiler. Marker hierarchy with aggregation, GPU timestamp queries that split rest-time into GPU-busy vs idle, counters, frame capture and scrub exported as plain JSON, and a Profiler panel in the editor — plus an agent-facing surface.
Rendering. Per-layer light masks (measured 7–12× on a low-end Android), LOD chains, BlobShadow as a grounding cue for tiers that can't afford real shadows, and GPU context-loss recovery that rebuilds the renderer in both viewports.
Shadows you can author. The directional shadow box now follows the view — or centres on an entity you nominate — so a walking subject stops losing its shadow. Rigged characters cast and receive properly, and shadow flags are authorable.
Haptics. A new engine subsystem with native provisioning, a HapticSettings trait and declarative haptics.* actions.
Building for devices. Native targets scaffold from the CLI or the editor, and the Build menu now picks the iOS/Android target and the specific device, starting the build from the selection. Shipped floors raised to iOS 16.4 / Android 12, with build.iosMinVersion driving both the JS target and the Xcode deployment target.
🐛 Fixes
- Assets — renaming, moving or deleting an asset forked it: the bound editor kept writing to the old path, and autosave wrote a deleted file back.
- Prefabs — prefab-edit lost its session on exit, on save and on Play/Stop; a prefab serialized blank asset refs; 2D prefabs previewed blank; a Transform-only group got no 2D gizmo, so it couldn't be dragged.
- Text — MTSDF glow was inert, outline flooded the quad, and shadow read neighbouring glyphs. GLSL float literals rounded to 1dp rendered glyphs as NaN; the msdf wasm never shipped at all; a dynamic font could rasterize another font's outlines; and a missing asset behind the worker handshake hung the whole game rather than failing.
- Scenes — a stopped editor baked runtime state into authored entities, and system-spawned entities were saved as authored content. Asset refs held on game traits and introduced by prefab-instance overrides are now collected.
- Skinning —
removeBonerenumbered the skeleton but not the parts, and a vertex bound only to the deleted bone collapsed to the origin. The weight brush is a Skin-panel gesture, not a SceneView one. - Builds — a build step's grandchildren outlived the abort meant to kill them; the shutdown path SIGKILLed a child in the same tick it SIGTERMed it; stopping Vite on Windows now takes its build tree with it. Builds serialize server-side behind a lock.
- Config privacy — private build fields (Apple Team ID, device ids, deploy targets) moved out of the committed project config, and out of the shipped bundle.
- Editor chrome — audio/texture/environment inspectors misreported an off-list setting, the Game toolbar wrapped out of its own bar, and Ctrl+C on macOS jumped the selection instead of doing nothing.
⚡ Performance
Sustained work on a low-end Android target, all measured on real hardware rather than estimated: suppressing IBL on the low tier took one scene from 20 → 44 fps; welding static visuals cut 709 scene objects to 55; a field floor was spending 184k triangles on flat ground. Transform propagation no longer pays full price twice a frame on a frozen scene, dynamic fonts seed from their bake and only generate on a miss, and the local test gate dropped from 245s to ~110s.
Two things were measured and declined: draw-call batching (works — 235 calls → 38 — and buys nothing; ships default off) and pointer prediction (the ON state was worse than OFF).
📝 Other notable
- ECS gained
spawnEntity/destroyEntity, making the index pairing impossible to forget. - Android honours
capacitor.statusBarHiddenfor immersive fullscreen. - Device tooling can pick which phone and claim it machine-wide, write as well as read, and record an input watch so a missed gesture leaves evidence.
Full Changelog: v0.3.8...v0.4.0
v0.3.8
A maintenance release focused on two things that could leave the editor stuck: a boot crash after updating over an existing install, and iOS/Android device debugging picking the wrong route.
🐛 Fixes
- The editor no longer crashes on boot after an update-over-install. A
does not provide an export named …SyntaxError could greet you on first launch after auto-update. Wiping the Vite dep-cache alone was a no-op — Vite serves dependency chunks asimmutable, and an engine-only update leaves the URL byte-identical, so Chromium replayed the pre-update body out of its own disk cache. Both the HTTP cache and V8's compiled-code cache are now cleared on a build-signature change, and the boot-recovery screen clears them before reloading (a bare reload could never fix this, which is why the retry was always spent for nothing). - Android devices no longer fall into the iOS WebDriverAgent path. A device reached by IP with no CDP route was assumed to be an iPhone, so every tap on an Android phone probed port 8100 on it and returned iOS advice ("Set MODOKI_IOS_DEVICE_UDID", pick from your paired iPhones). The lease now asks the device what it is at connect time and gates all four entry points on the answer. Android by-IP tap: 0.53s → 0.14s, with Android's real advice in the banner.
- A timed-out WebDriverAgent launch is no longer killed and then advised to be waited for. The timeout branch killed the agent and told you it might still be starting, so every subsequent input op paid a fresh 60-second spawn-then-kill of a signed agent on your phone, indefinitely. A slow first install is now left to finish and picked up by a later call, guarded so a second agent is never spawned alongside it. Still-starting and already-exited now report differently, because they need opposite advice.
- Bounded the device CDP discovery probe. An
adb forwardlistens whether or not anything is behind it, so a sleeping or WiFi-adb device could accept the socket and never answer — hangingdevice_taprather than merely slowing it. Capped at 4s, matching the rest of that path.
⚡ Performance
- WebDriverAgent probing is now macOS-only and bounded, removing an unbounded ~2.5s probe from every input op on non-macOS clones.
📝 Other notable
- Plans landed for unified debug/release build flags (#112) and video playback (#106), plus regression cover pinning the one-launch device guard.
Full Changelog: v0.3.7...v0.3.8
v0.3.7
Trusted input on real devices, a class of silent asset-loading failures fixed, and a security update.
✨ Features
- Trusted input on physical devices. Agent-driven taps, drags and typing on a connected device can now be real OS-level events (
isTrusted) rather than synthetic ones, so anything gated on trusted input — native gestures, some framework handlers — behaves exactly as it does under a human finger.- Android needs only adb and a debuggable WebView (true of debug builds), and gets the trusted path out of the box.
- iOS goes through WebDriverAgent, which is provisioned as a Build Support item: it is not bundled in the app — it is fetched from a pinned source and compiled + code-signed on your machine, so it needs Xcode and an Apple team. It also auto-installs only when it can actually succeed, rather than failing halfway.
- Without either, device input still works. Synthetic input remains the fallback everywhere, now fronted by a banner naming the cause and the consequence — as a prefix, not a trailing marker that was easy to skim past. Nothing silently degrades.
- Two gestures have no trusted form and say so instead of pretending:
hover(a touchscreen has no hover state) andscroll(it could only be a touch drag, which on a game canvas is a real gameplay gesture).
- iOS out-of-app screenshots. Capture works outside the app's own webview, so system UI and out-of-app states are visible.
🐛 Fixes
- A missing asset no longer reads as a corrupt one. A dev server answers a missing file with
200 OKand an HTML error page; the loaders parsed that as content and reported a corrupt scene, sending you after the wrong bug. Fixed for scenes and for the seven other asset fetches that had the same hole (#91). - Security:
uuidbumped to ^11.1.1 — GHSA-w5hq-g745-h8pq (CVE-2026-41907). - Native builds no longer ship stale plugin code. Engine Capacitor plugins are re-vendored on every native build, with a guard against a stale tarball — previously a build could succeed and silently install the previous native code (#90).
- The debug bridge releases port 9095 when the app is backgrounded, so the foreground app owns it (#95).
- Synthetic input hit-tests its target canvas instead of assuming it (#93).
📝 Other notable
- Windows and e2e now run on every push via the free public runner, alongside a DMG + Windows installer build — coverage that had been off for cost reasons.
- Publishing hardening. The OSS snapshot no longer ships device identifiers or LFS pointer stubs, and it now carries line-ending pins (without them, Windows saw every generated file as stale).
modoki-engine.commoved to a Cloudflare Worker, retiring the GCP load balancer.
Editor-only release: no engine API changes. Existing installs will pick this up through the in-app updater.
Full Changelog: v0.3.6...v0.3.7