Capture runs boot again: the fixture seed borrows TrainerOpen once (#2417)

Every capture run panicked at startup. The fixture seed `seed_ui_fixture` took `ResMut<TrainerOpen>` 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 <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-10-25 21:47:16 +07:00
committed by GitHub
parent 92c701c90d
commit e4bff386f8
2 changed files with 42 additions and 2 deletions
+3 -2
View File
@@ -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<CaptureCtx>,
mut commands: Commands,
progress: Res<WorldLoadProgress>,
mut quest_log: ResMut<crate::ui_quest_log::QuestLog>,
mut trainer: ResMut<crate::ui_trainer::TrainerOpen>,
icons: Option<Res<crate::entities::ItemDisplays>>,
) {
let icons = icons.as_deref();
@@ -123,7 +123,8 @@ pub(super) fn seed_ui_fixture(
let mut lua: Option<Box<dyn FnOnce(&mut UiScript) + '_>> = 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 {
+39
View File
@@ -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::<bevy::ecs::schedule::Schedules>()
.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"
);
}
}