fix(ui): restore effect and player display assets

Load and render DAT400-407 effect panels with the preserved wheel overlap, and add deterministic effect-7 coverage. Restore exact 1008:0996/0b9e score fields, configured-player rows, current/status indicators, signed status thresholds, and six 5x5 per-player ball-marker slots.

Test Plan:
- cargo test --all-targets
- cargo clippy --all-targets --all-features -- -D warnings
- rumdl check CHANGELOG.md RECONSTRUCTION.md README.md
- cargo run --quiet -- --simulate effect-7 --step 0 --screenshot /tmp/tdkpin-effect7.png
- cargo run --quiet -- --simulate launcher --step 0 --screenshot /tmp/tdkpin-display.png
- git diff --check
This commit is contained in:
2026-08-23 19:21:28 +02:00
parent 0643138c0e
commit c84df73857
7 changed files with 46 additions and 4 deletions
+3
View File
@@ -10,6 +10,9 @@ and this project adheres to
### Fixed ### Fixed
- Load and draw all DAT400-407 player/effect sprites at `(55,79)`, preserving
the original saved 14x14 wheel overlap at `(84,77)`; selected bonuses,
collect-bonus, and release-ball states are now visible.
- Restore display blits from `1008:0996`/`1008:0b9e`: 130x40 score fields at - Restore display blits from `1008:0996`/`1008:0b9e`: 130x40 score fields at
X=478, eight places ending at X=606, exact 139x31 configured-player rows, X=478, eight places ending at X=606, exact 139x31 configured-player rows,
11x11 active/status markers, 5x5 remaining-ball markers for every player, 11x11 active/status markers, 5x5 remaining-ball markers for every player,
+1 -1
View File
@@ -45,7 +45,7 @@ cargo run -- --simulate claw-6 --at 0.15 \
Use `--step N` instead of `--at SECONDS` to reproduce one exact update. The Use `--step N` instead of `--at SECONDS` to reproduce one exact update. The
available scenarios are `loading`, `attract`, `highscores`, `name-entry`, available scenarios are `loading`, `attract`, `highscores`, `name-entry`,
`autoplay`, `launcher`, `flippers`, `panel`, `targets`, `claw-1`, `claw-6`, `effect-7`, `autoplay`, `launcher`, `flippers`, `panel`, `targets`, `claw-1`, `claw-6`,
`claw-7`, and `claw-18`; `--seed N` fixes random choices. `claw-7`, and `claw-18`; `--seed N` fixes random choices.
`autoplay` charges each ball and operates the flippers from live ball position `autoplay` charges each ball and operates the flippers from live ball position
for long end-to-end validation runs. for long end-to-end validation runs.
+1 -1
View File
@@ -19,7 +19,7 @@ implementation.
| Subsystem | Rust status | Evidence and boundary | | 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, 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. | | 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. DAT400-407 expose the current per-player bonus/collect/release effect at `(55,79)` while preserving the saved 14x14 wheel overlap. 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. | | 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. | | 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. | | 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. |
+8
View File
@@ -688,6 +688,14 @@ impl App {
return; return;
}; };
draw_texture(
&self.assets.player_effects[usize::from(game.player_effect())],
55.0,
79.0,
WHITE,
);
self.draw_inactive_table_region(84, 77, 14, 14);
if let Some(frame) = game.panel_frame { if let Some(frame) = game.panel_frame {
self.draw_panel_animation(frame); self.draw_panel_animation(frame);
} else if let Some(state) = game } else if let Some(state) = game
+12
View File
@@ -10,6 +10,7 @@ pub struct Assets {
pub loading_progress: Texture2D, pub loading_progress: Texture2D,
pub highscore_background: Texture2D, pub highscore_background: Texture2D,
pub help: [Texture2D; 5], pub help: [Texture2D; 5],
pub player_effects: [Texture2D; 8],
pub media: [Texture2D; 4], pub media: [Texture2D; 4],
pub diamond: [Texture2D; 9], pub diamond: [Texture2D; 9],
pub wheel: Texture2D, pub wheel: Texture2D,
@@ -40,6 +41,16 @@ impl Assets {
texture(include_bytes!("../assets/original/images/dat_01004.png")), texture(include_bytes!("../assets/original/images/dat_01004.png")),
texture(include_bytes!("../assets/original/images/dat_01005.png")), texture(include_bytes!("../assets/original/images/dat_01005.png")),
]; ];
let player_effects = [
texture(include_bytes!("../assets/original/images/dat_00400.png")),
texture(include_bytes!("../assets/original/images/dat_00401.png")),
texture(include_bytes!("../assets/original/images/dat_00402.png")),
texture(include_bytes!("../assets/original/images/dat_00403.png")),
texture(include_bytes!("../assets/original/images/dat_00404.png")),
texture(include_bytes!("../assets/original/images/dat_00405.png")),
texture(include_bytes!("../assets/original/images/dat_00406.png")),
texture(include_bytes!("../assets/original/images/dat_00407.png")),
];
let media = [ let media = [
texture(include_bytes!("../assets/original/images/dat_00701.png")), texture(include_bytes!("../assets/original/images/dat_00701.png")),
texture(include_bytes!("../assets/original/images/dat_00702.png")), texture(include_bytes!("../assets/original/images/dat_00702.png")),
@@ -153,6 +164,7 @@ impl Assets {
loading_progress, loading_progress,
highscore_background, highscore_background,
help, help,
player_effects,
media, media,
diamond, diamond,
wheel, wheel,
+11
View File
@@ -406,6 +406,11 @@ impl Game {
events events
} }
pub(crate) fn begin_effect_scenario(&mut self, effect: u8) {
self.target_effect = effect.min(7);
self.object_active[usize::from(EFFECT_SENSOR.id)] = self.target_effect != 0;
}
fn fire_launcher(&mut self) { fn fire_launcher(&mut self) {
self.launcher_velocity_milli = self self.launcher_velocity_milli = self
.launcher_velocity_milli .launcher_velocity_milli
@@ -1801,6 +1806,10 @@ impl Game {
self.object_active[usize::from(EFFECT_SENSOR.id)] self.object_active[usize::from(EFFECT_SENSOR.id)]
} }
pub fn player_effect(&self) -> u8 {
self.target_effect.min(7)
}
fn next_claw_terminal_frame(&mut self) -> u8 { fn next_claw_terminal_frame(&mut self) -> u8 {
CLAW_TERMINAL_FRAMES[usize::from(self.random.below(4))] CLAW_TERMINAL_FRAMES[usize::from(self.random.below(4))]
} }
@@ -2450,6 +2459,7 @@ mod tests {
assert!((1..=6).contains(&game.target_effect)); assert!((1..=6).contains(&game.target_effect));
assert!(game.effect_target_active()); assert!(game.effect_target_active());
game.target_effect = 3; game.target_effect = 3;
assert_eq!(game.player_effect(), 3);
game.ball.in_launcher = false; game.ball.in_launcher = false;
game.ball.position = EFFECT_SENSOR.center; game.ball.position = EFFECT_SENSOR.center;
game.check_sensor_objects( game.check_sensor_objects(
@@ -2462,6 +2472,7 @@ mod tests {
assert_eq!(game.secondary_score_display(), 50_000); assert_eq!(game.secondary_score_display(), 50_000);
assert_eq!(game.player().score, 2_000); assert_eq!(game.player().score, 2_000);
assert_eq!(game.target_effect, 0); assert_eq!(game.target_effect, 0);
assert_eq!(game.player_effect(), 0);
assert!(!game.effect_target_active()); assert!(!game.effect_target_active());
} }
+10 -2
View File
@@ -17,6 +17,7 @@ pub enum Scenario {
Attract, Attract,
HighScores, HighScores,
NameEntry, NameEntry,
Effect7,
Autoplay, Autoplay,
Launcher, Launcher,
Flippers, Flippers,
@@ -35,6 +36,7 @@ impl Scenario {
Self::Attract => "attract", Self::Attract => "attract",
Self::HighScores => "highscores", Self::HighScores => "highscores",
Self::NameEntry => "name-entry", Self::NameEntry => "name-entry",
Self::Effect7 => "effect-7",
Self::Autoplay => "autoplay", Self::Autoplay => "autoplay",
Self::Launcher => "launcher", Self::Launcher => "launcher",
Self::Flippers => "flippers", Self::Flippers => "flippers",
@@ -57,6 +59,7 @@ impl Scenario {
| Self::Attract | Self::Attract
| Self::HighScores | Self::HighScores
| Self::NameEntry | Self::NameEntry
| Self::Effect7
| Self::Autoplay | Self::Autoplay
| Self::Launcher | Self::Launcher
| Self::Flippers | Self::Flippers
@@ -75,6 +78,7 @@ impl FromStr for Scenario {
"attract" => Ok(Self::Attract), "attract" => Ok(Self::Attract),
"highscores" => Ok(Self::HighScores), "highscores" => Ok(Self::HighScores),
"name-entry" => Ok(Self::NameEntry), "name-entry" => Ok(Self::NameEntry),
"effect-7" => Ok(Self::Effect7),
"autoplay" => Ok(Self::Autoplay), "autoplay" => Ok(Self::Autoplay),
"launcher" => Ok(Self::Launcher), "launcher" => Ok(Self::Launcher),
"flippers" => Ok(Self::Flippers), "flippers" => Ok(Self::Flippers),
@@ -85,7 +89,7 @@ impl FromStr for Scenario {
"claw-7" => Ok(Self::Claw7), "claw-7" => Ok(Self::Claw7),
"claw-18" => Ok(Self::Claw18), "claw-18" => Ok(Self::Claw18),
_ => Err(format!( _ => Err(format!(
"unknown scenario {value:?}; expected loading, attract, highscores, name-entry, autoplay, launcher, flippers, panel, targets, claw-1, claw-6, claw-7, or claw-18" "unknown scenario {value:?}; expected loading, attract, highscores, name-entry, effect-7, autoplay, launcher, flippers, panel, targets, claw-1, claw-6, claw-7, or claw-18"
)), )),
} }
} }
@@ -169,7 +173,7 @@ fn seconds_to_step(value: &str) -> Result<u64, String> {
} }
pub const fn usage() -> &'static str { 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: loading, attract, highscores, name-entry, 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, highscores, name-entry, effect-7, autoplay, launcher, flippers, panel, targets, claw-1, claw-6, claw-7, claw-18"
} }
#[derive(Debug, Serialize, PartialEq)] #[derive(Debug, Serialize, PartialEq)]
@@ -229,6 +233,9 @@ impl Simulation {
game.target_rotation_state = Some(0); game.target_rotation_state = Some(0);
game.wheel_holes = [true, false, true, false, false]; game.wheel_holes = [true, false, true, false, false];
} }
if scenario == Scenario::Effect7 {
game.begin_effect_scenario(7);
}
let initial_events = scenario let initial_events = scenario
.claw_terminal() .claw_terminal()
.map_or_else(Vec::new, |terminal| game.begin_claw_scenario(terminal)); .map_or_else(Vec::new, |terminal| game.begin_claw_scenario(terminal));
@@ -364,6 +371,7 @@ fn controls_for(scenario: Scenario, step: u64) -> Controls {
| Scenario::Attract | Scenario::Attract
| Scenario::HighScores | Scenario::HighScores
| Scenario::NameEntry | Scenario::NameEntry
| Scenario::Effect7
| Scenario::Panel | Scenario::Panel
| Scenario::Targets | Scenario::Targets
| Scenario::Claw1 | Scenario::Claw1