fix(input): preserve Win16 flipper key latches

Normal ball-end handling keeps the original flipper key bytes and geometry
unless the current player has finished. Rust instead reset both flippers on
every drain, skipped the normal WAVE 2021 release edge, and treated a key still
held after Tilt as a fresh press once Tilt cleared.

Preserve flipper state across ordinary drains. Tilt and a player's final ball
now clear only the key flags, let the timer epilogue lower raised geometry, and
latch each physical key until key-up before accepting another press. This
matches the original WM_KEYDOWN/WM_KEYUP lifetime while retaining modern
per-frame input sampling.

Test Plan:
- `cargo test --workspace --all-targets --all-features` -- 119 passed
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` -- passed
- `rumdl check --flavor commonmark RECONSTRUCTION.md CHANGELOG.md` -- passed
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-23 20:04:07 +02:00
parent 25019514c7
commit 3859498172
3 changed files with 73 additions and 7 deletions
+68 -6
View File
@@ -347,6 +347,7 @@ pub struct Game {
player_entry: PlayerEntry,
random: BorlandRandom,
pending_flipper_edges: [i8; 2],
flipper_release_latch: [bool; 2],
record_contacts: [u16; 176],
trigger_flags: [bool; 176],
object_active: [bool; 176],
@@ -390,6 +391,7 @@ impl Game {
player_entry: PlayerEntry::Open,
random: BorlandRandom::new(seed),
pending_flipper_edges: [0; 2],
flipper_release_latch: [false; 2],
record_contacts: [0; 176],
trigger_flags: [false; 176],
object_active: initial_object_activity(),
@@ -478,8 +480,22 @@ impl Game {
self.last_collision_id = None;
self.claw_frame_seconds = DETAIL_TIMER_SECONDS[usize::from(detail.clamp(1, 5) - 1)];
let mut events = Vec::new();
self.flipper_inputs.left_raised = controls.left_flipper && !self.tilted;
self.flipper_inputs.right_raised = controls.right_flipper && !self.tilted;
if !controls.left_flipper {
self.flipper_release_latch[0] = false;
} else if self.tilted {
self.flipper_release_latch[0] = true;
}
if !controls.right_flipper {
self.flipper_release_latch[1] = false;
} else if self.tilted {
self.flipper_release_latch[1] = true;
}
self.flipper_inputs.left_raised = controls.left_flipper
&& !self.tilted
&& !self.flipper_release_latch[0];
self.flipper_inputs.right_raised = controls.right_flipper
&& !self.tilted
&& !self.flipper_release_latch[1];
if self.ball.in_launcher && !self.tilted {
if controls.launch_down {
@@ -716,6 +732,8 @@ impl Game {
let threshold = self.random.below(10) + 30;
if threshold < self.tilt_counter {
self.tilted = true;
self.flipper_release_latch[0] |= self.flipper_inputs.left_raised;
self.flipper_release_latch[1] |= self.flipper_inputs.right_raised;
self.flipper_inputs = Flippers::default();
self.pending_flipper_edges.fill(0);
events.push(Event::Tilt);
@@ -2073,12 +2091,12 @@ impl Game {
events.push(Event::Sound(2008));
if let Some(score) = high_score_candidate {
events.push(Event::HighScoreCandidate(score));
self.flipper_release_latch[0] |= self.flipper_inputs.left_raised;
self.flipper_release_latch[1] |= self.flipper_inputs.right_raised;
self.flipper_inputs = Flippers::default();
}
self.tilted = false;
self.tilt_counter = 0;
self.flipper_inputs = Flippers::default();
self.flippers = Flippers::default();
self.pending_flipper_edges.fill(0);
self.secondary_ball = None;
self.score_mode = ScoreMode::Normal;
self.launcher_charge = 0.0;
@@ -4176,10 +4194,54 @@ mod tests {
game.ball.position = vec2(157.0, 454.0);
game.ball.velocity = vec2(0.0, 200.0);
game.update(0.030, 3, Controls::default());
let held = Controls {
left_flipper: true,
..Controls::default()
};
game.update(0.030, 3, held);
assert!(!game.tilted);
assert!(game.ball.in_launcher);
assert!(game.flipper_release_latch[0]);
game.update(0.0, 3, held);
assert!(!game.flipper_inputs.left_raised);
game.update(0.0, 3, Controls::default());
game.update(0.0, 3, held);
assert!(game.flipper_inputs.left_raised);
}
#[test]
fn ball_end_preserves_or_releases_flippers_like_key_message_latches() {
let mut live_player = Game::new(1);
live_player.flipper_inputs.left_raised = true;
live_player.flippers.left_raised = true;
live_player.drain(&mut Vec::new());
assert!(live_player.flipper_inputs.left_raised);
assert!(live_player.flippers.left_raised);
let mut finished_player = Game::new(2);
finished_player.players[0].balls = 1;
finished_player.flipper_inputs.left_raised = true;
finished_player.flippers.left_raised = true;
finished_player.drain(&mut Vec::new());
assert!(!finished_player.flipper_inputs.left_raised);
assert!(finished_player.flippers.left_raised);
assert!(finished_player.flipper_release_latch[0]);
let mut events = Vec::new();
finished_player.timer_tick(0.030, 0, &mut events);
assert!(!finished_player.flippers.left_raised);
assert_eq!(events, [Event::FlipperMove, Event::Sound(2021)]);
let held = Controls {
left_flipper: true,
..Controls::default()
};
finished_player.update(0.0, 3, held);
assert!(!finished_player.flipper_inputs.left_raised);
finished_player.update(0.0, 3, Controls::default());
finished_player.update(0.0, 3, held);
assert!(finished_player.flipper_inputs.left_raised);
}
#[test]