Repository navigation
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 two required CLIs.
Agent surface: validate_scene now actually checks that a referenced GUID resolves, not just that it's GUID-shaped; a parked/recycled entity slot now correctly reads as destroyed rather than alive; seven asset-import controls that were previously unreachable by an agent now are; a key blocked by the keyboard scope no longer reports ok:true; a bare/misspelled tool name inside a modoki_batch no longer voids the whole batch.
Many further editor, rendering, 2D and asset-pipeline fixes beyond the ones named above — several hundred in total across the release.
⚡ Performance
Beyond the tier work: the scroll view recycles entries rather than materialising them, and the first-frame stall on low-end hardware is fixed (see the shader-prewarm fix above). A 2D-only project no longer ships three.js's GLTF/HDR/UltraHDR/KTX2 loaders or the meshopt decoder (−184 kB raw / −59 kB gzip measured), and a genuine gap where render3d: false still shipped 546 KB of unreachable three/webgpu code is closed; a new video module toggle removes ~2.7 MB from a project that doesn't need video.
📝 Other notable
- Windows path handling, native toolchain detection, and the device-claim guard all hardened.
- The bundled GPU benchmark table (used by device classification) now vendors an expanded, permission-granted third-party dataset, credited in
THIRD-PARTY-NOTICES.md.
Full Changelog: v0.4.1...v0.5.2