Fix diagnostics back navigation and keep mod selection rows stable

This commit is contained in:
raptorreplica committed 2026-10-11 16:25:59 +11:00
1 parent ab23255d2c
commit 4b1edefeec
3 files changed
+136 -41

No files matched your search

+8
View File
@@ -8,6 +8,14 @@ Gameplay uses the original 320×200 simulation canvas, presented at 4:3 with asp
The widescreen mouse check covers all eight main actions at 1280×720, 1280×800 and 1920×1080, secondary-screen offsets and clicks outside the viewport. The full 42-frame menu/intro audit was rerun after the layout change. A relocated packaged SDL executable also rendered menu and gameplay captures at 1280×800 and 1920×1080, with nonzero stereo audio captures. Those screenshots are under ignored `.reference/standalone-package/`. The widescreen mouse check covers all eight main actions at 1280×720, 1280×800 and 1920×1080, secondary-screen offsets and clicks outside the viewport. The full 42-frame menu/intro audit was rerun after the layout change. A relocated packaged SDL executable also rendered menu and gameplay captures at 1280×800 and 1920×1080, with nonzero stereo audio captures. Those screenshots are under ignored `.reference/standalone-package/`.
Installed mod rows stay in stable ID order through activation, dependency
selection, disabling and priority changes. Requested load priority remains
separate and is visible in diagnostics; arrow-key changes report the new
priority without moving the cursor. Returning from details or a disable
confirmation restores the selected mod and its list position. Opening details
again while already in diagnostics preserves its parent, so Escape always
returns one level. Portable regressions exercise these navigation sequences.
The audit also verifies the complete startup reaches its return screen at exactly 1,891 70 Hz service ticks and restores menu music. Cancelling a mod-mismatch prompt preserves the original save; accepting it displays unavailable equipment without silently discarding it. The audit also verifies the complete startup reaches its return screen at exactly 1,891 70 Hz service ticks and restores menu music. Cancelling a mod-mismatch prompt preserves the original save; accepting it displays unavailable equipment without silently discarding it.
Run the local audit from `RustRaptor/`: Run the local audit from `RustRaptor/`:
+18 -18
View File
@@ -24,10 +24,10 @@ worker from its existing outputs before submitting another generation request.
## Next user fixes ## Next user fixes
1. Mod/save diagnostics: Escape can get trapped after details are reopened from 1. Mod/save diagnostics: parent and selection are now preserved when details
diagnostics, replacing its parent with itself. Preserve parent and selection. are reopened. The Escape regression passes; full screenshot audit is running.
2. Keep installed mod rows stable when enabled/disabled. Load priority must be 2. Installed mod rows now stay in ID order through toggles and priority changes,
separate from the displayed order; dependencies and confirmation stay intact. with dependency and confirmation behavior preserved. Navigation tests pass.
3. Default Forward Guns still appear to miss miscellaneous buildings/cars. 3. Default Forward Guns still appear to miss miscellaneous buildings/cars.
Expand actual original-content coverage beyond the earlier ten-unit test. Expand actual original-content coverage beyond the earlier ten-unit test.
@@ -43,20 +43,20 @@ mod artwork and sound belong in Git.
| Agent | Recover and continue | | Agent | Recover and continue |
| --- | --- | | --- | --- |
| hd_coordinator | Sole global manifest/ledger owner; register fresh approved29boss frames, keepMole9hold; audit all recoveredrequests before resuming | | hd_coordinator | Sole manifest/ledger owner; 29 boss and 48 terrain frames await final registration checks; retain Mole9 hold |
| hd_full_review | Terrain corpus49/66pagesreviewed; pages50–66remain;48terrainframeholdstaysuntilfullreview | | hd_full_review | All 66 terrain pages and 1,706 cases now reviewed; exact-hash terrain approval saved |
| hd_terrain_render | Frozen compositor SHA b30b4a65…;29bossframesapproved,Mole9held;8newportraitscreenssaved,7/fullboardinspectionremain | | hd_terrain_render | Frozen compositor SHA b30b4a65…; 29 boss frames approved, Mole9 held; all eight new portrait screenshots approved |
| campaign_integration | 34effect/projectilefamilies303IDsraw; detailed RESTART.md preserved; severalgeometry/brightness/layout corrections and independentreviewsremain | | campaign_integration | 34 effect/projectile families, 303 IDs have raw outputs; geometry, brightness and layout corrections continue |
| campaign_release_review | Terrain4queue84jobs1344IDs; recover currentraw/contextcandidates andholds | | campaign_release_review | Terrain 4: 84 jobs, 1,344 IDs; 64 candidates saved, half await further visual/Host QA |
| hd_dark_player | Equipment/HUDdigits, acceptedandheldfamilies; reconcilecurrentmanifest before newgeneration | | hd_dark_player | Equipment and HUD digits; reconcile accepted frames, preserve pending Death Ray/Energy review |
| hd_enemies_01 | Enemy01queue139IDs; sourcegeometryholds,SHIP25connectionfailure,APE4service-refusalpending | | hd_enemies_01 | Enemy 1: 139 IDs; source geometry holds, SHIP25 connection failures and APE service refusal preserved |
| hd_terrain_set_1 | Terrain1queue42jobs672IDs; first16registered; recover alreadycompleted103e0output andsubsequentcandidateholds | | hd_terrain_set_1 | Terrain 1: 42 jobs, 672 IDs; first 16 registered; 80 other candidates and explicit rejected scenery remain |
| hd_terrain_set_2 | Terrain2queue90jobs1437IDs; continue savedcanonicalchunks andall-contextreview | | hd_terrain_set_2 | Terrain 2: 90 jobs, 1,437 IDs; continue canonical chunks and full neighboring-context review |
| iron_coast_2 | Frozenenemy03firstdrafts59families138IDs; recovered45familyenemy02queuecontinues, old11familiesfrozen | | iron_coast_2 | Enemy 3: 59 families, 138 raw IDs; recovered 45-family enemy 2 queue continues, old 11 families frozen |
| iron_coast_3 | Boss01queue26families83IDs; recovered11completedatlasfamiliesneedbindings/QA; previous15candidatespreserved | | iron_coast_3 | Boss 1: 26 families, 83 IDs; 11 recovered atlas families need binding/QA; previous 15 candidates preserved |
| iron_coast_4 | Boss02queue27families83IDs; approvedsubset/corrections,connectionfailurecooldowns | | iron_coast_4 | Boss 2: 27 families, 83 IDs; continue corrections, complete reviews and respect service cooldowns |
| iron_coast_5 | Boss03complete25families82IDs/328scalechecks; equipment8reviewpending; temporarygroundgunbugassignment | | iron_coast_5 | Boss 3: 25 families, 82 candidates and 328 scale checks; temporarily auditing every destructible classic tile for ground-gun misses |
| iron_coast_6 | Terrain3queue68jobs1082IDs, withmiddleandlateshards; preserve strictdestroyedgeometryholds | | iron_coast_6 | Terrain 3: 68 jobs, 1,082 IDs with middle/late shards; retain destroyed-state geometry holds |
## Durable worker evidence ## Durable worker evidence
+110 -23
View File
@@ -189,6 +189,8 @@ struct Host {
awaiting_binding: Option<String>, awaiting_binding: Option<String>,
diagnostic_mod: Option<String>, diagnostic_mod: Option<String>,
diagnostic_return: Screen, diagnostic_return: Screen,
diagnostic_selection: usize,
mods_selection: usize,
pending_disable: Option<crate::mod_selection::DisablePlan>, pending_disable: Option<crate::mod_selection::DisablePlan>,
world: Option<World>, world: Option<World>,
cinematic: Option<Cinematic>, cinematic: Option<Cinematic>,
@@ -333,6 +335,8 @@ impl Host {
awaiting_binding: None, awaiting_binding: None,
diagnostic_mod: None, diagnostic_mod: None,
diagnostic_return: Screen::Main, diagnostic_return: Screen::Main,
diagnostic_selection: 0,
mods_selection: 0,
pending_disable: None, pending_disable: None,
world: None, world: None,
cinematic: None, cinematic: None,
@@ -373,8 +377,15 @@ impl Host {
}) })
} }
fn go(&mut self, screen: Screen) { fn go(&mut self, screen: Screen) {
if self.screen == Screen::Mods {
self.mods_selection = self.selected;
}
self.screen = screen; self.screen = screen;
self.selected = 0; self.selected = if screen == Screen::Mods {
self.mods_selection.min(self.rows().len().saturating_sub(1))
} else {
0
};
if screen == Screen::Shop { if screen == Screen::Shop {
self.shop_selling = false; self.shop_selling = false;
} }
@@ -905,13 +916,7 @@ impl Host {
.collect(), .collect(),
Screen::Mods => { Screen::Mods => {
let mut r = vec!["Apply selections".into(), "Rescan installed mods".into()]; let mut r = vec!["Apply selections".into(), "Rescan installed mods".into()];
let mut ids = self.pending.clone(); for id in self.mod_ids() {
for id in self.discovery.mods.keys() {
if !ids.contains(id) {
ids.push(id.clone());
}
}
for id in ids {
if let Some(m) = self.discovery.mods.get(&id) { if let Some(m) = self.discovery.mods.get(&id) {
r.push(format!( r.push(format!(
"[{}] {} {}", "[{}] {} {}",
@@ -966,18 +971,9 @@ impl Host {
} }
} }
fn mod_ids(&self) -> Vec<String> { fn mod_ids(&self) -> Vec<String> {
let mut ids = self // Installed rows stay in ID order. Activation priority is a separate
.pending // pending list, so toggles and dependency selection cannot move rows.
.iter() self.discovery.mods.keys().cloned().collect()
.filter(|id| self.discovery.mods.contains_key(*id))
.cloned()
.collect::<Vec<_>>();
for id in self.discovery.mods.keys() {
if !ids.contains(id) {
ids.push(id.clone());
}
}
ids
} }
fn require_pilot(&self) -> Result<()> { fn require_pilot(&self) -> Result<()> {
if let Some(error) = &self.pilot_error { if let Some(error) = &self.pilot_error {
@@ -1310,6 +1306,13 @@ impl Host {
manifest.conflicts.join(", ") manifest.conflicts.join(", ")
} }
)); ));
if let Some(priority) = self.pending.iter().position(|id| id == mod_id) {
lines.push(format!(
"Requested load priority: {} / {} (later overrides win)",
priority + 1,
self.pending.len()
));
}
} }
} }
if let Some(e) = &self.pilot_error { if let Some(e) = &self.pilot_error {
@@ -1335,6 +1338,7 @@ impl Host {
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", ") .join(", ")
)); ));
lines.push(format!("Pending order: {}", self.pending.join(", ")));
lines.extend(self.active.registry.overrides.clone()); lines.extend(self.active.registry.overrides.clone());
for m in self.discovery.mods.values() { for m in self.discovery.mods.values() {
lines.push(format!( lines.push(format!(
@@ -1358,12 +1362,16 @@ impl Host {
.collect() .collect()
} }
fn open_diagnostics(&mut self) { fn open_diagnostics(&mut self) {
if self.screen == Screen::Diagnostics {
return;
}
self.diagnostic_mod = if self.screen == Screen::Mods && self.selected >= 2 { self.diagnostic_mod = if self.screen == Screen::Mods && self.selected >= 2 {
self.mod_ids().get(self.selected - 2).cloned() self.mod_ids().get(self.selected - 2).cloned()
} else { } else {
None None
}; };
self.diagnostic_return = self.screen; self.diagnostic_return = self.screen;
self.diagnostic_selection = self.selected;
self.go(Screen::Diagnostics); self.go(Screen::Diagnostics);
self.play_ui_sound("FX_SWEP"); self.play_ui_sound("FX_SWEP");
} }
@@ -1727,8 +1735,11 @@ impl Host {
}; };
if next != n { if next != n {
self.pending.swap(n, next); self.pending.swap(n, next);
// Keep the selected mod under the cursor as the order changes. self.message = format!(
self.selected = next + 2; "Load priority {} / {}; later overrides win",
next + 1,
self.pending.len()
);
self.play_ui_sound("FX_SWEP"); self.play_ui_sound("FX_SWEP");
} }
} }
@@ -1818,7 +1829,12 @@ impl Host {
self.go(self.previous) self.go(self.previous)
} }
Screen::Mods if self.world.is_some() => self.go(Screen::Pause), Screen::Mods if self.world.is_some() => self.go(Screen::Pause),
Screen::Diagnostics => self.go(self.diagnostic_return), Screen::Diagnostics => {
self.go(self.diagnostic_return);
self.selected = self
.diagnostic_selection
.min(self.rows().len().saturating_sub(1));
}
Screen::Bindings => { Screen::Bindings => {
self.awaiting_binding = None; self.awaiting_binding = None;
self.save_config()?; self.save_config()?;
@@ -6767,6 +6783,70 @@ mod tests {
assert_eq!(host.slots(), vec![0, 1]); assert_eq!(host.slots(), vec![0, 1]);
} }
#[test] #[test]
fn mod_diagnostics_reopening_preserves_parent_and_selected_row() {
let (_dir, mut host) = ui_fixture();
install_dependency_fixture(&mut host);
host.go(Screen::Mods);
host.selected = 3;
host.open_diagnostics();
assert!(host.screen == Screen::Diagnostics);
assert_eq!(host.diagnostic_mod.as_deref(), Some("dependent"));
host.move_selection(3);
host.open_diagnostics(); // A second D press must not replace its parent.
assert!(host.diagnostic_return == Screen::Mods);
assert_eq!(host.diagnostic_mod.as_deref(), Some("dependent"));
host.back().unwrap();
assert!(host.screen == Screen::Mods);
assert_eq!(host.selected, 3);
host.back().unwrap();
assert!(host.screen == Screen::Main);
host.go(Screen::Options);
host.selected = 8;
host.open_diagnostics();
host.open_diagnostics();
host.back().unwrap();
assert!(host.screen == Screen::Options);
assert_eq!(host.selected, 8);
}
#[test]
fn mod_rows_and_cursor_stay_fixed_through_dependency_enable_disable_and_priority() {
let (_dir, mut host) = ui_fixture();
install_dependency_fixture(&mut host);
host.go(Screen::Mods);
let installed = host.mod_ids();
let row = installed.iter().position(|id| id == "dependent").unwrap() + 2;
host.selected = row;
host.activate().unwrap();
assert_eq!(host.mod_ids(), installed);
assert_eq!(host.selected, row);
assert_eq!(host.pending, vec!["dependency", "dependent"]);
host.adjust(-1);
assert_eq!(host.pending, vec!["dependent", "dependency"]);
assert_eq!(host.mod_ids(), installed);
assert_eq!(host.selected, row);
host.activate().unwrap();
assert!(host.screen == Screen::ConfirmDisable);
host.back().unwrap();
assert!(host.screen == Screen::Mods);
assert_eq!(host.selected, row);
host.activate().unwrap();
host.selected = 0;
host.activate().unwrap();
assert!(host.screen == Screen::Mods);
assert!(host.pending.is_empty());
assert_eq!(host.mod_ids(), installed);
assert_eq!(host.selected, row);
host.selected = 2;
host.activate().unwrap();
host.activate().unwrap(); // Standalone dependency disables immediately.
assert!(host.screen == Screen::Mods);
assert!(host.pending.is_empty());
assert_eq!(host.selected, 2);
assert_eq!(host.mod_ids(), installed);
}
#[test]
fn selecting_a_mod_selects_its_installed_dependency_without_applying() { fn selecting_a_mod_selects_its_installed_dependency_without_applying() {
let (_dir, mut host) = ui_fixture(); let (_dir, mut host) = ui_fixture();
install_dependency_fixture(&mut host); install_dependency_fixture(&mut host);
@@ -7289,9 +7369,15 @@ mod tests {
.unwrap() .unwrap()
+ 2; + 2;
capture(&mut host, "17-mod-manager-dependencies"); capture(&mut host, "17-mod-manager-dependencies");
let mod_rows = host.mod_ids();
let mod_selection = host.selected;
host.open_diagnostics(); host.open_diagnostics();
capture(&mut host, "18-mod-details"); capture(&mut host, "18-mod-details");
host.open_diagnostics();
host.back().unwrap(); host.back().unwrap();
assert!(host.screen == Screen::Mods);
assert_eq!(host.mod_ids(), mod_rows);
assert_eq!(host.selected, mod_selection);
host.selected = host host.selected = host
.mod_ids() .mod_ids()
.iter() .iter()
@@ -7302,6 +7388,7 @@ mod tests {
assert!(host.screen == Screen::ConfirmDisable); assert!(host.screen == Screen::ConfirmDisable);
capture(&mut host, "19-mod-disable-confirmation"); capture(&mut host, "19-mod-disable-confirmation");
host.back().unwrap(); host.back().unwrap();
assert_eq!(host.selected, mod_selection);
host.go(Screen::Missions); host.go(Screen::Missions);
host.selected = host host.selected = host
.missions() .missions()