Skip to content

feat: editor_api — wasm editor control surface for studio Play mode (contract v1) - #662

Merged
apotema merged 2 commits into
mainfrom
feat/editor-api
Jul 3, 2026
Merged

apotema merged 2 commits into
mainfrom
feat/editor-api

Conversation

@apotema

@apotema apotema commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Engine side of labelle-studio's Play mode (Phase 3). The studio runs the game compiled to wasm in an iframe and drives it via exported functions (validated by the working spike on FP's bgfx-wasm build: plain export fn + emcc -sEXPORTED_FUNCTIONS).

The contract (v1 — assembler + studio build against exactly this)

src/editor_api.zig, exported from the root as pub const editor_api. Non-preview builds emit no editor_* symbols (see verification below).

// Exports (all plain `export fn`):
editor_alloc(len: usize) ?[*]u8            // c_allocator (== emscripten malloc on wasm)
editor_free(ptr: [*]u8, len: usize) void
editor_pause(paused: i32) void             // 1 = freeze SIM (render continues), 0 = resume
editor_step(frames: u32) void              // while paused: advance N sim ticks
editor_set_scene(name: [*]const u8, len: usize) i32                       // 0 = ok
editor_load_scene(name: [*]const u8, nlen: usize, src: [*]const u8, slen: usize) i32
   // copy + store src in a runtime scene-source override map; if `name` is the
   // current scene, reload it now; 0 = ok
editor_set_entity_position(id: u64, x: f32, y: f32) void  // world coords + markPositionDirty
editor_pick(x: f32, y: f32) i64            // v1: always -1 (studio picks client-side)
editor_scene_digest(out: [*]u8, cap: usize) usize
   // JSON: {"scene":"...","paused":0|1,"entity_count":N,
   //        "entities":[{"id":<u64>,"prefab":"?","sprite":"?","x":f,"y":f},...]}
   // prefab/sprite included when known; entity list truncated to fit cap
   // (always valid JSON); returns bytes written
editor_set_camera(x: f32, y: f32, zoom: f32) void   // engages override
editor_release_camera() void

// Non-export API for the generated main (the assembler splices these 3 touchpoints):
pub fn bind(g: anytype, runner: anytype) void   // once, after Game init
pub fn shouldTick() bool                        // gate the SIM half of the frame loop
pub fn frame(g: anytype) void                   // AFTER the sim tick: re-asserts editor camera

How bind/dispatch works

bind is comptime-generic: it instantiates Holder(GP, RP) for the concrete *Game/Runner types, stores the pointers in that instantiation's container-level vars, and publishes a *const VTable of plain function pointers (set_scene / load_scene / set_entity_position / scene_digest / apply_camera). The exports are non-generic and dispatch through the vtable; before bind, every export is a safe no-op (digest writes {}, scene ops return -1, void setters ignore; editor_pause/editor_step are pure editor state and work pre-bind).

