From c80f5e213650727043421252dd468127e9011708 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sun, 23 Aug 2026 19:03:09 +0200 Subject: [PATCH] fix(ui): restore staged loading progress Correct raw 1000:531e's Pascal-order BitBlt to destination (202,328), height 13, and width (stage-1)*7 from original DAT994. Render stages 2 through 35 over DAT995, add deterministic loading screenshots, and update the C harness and evidence ledger for the corrected 237x13 source. Test Plan: - bash original/tools/test_reconstructed_c.sh - python3 original/tools/audit_reconstruction.py --require-complete - cargo test --all-targets - cargo clippy --all-targets --all-features -- -D warnings - rumdl check original/C_RECONSTRUCTION_FINAL_AUDIT.md tdkpin-rs/CHANGELOG.md tdkpin-rs/RECONSTRUCTION.md tdkpin-rs/README.md - cargo run --quiet -- --simulate loading --at 0.55 --screenshot /tmp/tdkpin-loading-mid.png - git diff --check --- original/C_RECONSTRUCTION_FINAL_AUDIT.md | 4 ++ original/FUNCTION_RECONSTRUCTION.tsv | 2 +- original/reconstructed/tdkpin_game_assets.c | 12 ++--- .../reconstructed/tests/test_game_assets.c | 10 ++-- tdkpin-rs/CHANGELOG.md | 3 ++ tdkpin-rs/README.md | 4 +- tdkpin-rs/RECONSTRUCTION.md | 2 +- tdkpin-rs/src/app.rs | 47 ++++++++++++++++++- tdkpin-rs/src/assets.rs | 3 ++ tdkpin-rs/src/main.rs | 8 ++-- tdkpin-rs/src/simulation.rs | 14 ++++-- 11 files changed, 83 insertions(+), 26 deletions(-) diff --git a/original/C_RECONSTRUCTION_FINAL_AUDIT.md b/original/C_RECONSTRUCTION_FINAL_AUDIT.md index 2af06a2..6f348dd 100644 --- a/original/C_RECONSTRUCTION_FINAL_AUDIT.md +++ b/original/C_RECONSTRUCTION_FINAL_AUDIT.md @@ -78,6 +78,10 @@ ledger gates below still pass. Raw `1000:c79c` also confirms that the response reads fixed candidate slot 1 at `SS:...d8a2`; later nearer candidates are stored in subsequent 0x34-byte slots but do not replace the applied record. A focused two-candidate harness now seals that record-order behavior. +Raw `1000:531e` further corrects the Pascal-order BitBlt interpretation: the +DAT994 progress strip is drawn at `(202,328)` with height 13 and width +`(stage-1)*7`, rather than the impossible historical 328x202 read from a +237x13 bitmap. The focused loading-stage harness seals stages 1, 35, and 0. ## Reproducible gates diff --git a/original/FUNCTION_RECONSTRUCTION.tsv b/original/FUNCTION_RECONSTRUCTION.tsv index 642dc5c..16204af 100644 --- a/original/FUNCTION_RECONSTRUCTION.tsv +++ b/original/FUNCTION_RECONSTRUCTION.tsv @@ -6,7 +6,7 @@ address raw_name segment body_bytes status reconstructed_name module evidence no 1000:01d3 FUN_1000_01d3 Code1 155 verified tdkpin_main_window_construct reconstructed/tdkpin_game_windows.c disassembly 1000:01d3-026d; verified Borland constructor and TWindow base; USER GetSystemMetrics; reconstructed/tests/test_game_windows.c Exact 14-byte far-constructor frame, null base VMT, 640x480 geometry, signed divide-by-two screen centering, style 0x86ca0000 replacement, returned far object, and constructor-entry failure are represented and tested. 1000:0270 FUN_1000_0270 Code1 20357 verified tdkpin_setup_game_window reconstructed/tdkpin_game_setup.c disassembly 1000:0270-51f4; generated typed collision data checked against TDKPIN_GHIDRA_COMPLETE_RAW.c; verified setup dependencies; exact 20 quad calls; reconstructed/tests/test_game_setup.c; tools/generate_game_setup_collision_data.py --check Exact far-window RETF-4 ABI, low-memory message/close/RuntimeExit branch, ObjectWindows/cursor/palette setup, tick seed, all globals/receiver/player defaults, milli/pixel state, executable-relative INI-name construction, clamped language/speed configuration, timer creation/failure-without-close path, 177 ordered typed collision calls including preliminary 169/170 then final overwrite, twenty word quads, and 175 active/contact/zero mirrors are represented; success, low-memory, timer-failure, call-order, table-boundary, and mirror tests pass. 1000:51f5 FUN_1000_51f5 Code1 297 verified tdkpin_asset_window_destruct reconstructed/tdkpin_asset_cleanup.c disassembly 1000:51f5-531d; verified sound/message/TWindow/Borland dependencies; exact global/window bitmap inventory; reconstructed/tests/test_asset_cleanup.c Exact Wait cursor save/restore, sound unload, bd9/bd8 publication, optional +49 Dispose(mode 1), message drain, deletion order window+52 then 0859,0863,0865,085f,085b,0861,085d, arrays 430b[0..7],4319[1..4],4321[1..9], intentionally stale handles and retained window+54, PostQuitMessage(18), TWindow destructor, erase=true invalidation, derived ownership finish, six-byte cleanup, and 29-handle sequence are tested. -1000:531e FUN_1000_531e Code1 114 verified tdkpin_draw_loading_stage reconstructed/tdkpin_game_assets.c disassembly 1000:531e-538f; exact caller-BP/SS offsets; GDI compatible-DC/select/BitBlt/DeleteObject; reconstructed/tests/test_game_assets.c Exact BP-0c/-04/-02/+0e shared frame contract, byte-stage zero extension, 16-bit (stage-1)*7 wrap, fixed destination 13/y and 328x202 SRCCOPY, select/restore, intentional DeleteObject-on-HDC, four-byte cleanup, and scratch writes are branch-tested. +1000:531e FUN_1000_531e Code1 114 verified tdkpin_draw_loading_stage reconstructed/tdkpin_game_assets.c disassembly 1000:531e-538f; exact caller-BP/SS offsets; GDI compatible-DC/select/BitBlt/DeleteObject; reconstructed/tests/test_game_assets.c Exact BP-0c/-04/-02/+0e shared frame contract, byte-stage zero extension, 16-bit (stage-1)*7 width wrap, fixed destination (202,328), 13-pixel height, DAT994 source origin, SRCCOPY, select/restore, intentional DeleteObject-on-HDC, four-byte cleanup, and scratch writes are branch-tested. 1000:5390 FUN_1000_5390 Code1 1367 verified tdkpin_initialize_assets_and_redraw reconstructed/tdkpin_asset_initializer.c disassembly 1000:5390-58e6; all internal dependencies verified; exact DAT/BITMAP ID sequence and DGROUP/window offsets; reconstructed/tests/test_asset_initializer.c Exact public ten-byte ABI, entry palette select/unrealize/realize and message drain, static helper split at binary phases 53c5/5619, DAT IDs 995,994,998x2,400..407,701..704,801..809,900,997,901,600, BITMAP IDs 500/102/101, progress stages 2..35, sound init, handle publications, loading-surface draw/delete, palette/cursor restoration, already-redrawn palette retention, transient scene composition, optional mask/image ROPs with coordinate+19 dimensions, score factor/product and positive-player redraw, status sentinel/update, scene-ready publication, and all three control-flow phases are represented and tested. 1000:58e7 FUN_1000_58e7 Code1 111 verified tdkpin_palette_change_variant_a reconstructed/tdkpin_game_assets.c disassembly 1000:58e7-5955; exact ObjectWindowsMessage result offsets; USER IsIconic/InvalidateRect/UpdateWindow; reconstructed/tests/test_palette_messages.c Exact zero-wParam reentrancy reset/result zero, active suppression, iconic suppression, non-iconic invalidate/update ordering, result DWORD clear, state publication, uninitialized uint16 suppressed return, and eight-byte cleanup are branch-tested. 1000:5956 FUN_1000_5956 Code1 74 verified tdkpin_palette_change_variant_b reconstructed/tdkpin_game_assets.c disassembly 1000:5956-599f; shared palette-update state and invalidate helper; reconstructed/tests/test_palette_messages.c Exact wParam==0 and inactive conjunction, state set, null-rect/non-erasing invalidate, synchronous update, message result DWORD clear, no-op branches, and eight-byte cleanup are represented and tested. diff --git a/original/reconstructed/tdkpin_game_assets.c b/original/reconstructed/tdkpin_game_assets.c index 6bd71a9..b734666 100644 --- a/original/reconstructed/tdkpin_game_assets.c +++ b/original/reconstructed/tdkpin_game_assets.c @@ -10,9 +10,9 @@ enum { FRAME_PREVIOUS_BITMAP = -0x04, FRAME_LOADING_BITMAP = -0x02, FRAME_DESTINATION_DC = 0x0e, - LOADING_DESTINATION_X = 13, - LOADING_WIDTH = 328, - LOADING_HEIGHT = 202, + LOADING_DESTINATION_X = 202, + LOADING_DESTINATION_Y = 328, + LOADING_HEIGHT = 13, WIN16_SRCCOPY = 0x00cc0020, DGROUP_OVERLAY_BITMAP = 0x0867, WINDOW_HANDLE_OFFSET = 4, @@ -51,12 +51,12 @@ void tdkpin_draw_loading_stage(Win16FarPtr caller_bp, uint8_t stage) HGDIOBJ16 previous = SelectObject16(memory_dc, loading_bitmap); win16_write_u16( frame_field(caller_bp, FRAME_PREVIOUS_BITMAP), 0, previous); - uint16_t wrapped_y = (uint16_t)(((uint16_t)stage - 1) * 7); + uint16_t wrapped_width = (uint16_t)(((uint16_t)stage - 1) * 7); (void)BitBlt16( destination, LOADING_DESTINATION_X, - (INT16)wrapped_y, - LOADING_WIDTH, + LOADING_DESTINATION_Y, + (INT16)wrapped_width, LOADING_HEIGHT, memory_dc, 0, diff --git a/original/reconstructed/tests/test_game_assets.c b/original/reconstructed/tests/test_game_assets.c index 1e82acf..bc9c494 100644 --- a/original/reconstructed/tests/test_game_assets.c +++ b/original/reconstructed/tests/test_game_assets.c @@ -5,7 +5,7 @@ static uint8_t g_stack[0x400]; static unsigned g_step; -static INT16 g_expected_y; +static INT16 g_expected_width; HDC16 CreateCompatibleDC16(HDC16 dc) { @@ -37,8 +37,8 @@ BOOL16 BitBlt16( { assert(++g_step == 3); assert(destination == 0x1111 && source == 0x2222); - assert(destination_x == 13 && destination_y == g_expected_y); - assert(width == 328 && height == 202); + assert(destination_x == 202 && destination_y == 328); + assert(width == g_expected_width && height == 13); assert(source_x == 0 && source_y == 0); assert(raster_operation == 0x00cc0020); return 1; @@ -50,14 +50,14 @@ BOOL16 DeleteObject16(HGDIOBJ16 object) return 1; } -static void run_stage(uint8_t stage, INT16 expected_y) +static void run_stage(uint8_t stage, INT16 expected_width) { memset(g_stack, 0, sizeof(g_stack)); Win16FarPtr frame = win16_make_far_pointer(0x6000, 0x100); win16_write_u16(frame, 0x0e, 0x1111); win16_write_u16(frame, 0xfffe, 0x3333); g_step = 0; - g_expected_y = expected_y; + g_expected_width = expected_width; tdkpin_draw_loading_stage(frame, stage); assert(g_step == 5); assert(win16_read_u16(win16_far_add_offset(frame, 0xfff4)) == 0x2222); diff --git a/tdkpin-rs/CHANGELOG.md b/tdkpin-rs/CHANGELOG.md index 5103902..593e8d8 100644 --- a/tdkpin-rs/CHANGELOG.md +++ b/tdkpin-rs/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to ### Fixed +- Restore the staged loading presentation by blitting original DAT994 over + DAT995 at `(202,328)`, height 13 and clipped width `(stage-1)*7`; correct the + previously impossible Pascal-order C interpretation of a 328x202 blit. - Restore the original idle/attract timer animation: target and word-quad chases, magnetic flashes, five-record strip, four-point chase, DAT801-809 item progression, and the circular `WELCOME TO THE MACHINE` BITMAP500 diff --git a/tdkpin-rs/README.md b/tdkpin-rs/README.md index b54116e..de89a0a 100644 --- a/tdkpin-rs/README.md +++ b/tdkpin-rs/README.md @@ -44,8 +44,8 @@ cargo run -- --simulate claw-6 --at 0.15 \ ``` Use `--step N` instead of `--at SECONDS` to reproduce one exact update. The -available scenarios are `attract`, `autoplay`, `launcher`, `flippers`, `panel`, -`targets`, `claw-1`, `claw-6`, `claw-7`, and `claw-18`; `--seed N` fixes +available scenarios are `loading`, `attract`, `autoplay`, `launcher`, `flippers`, +`panel`, `targets`, `claw-1`, `claw-6`, `claw-7`, and `claw-18`; `--seed N` fixes random choices. `autoplay` charges each ball and operates the flippers from live ball position for long end-to-end validation runs. diff --git a/tdkpin-rs/RECONSTRUCTION.md b/tdkpin-rs/RECONSTRUCTION.md index e03aeb5..4afed5d 100644 --- a/tdkpin-rs/RECONSTRUCTION.md +++ b/tdkpin-rs/RECONSTRUCTION.md @@ -19,7 +19,7 @@ implementation. | Subsystem | Rust status | Evidence and boundary | | --- | --- | --- | -| Artwork | Exact | All 34 custom DIB images, three standard bitmaps, icon, and palette derivatives are preserved in `assets/original/`. The game uses the original 640x460 table, loading, help, ball, wheel, robot, plunger, media, and diamond frames. | +| Artwork | Exact | All 34 custom DIB images, three standard bitmaps, icon, and palette derivatives are preserved in `assets/original/`. The game uses the original 640x460 table, help, ball, wheel, robot, plunger, media, and item frames. Loading presents DAT995 and advances the original DAT994 strip at raw `1000:531e` destination `(202,328)`, height 13, and clipped width `(stage-1)*7` through stages 2-35. | | Audio | Exact samples and recovered dispatch | All 16 mono PCM WAV resources are embedded unchanged. Playback is emitted at the reconstructed call sites, including multi-sound bank completions and nudge-then-tilt ordering. WAV 2022 is loaded by the original generic resource loop but has no playback call and is therefore never played by Rust. | | Help and languages | Exact | Original resource images 1001-1005 are displayed directly. | | Playfield collision layout | Recovered | All 109 active type-2 line objects and 40 static active type-1 circles are transcribed from the original 175-object registration table. The registration routine converts its sideways inputs with `screen = (y, x - 20)` and accumulates explicitly relative objects. Every static record retains its `+0x49` layer mask; the scanner selects layer 1 below the old Y value 250,000 and layer 2 at or above it, while mask 3 records remain shared. Before detection, the raw `1000:9b69` predicted position must lie in the record bounds derived with the registered five-pixel ball margin. Type-2 records retain every recovered Real48 normal/tangent response pair and registered one-sided orientation. Type-1 records retain their swept-circle radius, radial rebound, tangent coupling, and bumper kick. Detection retains unresolved normal/material candidates so later scan-time motion changes can participate in the selected response. Raw stack slot `SS:...d8a2` makes the first detected record ID win; later nearer candidates are stored but never applied, and both C/Rust have two-candidate regression coverage for this quirk. Each flipper uses its exact two line records plus moving tip circle in both positions. Moving-flipper contact ports `1000:7ed9` rather than fitting live samples: delta-specific pivots/edges, integer cross gates, radial/penetration calculations, response-record gain, and position/velocity publication are tested against all four C harness directions and the raised release geometry. Object 174 is overwritten with the live first ball and Rust handles its ball-to-ball role directly. | diff --git a/tdkpin-rs/src/app.rs b/tdkpin-rs/src/app.rs index 126cba0..c9b5f85 100644 --- a/tdkpin-rs/src/app.rs +++ b/tdkpin-rs/src/app.rs @@ -113,6 +113,7 @@ pub struct App { return_screen: Screen, game: Option, setting_row: usize, + loading_started: f64, loading_until: f64, last_help_click: f64, message: String, @@ -129,6 +130,7 @@ impl App { let saved = persistence.load(); let render_target = render_target(640, 460); render_target.texture.set_filter(FilterMode::Nearest); + let loading_started = get_time(); Self { assets, persistence, @@ -138,7 +140,8 @@ impl App { return_screen: Screen::Attract, game: None, setting_row: 0, - loading_until: get_time() + 1.1, + loading_started, + loading_until: loading_started + 1.1, last_help_click: -1.0, message: String::new(), message_until: 0.0, @@ -182,6 +185,14 @@ impl App { } } + pub fn set_simulation_loading(&mut self, steps: u64) { + self.game = None; + self.screen = Screen::Loading; + let step = u32::try_from(steps).unwrap_or(u32::MAX); + self.loading_started = get_time() - f64::from(step) / 120.0; + self.loading_until = get_time() + 60.0; + } + pub fn render_simulation(&self) { self.draw_logical(); } @@ -431,7 +442,7 @@ impl App { clear_background(BLACK); match self.screen { - Screen::Loading => draw_texture(&self.assets.loading, 0.0, 0.0, WHITE), + Screen::Loading => self.draw_loading(), Screen::Help => { let index = Language::ALL .iter() @@ -525,6 +536,28 @@ impl App { self.draw_intro_marquee(); } + #[allow(clippy::cast_precision_loss)] + fn draw_loading(&self) { + draw_texture(&self.assets.loading, 0.0, 0.0, WHITE); + let stage = Self::loading_stage_at(get_time() - self.loading_started); + let width = i32::from(stage.saturating_sub(1)) * 7; + draw_texture_region( + &self.assets.loading_progress, + 202.0, + 328.0, + width.min(237) as f32, + 13.0, + 0.0, + 0.0, + ); + } + + #[allow(clippy::cast_possible_truncation)] + fn loading_stage_at(elapsed: f64) -> u8 { + let progress = (elapsed / 1.1).clamp(0.0, 0.999_999); + u8::try_from(2 + (progress * 34.0).floor() as i32).unwrap_or(35) + } + fn draw_intro_marquee(&self) { let Some(source_x) = self.attract.marquee_source_x() else { return; @@ -1211,6 +1244,16 @@ mod tests { } } + #[test] + fn loading_progress_covers_original_stages_two_through_thirty_five() { + assert_eq!(App::loading_stage_at(-1.0), 2); + assert_eq!(App::loading_stage_at(0.0), 2); + assert_eq!(App::loading_stage_at(1.1 / 34.0), 3); + assert_eq!(App::loading_stage_at(1.09), 35); + assert_eq!(App::loading_stage_at(99.0), 35); + assert_eq!(i32::from(App::loading_stage_at(99.0) - 1) * 7, 238); + } + #[test] fn attract_phases_follow_the_reconstructed_tick_quotients() { let at = |tick| AttractAnimation { diff --git a/tdkpin-rs/src/assets.rs b/tdkpin-rs/src/assets.rs index 0fba435..e8b8839 100644 --- a/tdkpin-rs/src/assets.rs +++ b/tdkpin-rs/src/assets.rs @@ -7,6 +7,7 @@ pub struct Assets { pub active_table: Texture2D, pub inactive_table: Texture2D, pub loading: Texture2D, + pub loading_progress: Texture2D, pub help: [Texture2D; 5], pub media: [Texture2D; 4], pub diamond: [Texture2D; 9], @@ -29,6 +30,7 @@ impl Assets { let active_table = texture(include_bytes!("../assets/original/images/dat_00997.png")); let inactive_table = texture(include_bytes!("../assets/original/images/dat_00998.png")); let loading = texture(include_bytes!("../assets/original/images/dat_00995.png")); + let loading_progress = texture(include_bytes!("../assets/original/images/dat_00994.png")); let help = [ texture(include_bytes!("../assets/original/images/dat_01001.png")), texture(include_bytes!("../assets/original/images/dat_01002.png")), @@ -146,6 +148,7 @@ impl Assets { active_table, inactive_table, loading, + loading_progress, help, media, diamond, diff --git a/tdkpin-rs/src/main.rs b/tdkpin-rs/src/main.rs index 5502a92..d222e88 100644 --- a/tdkpin-rs/src/main.rs +++ b/tdkpin-rs/src/main.rs @@ -60,10 +60,10 @@ async fn run_simulation(request: Request) -> Result<(), String> { } if let Some(path) = &request.screenshot { let mut app = App::load().await; - if request.scenario == Scenario::Attract { - app.set_simulation_attract(request.target_step); - } else { - app.set_simulation_game(simulation.game().clone()); + match request.scenario { + Scenario::Loading => app.set_simulation_loading(request.target_step), + Scenario::Attract => app.set_simulation_attract(request.target_step), + _ => app.set_simulation_game(simulation.game().clone()), } app.render_simulation(); next_frame().await; diff --git a/tdkpin-rs/src/simulation.rs b/tdkpin-rs/src/simulation.rs index 131fbaf..35853b6 100644 --- a/tdkpin-rs/src/simulation.rs +++ b/tdkpin-rs/src/simulation.rs @@ -13,6 +13,7 @@ const MAX_SIMULATION_STEPS: u64 = 72_000; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Scenario { + Loading, Attract, Autoplay, Launcher, @@ -28,6 +29,7 @@ pub enum Scenario { impl Scenario { pub const fn name(self) -> &'static str { match self { + Self::Loading => "loading", Self::Attract => "attract", Self::Autoplay => "autoplay", Self::Launcher => "launcher", @@ -47,7 +49,8 @@ impl Scenario { Self::Claw6 => Some(6), Self::Claw7 => Some(7), Self::Claw18 => Some(18), - Self::Attract + Self::Loading + | Self::Attract | Self::Autoplay | Self::Launcher | Self::Flippers @@ -62,6 +65,7 @@ impl FromStr for Scenario { fn from_str(value: &str) -> Result { match value { + "loading" => Ok(Self::Loading), "attract" => Ok(Self::Attract), "autoplay" => Ok(Self::Autoplay), "launcher" => Ok(Self::Launcher), @@ -73,7 +77,7 @@ impl FromStr for Scenario { "claw-7" => Ok(Self::Claw7), "claw-18" => Ok(Self::Claw18), _ => Err(format!( - "unknown scenario {value:?}; expected attract, autoplay, launcher, flippers, panel, targets, claw-1, claw-6, claw-7, or claw-18" + "unknown scenario {value:?}; expected loading, attract, autoplay, launcher, flippers, panel, targets, claw-1, claw-6, claw-7, or claw-18" )), } } @@ -157,7 +161,7 @@ fn seconds_to_step(value: &str) -> Result { } pub const fn usage() -> &'static str { - "Usage:\n tdkpin-rs\n tdkpin-rs --simulate SCENARIO [--at SECONDS | --step N] [--screenshot FILE.png] [--trace FILE.json] [--seed N]\n\nScenarios: attract, autoplay, launcher, flippers, panel, targets, claw-1, claw-6, claw-7, claw-18" + "Usage:\n tdkpin-rs\n tdkpin-rs --simulate SCENARIO [--at SECONDS | --step N] [--screenshot FILE.png] [--trace FILE.json] [--seed N]\n\nScenarios: loading, attract, autoplay, launcher, flippers, panel, targets, claw-1, claw-6, claw-7, claw-18" } #[derive(Debug, Serialize, PartialEq)] @@ -238,7 +242,7 @@ impl Simulation { } else { controls_for(self.scenario, self.step) }; - let events = if self.scenario == Scenario::Attract { + let events = if matches!(self.scenario, Scenario::Loading | Scenario::Attract) { Vec::new() } else { self.game.update(SIMULATION_DT, 3, controls) @@ -332,7 +336,7 @@ impl Simulation { fn controls_for(scenario: Scenario, step: u64) -> Controls { match scenario { - Scenario::Attract => Controls::default(), + Scenario::Loading | Scenario::Attract => Controls::default(), Scenario::Autoplay => unreachable!("autoplay controls need mutable simulation state"), Scenario::Launcher => Controls { launch_down: step < u64::from(SIMULATION_HZ),