From e4bff386f8a9c245f47d1ad41bd8f504f44e2305 Mon Sep 17 00:00:00 2001 From: Sam Sung <51686767+samwhosung@users.noreply.github.com> Date: Sun, 25 Oct 2026 21:47:16 +0700 Subject: [PATCH] Capture runs boot again: the fixture seed borrows TrainerOpen once (#2417) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every capture run panicked at startup. The fixture seed `seed_ui_fixture` took `ResMut` beside the VM turn, whose call host has lent the trainer window since #2331, and Bevy refuses a system that borrows one resource mutably twice. The seed now reaches the trainer state through the turn's parts. A sweep found no other capture system that overlaps the host. Test: `every_capture_system_initializes_in_the_client` builds the client with the capture plugin and initialises every schedule's systems. It panicked with the access conflict before the fix. The existing schedule census left the capture plugin out, which is why the gates stayed green. Fixes #2413 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 5.5 --- crates/benilla-app/src/capture/fixtures.rs | 5 +-- crates/benilla-app/src/capture/mod.rs | 39 ++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/crates/benilla-app/src/capture/fixtures.rs b/crates/benilla-app/src/capture/fixtures.rs index 380dc7aaf..2d3d993a3 100644 --- a/crates/benilla-app/src/capture/fixtures.rs +++ b/crates/benilla-app/src/capture/fixtures.rs @@ -108,13 +108,13 @@ pub(super) fn seed_ui_fixture( ResMut<'static, crate::loading_screen::LoadingScreen>, ResMut<'static, crate::ui_gossip::GossipState>, ResMut<'static, crate::ui_quest::QuestGiver>, + ResMut<'static, crate::ui_trainer::TrainerOpen>, ), )>, mut ctx: ResMut, mut commands: Commands, progress: Res, mut quest_log: ResMut, - mut trainer: ResMut, icons: Option>, ) { let icons = icons.as_deref(); @@ -123,7 +123,8 @@ pub(super) fn seed_ui_fixture( let mut lua: Option> = None; vm.turn(|script, parts| { let (mut merchant, mut loot, mut names, mut selection, mut player, store, rest) = parts; - let (mut actions, mut bank, mut exit, mut loading, mut gossip, mut quest) = rest; + let (mut actions, mut bank, mut exit, mut loading, mut gossip, mut quest, mut trainer) = + rest; let (mut items, mut index, mut home_bind) = store; // A glue-screen capture has no world scenario, and no glue screen opens a UI fixture. let Some(scenario) = ctx.scenario else { diff --git a/crates/benilla-app/src/capture/mod.rs b/crates/benilla-app/src/capture/mod.rs index 270c6ad7f..89dc8a777 100644 --- a/crates/benilla-app/src/capture/mod.rs +++ b/crates/benilla-app/src/capture/mod.rs @@ -1064,3 +1064,42 @@ fn drive_capture( } } } + +#[cfg(test)] +mod tests { + use crate::local_state::test_env::{EnvGuard, ENV_LOCK}; + use bevy::prelude::*; + + /// A capture run's systems initialize in the client's own world, so a parameter that overlaps + /// the VM turn's call host fails here and not at the first capture's schedule build. + #[test] + fn every_capture_system_initializes_in_the_client() { + let _l = ENV_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let _c = EnvGuard::set("WOW_CAPTURE", "ui-trainer"); + let mut app = crate::game_plugins::schedule_tests::headless_client_with(|app| { + app.add_plugins(super::CapturePlugin); + }); + let world = app.world_mut(); + let labels: Vec<_> = world + .resource::() + .iter() + .map(|(_, s)| s.label()) + .collect(); + let mut seen = false; + for label in labels { + world.schedule_scope(label, |world, schedule| { + schedule.graph_mut().systems.initialize(world); + seen |= + schedule.graph().systems.iter().any(|(_, system, _)| { + system.name().to_string().contains("seed_ui_fixture") + }); + }); + } + assert!( + seen, + "the census reads the capture seed among the client's systems" + ); + } +}