From 4b1edefeece391fd71e54a479afcf732e7d752a6 Mon Sep 17 00:00:00 2001 From: raptorreplica Date: Sun, 11 Oct 2026 16:25:59 +1100 Subject: [PATCH] Fix diagnostics back navigation and keep mod selection rows stable --- RustRaptor/docs/menu-validation.md | 8 ++ RustRaptor/docs/resume-checkpoint.md | 36 ++++---- RustRaptor/src/desktop.rs | 133 ++++++++++++++++++++++----- 3 files changed, 136 insertions(+), 41 deletions(-) diff --git a/RustRaptor/docs/menu-validation.md b/RustRaptor/docs/menu-validation.md index 97065155..5cb36170 100644 --- a/RustRaptor/docs/menu-validation.md +++ b/RustRaptor/docs/menu-validation.md @@ -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/`. +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. Run the local audit from `RustRaptor/`: diff --git a/RustRaptor/docs/resume-checkpoint.md b/RustRaptor/docs/resume-checkpoint.md index e10b345d..d45a6fe4 100644 --- a/RustRaptor/docs/resume-checkpoint.md +++ b/RustRaptor/docs/resume-checkpoint.md @@ -24,10 +24,10 @@ worker from its existing outputs before submitting another generation request. ## Next user fixes -1. Mod/save diagnostics: Escape can get trapped after details are reopened from - diagnostics, replacing its parent with itself. Preserve parent and selection. -2. Keep installed mod rows stable when enabled/disabled. Load priority must be - separate from the displayed order; dependencies and confirmation stay intact. +1. Mod/save diagnostics: parent and selection are now preserved when details + are reopened. The Escape regression passes; full screenshot audit is running. +2. Installed mod rows now stay in ID order through toggles and priority changes, + with dependency and confirmation behavior preserved. Navigation tests pass. 3. Default Forward Guns still appear to miss miscellaneous buildings/cars. 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 | | --- | --- | -| hd_coordinator | Sole global manifest/ledger owner; register fresh approved29boss frames, keepMole9hold; audit all recoveredrequests before resuming | -| hd_full_review | Terrain corpus49/66pagesreviewed; pages50–66remain;48terrainframeholdstaysuntilfullreview | -| hd_terrain_render | Frozen compositor SHA b30b4a65…;29bossframesapproved,Mole9held;8newportraitscreenssaved,7/fullboardinspectionremain | -| campaign_integration | 34effect/projectilefamilies303IDsraw; detailed RESTART.md preserved; severalgeometry/brightness/layout corrections and independentreviewsremain | -| campaign_release_review | Terrain4queue84jobs1344IDs; recover currentraw/contextcandidates andholds | -| hd_dark_player | Equipment/HUDdigits, acceptedandheldfamilies; reconcilecurrentmanifest before newgeneration | -| hd_enemies_01 | Enemy01queue139IDs; sourcegeometryholds,SHIP25connectionfailure,APE4service-refusalpending | -| hd_terrain_set_1 | Terrain1queue42jobs672IDs; first16registered; recover alreadycompleted103e0output andsubsequentcandidateholds | -| hd_terrain_set_2 | Terrain2queue90jobs1437IDs; continue savedcanonicalchunks andall-contextreview | -| iron_coast_2 | Frozenenemy03firstdrafts59families138IDs; recovered45familyenemy02queuecontinues, old11familiesfrozen | -| iron_coast_3 | Boss01queue26families83IDs; recovered11completedatlasfamiliesneedbindings/QA; previous15candidatespreserved | -| iron_coast_4 | Boss02queue27families83IDs; approvedsubset/corrections,connectionfailurecooldowns | -| iron_coast_5 | Boss03complete25families82IDs/328scalechecks; equipment8reviewpending; temporarygroundgunbugassignment | -| iron_coast_6 | Terrain3queue68jobs1082IDs, withmiddleandlateshards; preserve strictdestroyedgeometryholds | +| hd_coordinator | Sole manifest/ledger owner; 29 boss and 48 terrain frames await final registration checks; retain Mole9 hold | +| 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…; 29 boss frames approved, Mole9 held; all eight new portrait screenshots approved | +| campaign_integration | 34 effect/projectile families, 303 IDs have raw outputs; geometry, brightness and layout corrections continue | +| campaign_release_review | Terrain 4: 84 jobs, 1,344 IDs; 64 candidates saved, half await further visual/Host QA | +| hd_dark_player | Equipment and HUD digits; reconcile accepted frames, preserve pending Death Ray/Energy review | +| hd_enemies_01 | Enemy 1: 139 IDs; source geometry holds, SHIP25 connection failures and APE service refusal preserved | +| 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 | Terrain 2: 90 jobs, 1,437 IDs; continue canonical chunks and full neighboring-context review | +| iron_coast_2 | Enemy 3: 59 families, 138 raw IDs; recovered 45-family enemy 2 queue continues, old 11 families frozen | +| iron_coast_3 | Boss 1: 26 families, 83 IDs; 11 recovered atlas families need binding/QA; previous 15 candidates preserved | +| iron_coast_4 | Boss 2: 27 families, 83 IDs; continue corrections, complete reviews and respect service cooldowns | +| 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 | Terrain 3: 68 jobs, 1,082 IDs with middle/late shards; retain destroyed-state geometry holds | ## Durable worker evidence diff --git a/RustRaptor/src/desktop.rs b/RustRaptor/src/desktop.rs index a727e7bd..c8b94274 100644 --- a/RustRaptor/src/desktop.rs +++ b/RustRaptor/src/desktop.rs @@ -189,6 +189,8 @@ struct Host { awaiting_binding: Option, diagnostic_mod: Option, diagnostic_return: Screen, + diagnostic_selection: usize, + mods_selection: usize, pending_disable: Option, world: Option, cinematic: Option, @@ -333,6 +335,8 @@ impl Host { awaiting_binding: None, diagnostic_mod: None, diagnostic_return: Screen::Main, + diagnostic_selection: 0, + mods_selection: 0, pending_disable: None, world: None, cinematic: None, @@ -373,8 +377,15 @@ impl Host { }) } fn go(&mut self, screen: Screen) { + if self.screen == Screen::Mods { + self.mods_selection = self.selected; + } 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 { self.shop_selling = false; } @@ -905,13 +916,7 @@ impl Host { .collect(), Screen::Mods => { let mut r = vec!["Apply selections".into(), "Rescan installed mods".into()]; - let mut ids = self.pending.clone(); - for id in self.discovery.mods.keys() { - if !ids.contains(id) { - ids.push(id.clone()); - } - } - for id in ids { + for id in self.mod_ids() { if let Some(m) = self.discovery.mods.get(&id) { r.push(format!( "[{}] {} {}", @@ -966,18 +971,9 @@ impl Host { } } fn mod_ids(&self) -> Vec { - let mut ids = self - .pending - .iter() - .filter(|id| self.discovery.mods.contains_key(*id)) - .cloned() - .collect::>(); - for id in self.discovery.mods.keys() { - if !ids.contains(id) { - ids.push(id.clone()); - } - } - ids + // Installed rows stay in ID order. Activation priority is a separate + // pending list, so toggles and dependency selection cannot move rows. + self.discovery.mods.keys().cloned().collect() } fn require_pilot(&self) -> Result<()> { if let Some(error) = &self.pilot_error { @@ -1310,6 +1306,13 @@ impl Host { 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 { @@ -1335,6 +1338,7 @@ impl Host { .collect::>() .join(", ") )); + lines.push(format!("Pending order: {}", self.pending.join(", "))); lines.extend(self.active.registry.overrides.clone()); for m in self.discovery.mods.values() { lines.push(format!( @@ -1358,12 +1362,16 @@ impl Host { .collect() } fn open_diagnostics(&mut self) { + if self.screen == Screen::Diagnostics { + return; + } self.diagnostic_mod = if self.screen == Screen::Mods && self.selected >= 2 { self.mod_ids().get(self.selected - 2).cloned() } else { None }; self.diagnostic_return = self.screen; + self.diagnostic_selection = self.selected; self.go(Screen::Diagnostics); self.play_ui_sound("FX_SWEP"); } @@ -1727,8 +1735,11 @@ impl Host { }; if next != n { self.pending.swap(n, next); - // Keep the selected mod under the cursor as the order changes. - self.selected = next + 2; + self.message = format!( + "Load priority {} / {}; later overrides win", + next + 1, + self.pending.len() + ); self.play_ui_sound("FX_SWEP"); } } @@ -1818,7 +1829,12 @@ impl Host { self.go(self.previous) } 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 => { self.awaiting_binding = None; self.save_config()?; @@ -6767,6 +6783,70 @@ mod tests { assert_eq!(host.slots(), vec![0, 1]); } #[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() { let (_dir, mut host) = ui_fixture(); install_dependency_fixture(&mut host); @@ -7289,9 +7369,15 @@ mod tests { .unwrap() + 2; capture(&mut host, "17-mod-manager-dependencies"); + let mod_rows = host.mod_ids(); + let mod_selection = host.selected; host.open_diagnostics(); capture(&mut host, "18-mod-details"); + host.open_diagnostics(); host.back().unwrap(); + assert!(host.screen == Screen::Mods); + assert_eq!(host.mod_ids(), mod_rows); + assert_eq!(host.selected, mod_selection); host.selected = host .mod_ids() .iter() @@ -7302,6 +7388,7 @@ mod tests { assert!(host.screen == Screen::ConfirmDisable); capture(&mut host, "19-mod-disable-confirmation"); host.back().unwrap(); + assert_eq!(host.selected, mod_selection); host.go(Screen::Missions); host.selected = host .missions()