Details worth noting:

  • Camera: editor_set_camera stores the override and applies immediately; frame(g) re-asserts it after every tick (FP's camera_control re-asserts the gameplay camera each tick — the editor wins by writing last). Comptime no-op for camera-less games (Game.CameraType == void).
  • editor_set_scene validates the name against the scene registries before calling setScene — a raw setScene tears the current scene down before discovering SceneNotFound, so a typo'd editor request must not nuke the running scene. Asset-gate deferrals queue a retry via queueSceneChange (same commit detection as fix: queued scene change dropped when its assets aren't ready yet #635).
  • Entity move goes through setWorldPosition (world coords → local for parented entities), which routes through setPosition → markPositionDirtyWithChildren.

Scene-source override map

New Game-owned scene_source_overrides (keys and values are allocator-owned copies; replacing an entry frees the previous source, freed on deinit). Consulted before the embedded source at both loader seams:

  • loadSceneFromSource (the assembler-generated per-scene loader) resolves by the name of the scene being loaded — a new loading_scene_name is set around the three loader_fn call sites (setScene, setSceneAtomic, hot-reload).
  • loadSceneFile (include fragments) resolves by exact path, then by the path's stem (an override stored under "frag" also replaces "scenes/frag.jsonc").

How "no editor symbols in non-preview builds" was verified

root.zig exposes the module only as a lazy pub const editor_api = @import("editor_api.zig") and nothing else in the engine references it. Built two otherwise-identical probe executables against this branch's engine module:

  • noeditor (uses Game init/tick/render, never references editor_api): nm shows 0 editor_* symbols.
  • witheditor (calls bind/shouldTick/frame): nm shows all 10 exports as global T _editor_* symbols.

Both binaries also run correctly.

Tests

zig build + zig build test exit 0 (all 63 test binaries green).

  • test/editor_api_test.zig — pause/step tick gating, pre-bind no-op safety, alloc/free round-trip, digest JSON shape (parsed with std.json), truncation validity (including a cap sweep 2..400, every output parses), editor_set_entity_position (incl. bad ids), editor_set_scene, camera override state machine (engage → re-assert-after-tick → release; comptime no-op on StubRender).
  • test/scene_source_override_test.zig — set → load consults override (both loader seams), replace frees old + takes effect (leak-checked), exact-key-vs-stem lookup rules, editor_load_scene end-to-end (stores + reloads current scene now).

Deviations from the task spec

  • Version was bumped 1.66.0 → 1.67.0 (main had already released 1.66.0).
  • The digest hand-rolls JSON into the caller buffer instead of the repo's std.json.Stringify.valueAlloc convention — the export is allocation-free with cap-bounded truncation semantics; escaping is implemented explicitly and validated by parsing every emitted digest in tests.
  • editor_load_scene/editor_set_scene return -1 pre-bind (the contract's "setters return/ignore" left the i32 value open).

https://claude.ai/code/session_017pW3ifKf9wgxNg4viy6okw

…contract v1)

Engine side of labelle-studio's Play mode (Phase 3). The studio runs
the game compiled to wasm in an iframe and drives it through plain
`export fn editor_*` symbols; the assembler-generated main binds the
concrete Game/Runner into the module under its preview flag.

- src/editor_api.zig: bind/vtable dispatch, pause/step sim gating
  (shouldTick), post-tick camera override re-assert (frame), scene
  digest JSON with cap-safe truncation, editor_alloc/free on the C
  allocator (== emscripten malloc on wasm). Every export is a safe
  no-op before bind. Non-preview builds emit no editor_* symbols
  (lazy pub const, verified by nm on paired probe binaries).
- Runtime scene-source override map (editor_load_scene): Game-owned
  copies keyed by scene name, consulted before the embedded source in
  loadSceneFromSource (via loading_scene_name, set around the three
  loader_fn call sites) and in loadSceneFile (exact path, then stem).
  Replaced entries free the previous copy.
- Tests: test/editor_api_test.zig + test/scene_source_override_test.zig
  wired into build.zig test_files.
- Version 1.66.0 -> 1.67.0.

Claude-Session: https://claude.ai/code/session_017pW3ifKf9wgxNg4viy6okw
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai

coderabbitai Bot commented Jul 3, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds an editor play-mode API, runtime scene-source override storage and lookup, loader integration for override-aware scene loading, and tests plus build/version updates.

Changes

Editor API and scene override flow

Layer / File(s) Summary
Editor API core and exports
src/editor_api.zig, src/root.zig
Adds editor state, bind/tick/frame helpers, wasm exports for pause/step/scene/entity/camera/digest/alloc/free, JSON digest serialization helpers, and a root-level re-export.
Game override state and API
src/game.zig, src/game/lifecycle_mixin.zig, src/game/misc_mixin.zig
Adds runtime scene-source override storage, loading-scene tracking, initialization/cleanup, and exact/stem-based override lookup plus add/remove helpers.
Scene loading uses override source
src/game/scene_mixin.zig, src/game/loop_mixin.zig, src/jsonc/scene_loader/scene_process.zig
Sets loading context around scene loads and hot reloads, and resolves in-memory/file scene sources through runtime overrides before embedded or filesystem fallbacks.
Editor API behavior tests
test/editor_api_test.zig
Adds coverage for pause/step gating, pre-bind safety, digest output/truncation, entity position updates, scene switching, and camera override behavior.
Scene override tests and build wiring
test/scene_source_override_test.zig, build.zig, build.zig.zon
Adds tests for override replacement, resolution, include fragments, editor load behavior, malformed rollback, registers the tests, and bumps the package version.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Host
  participant editor_api
  participant Game
  participant SceneProcess

  Host->>editor_api: editor_load_scene(name, src)
  editor_api->>Game: setSceneSourceOverride(name, src)
  alt current scene reload
    editor_api->>Game: setScene(name)
    Game->>Game: loading_scene_name = name
    Game->>SceneProcess: entry.loader_fn(self)
    SceneProcess->>Game: sceneSourceOverride(name)
    Game-->>SceneProcess: override source
    Game->>Game: loading_scene_name = null
  else non-current scene
    editor_api-->>Host: override stored
  end
Loading

Poem

A rabbit tapped the pause with glee,
Then hopped through scenes in memory.
Overrides twinkled, neat and bright,
Digests stayed valid day and night.
Hop, hop — the camera held its place!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the new editor_api wasm control surface for studio Play mode.
Description check ✅ Passed The description is detailed and clearly matches the editor API, scene override, and testing changes in the PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/editor-api

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 59d7c6be61

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/editor_api.zig Outdated
Comment on lines +37 to +38
//! g.render();
//! editor_api.frame(&g); // AFTER the tick: re-assert camera

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reassert the editor camera before rendering

If the assembler follows this documented touchpoint sequence, editor_api.frame runs only after g.render(). In unpaused Play mode where a tick script updates the gameplay camera, the render for that frame has already used the script camera, and the editor override written here is overwritten again on the next tick before the next render, so studio camera control only reliably works while paused. The reassertion needs to happen after the tick but before rendering.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7a6ba8d. Valid: with frame() documented after g.render(), an unpaused frame always rendered the script's camera write and the override only held while paused. The documented touchpoint sequence (this module doc is the contract the assembler splices from) is now tick → editor_api.frame → g.render(), and frame's doc spells out why the ordering is load-bearing. Added a loop-order test (camera override: documented loop order…) that simulates 3 unpaused iterations of tick-writes-camera → frame → render and asserts the override is what render observes every frame.

Comment thread src/editor_api.zig Outdated
fn loadSceneImpl(name: []const u8, src: []const u8) i32 {
game.setSceneSourceOverride(name, src) catch return -1;
if (game.getCurrentSceneName()) |cur| {
if (std.mem.eql(u8, cur, name)) return setSceneImpl(name);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid blanking the scene on bad editor reloads

When the studio sends malformed or partially typed JSONC for the current scene, this path stores the override and immediately calls setSceneImpl, whose setScene unloads the current scene before the loader can fail. The export then returns -1 with current_scene_name cleared and the bad override still installed, so the preview goes blank and a later corrected editor_load_scene no longer reloads automatically because there is no current scene to match.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7a6ba8d. Valid — same teardown-before-validate class as the editor_set_scene issue: a malformed source for the current scene blanked the preview (setScene unloads before the loader parses), left the bad override installed, and cleared current_scene_name so a corrected editor_load_scene no longer auto-reloaded. The current-scene reload is now transactional: snapshot the previous override (exact key), install the new source, and on reload failure roll the map back (new Game.removeSceneSourceOverride) and reload from the last-good source. Returns -1 with the scene intact; a corrected follow-up auto-reloads. Covered by editor_load_scene: malformed source for the current scene rolls back instead of blanking (both the no-previous-override and previous-good-override cases).

Comment thread src/editor_api.zig Outdated
Comment on lines +368 to +371
const pos = game.getComponent(ent, core.Position);
const x: f32 = if (pos) |p| jsonSafeFloat(p.x) else 0;
const y: f32 = if (pos) |p| jsonSafeFloat(p.y) else 0;
try appendFmt(buf, cur, ",\"x\":{d},\"y\":{d}}}", .{ x, y });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Report world positions in the scene digest

For parented entities, the Position component is local to the parent, but this digest publishes it directly while editor_set_entity_position explicitly consumes world coordinates. In scenes with nested prefab/child entities, the studio will render and edit those entities at their local offsets instead of their actual world positions, causing selections/drags to appear in the wrong place or jump when written back.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7a6ba8d. Valid: the digest published the raw (parent-relative) Position while editor_set_entity_position consumes world coordinates, so nested prefab children would draw/drag at local offsets. The digest now emits game.getWorldPosition(ent) and the export doc states x/y are world coords. Covered by digest: parented entities report WORLD positions…, including the round-trip assertion that feeding the digested coords back through editor_set_entity_position leaves the local Position unchanged.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/game/loop_mixin.zig (1)

160-164: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Minor style inconsistency: no defer/block-scope guard here unlike the other two call sites.

Currently correct only because entry.loader_fn(self) catch {} swallows all errors, so the following self.loading_scene_name = null; always executes. But this differs from setScene/setSceneAtomic, which wrap the same pattern in a defer-guarded block. A future edit that changes catch {} to try (or adds an early return) would silently leave loading_scene_name stuck, corrupting the next override resolution. Matching the other two sites removes that footgun cheaply.

♻️ Proposed fix for consistency
-                        self.loading_scene_name = name;
-                        entry.loader_fn(self) catch {};
-                        self.loading_scene_name = null;
+                        {
+                            self.loading_scene_name = name;
+                            defer self.loading_scene_name = null;
+                            entry.loader_fn(self) catch {};
+                        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/game/loop_mixin.zig` around lines 160 - 164, The scene-loading override
block in `loop_mixin` should match the `setScene` and `setSceneAtomic` pattern
by using a `defer`-style guard around `loading_scene_name`. Update the
`entry.loader_fn(self)` call site so `self.loading_scene_name` is always cleared
through scoped cleanup rather than relying on the current `catch {}` path. This
keeps the behavior consistent across all loader entry points and prevents
`loading_scene_name` from remaining set if the call flow changes later.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/game/loop_mixin.zig`:
- Around line 160-164: The scene-loading override block in `loop_mixin` should
match the `setScene` and `setSceneAtomic` pattern by using a `defer`-style guard
around `loading_scene_name`. Update the `entry.loader_fn(self)` call site so
`self.loading_scene_name` is always cleared through scoped cleanup rather than
relying on the current `catch {}` path. This keeps the behavior consistent
across all loader entry points and prevents `loading_scene_name` from remaining
set if the call flow changes later.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7bab0ca6-7c8a-492b-ad76-f1d991ef57d0

📥 Commits

Reviewing files that changed from the base of the PR and between 92a6e06 and 59d7c6b.

📒 Files selected for processing (12)
  • build.zig
  • build.zig.zon
  • src/editor_api.zig
  • src/game.zig
  • src/game/lifecycle_mixin.zig
  • src/game/loop_mixin.zig
  • src/game/misc_mixin.zig
  • src/game/scene_mixin.zig
  • src/jsonc/scene_loader/scene_process.zig
  • src/root.zig
  • test/editor_api_test.zig
  • test/scene_source_override_test.zig

…al editor_load_scene, world-space digest

- P1 (touchpoint ordering): the documented generated-main sequence now
  places editor_api.frame(&g) AFTER the tick but BEFORE g.render() —
  with frame after render, every unpaused frame rendered the script's
  camera and the override only held while paused. Doc-contract fix +
  a loop-order test asserting the override is visible at render time
  on every unpaused frame.
- P1 (blank preview on bad reload): editor_load_scene for the CURRENT
  scene is now transactional — snapshot the previous override
  (exact key), install the new source, and on reload failure roll the
  map back (new Game.removeSceneSourceOverride) and reload the
  last-good source. A malformed edit reports -1 with the scene intact
  and a corrected follow-up still auto-reloads.
- P2 (digest coordinate space): entity x/y in editor_scene_digest now
  come from getWorldPosition, matching the world space
  editor_set_entity_position consumes; parented prefab children no
  longer report parent-local offsets. Round-trip test added.

Claude-Session: https://claude.ai/code/session_017pW3ifKf9wgxNg4viy6okw

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
test/editor_api_test.zig (1)

98-106: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Missing unbind() isolation calls.

Every other test in this file brackets itself with editor_api.unbind() at start and defer editor_api.unbind(), per the file's own documented convention (module-scope dispatch state). This test omits both. Low practical risk since alloc/free are likely independent of bind state, but it's inconsistent with the stated isolation pattern and could bite if editor_alloc/editor_free state is ever coupled to bind/unbind internals.

♻️ Suggested fix
 test "editor_alloc / editor_free round-trip" {
+    editor_api.unbind();
+    defer editor_api.unbind();
+
     const ptr = editor_api.editor_alloc(128) orelse return error.TestUnexpectedResult;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/editor_api_test.zig` around lines 98 - 106, The editor_alloc /
editor_free round-trip test is missing the standard bind-state isolation used
elsewhere in this file. Update the test body to follow the same pattern as the
other editor_api tests by calling editor_api.unbind() at the start and deferring
editor_api.unbind() at the end. Keep the rest of the assertions unchanged so the
test remains focused on allocator behavior while preserving module-scope
dispatch state isolation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/editor_api_test.zig`:
- Around line 98-106: The editor_alloc / editor_free round-trip test is missing
the standard bind-state isolation used elsewhere in this file. Update the test
body to follow the same pattern as the other editor_api tests by calling
editor_api.unbind() at the start and deferring editor_api.unbind() at the end.
Keep the rest of the assertions unchanged so the test remains focused on
allocator behavior while preserving module-scope dispatch state isolation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fc9469a0-08c3-4012-84ee-cfb82901eb03

📥 Commits

Reviewing files that changed from the base of the PR and between 59d7c6b and 7a6ba8d.

📒 Files selected for processing (5)
  • src/editor_api.zig
  • src/game.zig
  • src/game/misc_mixin.zig
  • test/editor_api_test.zig
  • test/scene_source_override_test.zig
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/game.zig
  • test/scene_source_override_test.zig
  • src/editor_api.zig

@apotema
apotema merged commit bf2216f into main Jul 3, 2026
3 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a6ba8de63

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/editor_api.zig
// …and reload the scene from the last-good source so the
// preview doesn't stay blank. If even that fails we're no
// worse off than before this rollback existed.
_ = setSceneImpl(name);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Balance asset acquires when rolling back bad scene sources

When editor_load_scene receives malformed JSONC for the current scene and that scene has a non-empty asset manifest, the failed setSceneImpl call has already acquired the target assets and cleared current_scene_name before returning -1. Retrying through setSceneImpl here restores the scene, but because there is no previous scene name anymore the success path never releases the speculative acquire, so each bad editor save pins the scene's asset refcounts one higher. The rollback path should release/clear the pending manifest acquire before reloading the last-good source.

Useful? React with 👍 / 👎.

Comment thread src/editor_api.zig
// request must not nuke the running scene.
if (game.scenes.get(name) == null and game.jsonc_scenes.get(name) == null)
return -1;
game.setScene(name) catch return -1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Roll back failed switches to overridden scenes

When an override has been stored for a non-current scene, switching to that scene through editor_set_scene reaches this raw game.setScene call without the rollback used by editor_load_scene. If the stored source is malformed or partially typed, setScene unloads the currently running scene before the loader fails, so the export returns -1 with the preview blank and the bad override still installed. This is fresh evidence beyond the current-scene reload path: the non-current override path is not covered by loadSceneImpl's transaction.

Useful? React with 👍 / 👎.

Comment thread src/editor_api.zig
pending_steps -= 1;
return true;
}
return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep Game.tick running during editor pause

When the studio is paused, this false is intended to gate only simulation, but the documented generated main wraps the whole g.tick(dt) in shouldTick(). I checked src/game/loop_mixin.zig: Game.tick pumps assets and processes pending_scene_change before its own pause gate, so a paused editor_set_scene that defers on asset readiness queues a retry but neither the asset pump nor the retrier runs until unpaused or stepped. The editor pause path needs to keep the always-run tick work alive while freezing gameplay simulation.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant