From 26b7a9ecf37bfdc0415fcc06a669fc31e148ccf7 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sun, 23 Aug 2026 20:37:45 +0200 Subject: [PATCH] fix(input): match original low-byte key scans The Win16 handlers mask the keyboard scan to its low byte. Main and keypad Enter therefore both produce the right-flipper scan 0x1c, both Ctrl keys produce the left-flipper scan 0x1d, and scan 0x1b is the physical main-keyboard plus/right-bracket position accepted alongside keypad plus. Rust omitted main Enter and scan 0x1b, and incorrectly assigned Right Ctrl to the right flipper. Map the native keys to those recovered scan semantics. Keep A/D, arrows, and Equal only as explicit modern aliases and document the distinction. Test Plan: - `cargo test --workspace --all-targets --all-features` -- 125 passed - `cargo clippy --workspace --all-targets --all-features -- -D warnings` -- passed - `rumdl check --flavor commonmark README.md RECONSTRUCTION.md CHANGELOG.md` -- passed - `git diff --cached --check` -- passed --- tdkpin-rs/CHANGELOG.md | 3 +++ tdkpin-rs/README.md | 6 +++--- tdkpin-rs/RECONSTRUCTION.md | 2 +- tdkpin-rs/src/app.rs | 16 +++++++++++++--- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/tdkpin-rs/CHANGELOG.md b/tdkpin-rs/CHANGELOG.md index 8221ef3..2ea2003 100644 --- a/tdkpin-rs/CHANGELOG.md +++ b/tdkpin-rs/CHANGELOG.md @@ -175,6 +175,9 @@ and this project adheres to and a player's final ball now clear the key latch, release a raised flipper through its normal WAVE 2021 timer edge, and require physical key-up before the same key can raise it again. +- Match the original low-byte keyboard scans: both Ctrl keys operate the left + flipper, both Enter keys operate the right, and the physical `0x1b` + main-keyboard plus/right-bracket position starts or adds players. - Route a single-ball type-3 completion during Tilt through `1000:ae6e` instead of granting a free launcher reset: lock holes consume the ball, while record 148 can still take its required-state special-respawn branch. diff --git a/tdkpin-rs/README.md b/tdkpin-rs/README.md index 2220638..76fa01c 100644 --- a/tdkpin-rs/README.md +++ b/tdkpin-rs/README.md @@ -54,10 +54,10 @@ for long end-to-end validation runs. | Action | Original key | Additional modern key | | --- | --- | --- | -| Start game / add up to 4 players | `+` | `=` uses the same main-keyboard key | +| Start game / add up to 4 players | Keypad `+` or the main `+`/`]` position | `=` is also accepted | | Charge launcher | Hold Down arrow, release to launch | - | -| Left flipper | Left Ctrl | `A` or Left arrow | -| Right flipper | Keypad Enter | Right Ctrl, `D`, or Right arrow | +| Left flipper | Ctrl (left or right) | `A` or Left arrow | +| Right flipper | Enter (main or keypad) | `D` or Right arrow | | Nudge | Space, Left Shift, keypad `3` | Right Shift aliases keypad `3` | | Help | F1 | Enter/Escape returns | | Settings | `TDKPIN.INI` before launch | F10 opens the portable settings screen | diff --git a/tdkpin-rs/RECONSTRUCTION.md b/tdkpin-rs/RECONSTRUCTION.md index 1e1c0b5..8b0dfb7 100644 --- a/tdkpin-rs/RECONSTRUCTION.md +++ b/tdkpin-rs/RECONSTRUCTION.md @@ -29,7 +29,7 @@ implementation. | Numeric scoring | Recovered gameplay values | Static scores come from the initialized 175-object ledger. Dynamic bumper progression, target-bank completion, diamond awards, 10k-160k lock bonuses, 310k transfer, six effect values, multiball mode, and all four media thresholds are transcribed from `1000:b476`, `1000:c4e1`, `1000:bc36`, and live state probes. The central helper suppresses all mutation during Tilt, applies a 2x active-multiball factor, and then independently applies the active ball's 2x factor, allowing the original 4x stack. Lock and effect awards share the original per-player secondary score and display multiplier; the fifth hole transfers and clears it, increments the multiplier, and grants the recovered ball award. Score mutation uses the original 32-bit wrapping behavior; media markers use the binary's signed-high/unsigned-low comparison, and each add operation can advance at most one threshold. Type-4 WAVE 2004, bumper WAVE 2006, and bank-completion WAVE 2017 dispatch before their associated score mutations, leaving a crossed marker's WAVE 2007 as the audible asynchronous sample. Rendering separately follows `1008:0996`'s eight-place 130x40 fields/right edge X=606 and `1008:0b9e`'s strict signed status thresholds; configured-player rows and 5x5 remaining-ball markers retain their exact dimensions and current/noncurrent counts. | | High scores | Recovered visible flow; portable storage | The original 276-byte table is decoded as ten `IWIK`-XOR-obfuscated little-endian scores plus ten 22-byte names. Each player is checked immediately when their own last ball is lost; qualifying scores use the original signed-high/unsigned-low comparison and a `TDK Pinball Player`-prefilled DIALOG 32514 flow with the exact caption, prompt, 21-character limit, OK/Cancel semantics, and allowance for an empty accepted name. The `HighScore` child uses exact DAT993 at 360x300, heading `(180,27)`, name/rank X=30, right-aligned score X=320, and ten 20-pixel rows before play resumes. Persisted updates use portable JSON rather than rewriting the Win16 file. | | Configuration | Recovered behavior; portable follow-up storage | Before portable JSON exists, Rust reads the original case-insensitive `[Settings]` keys `Language`, `Speed`, and singular `Sound` from the executable-adjacent INI, falling back to the embedded distributed TDKPIN.INI. Values retain the Win16 defaults/clamps; the shipped file therefore selects German, speed 3, and sound-on because its plural `Sounds` line does not override singular `Sound`. F12 toggles only runtime playback without UI or persistence. Later explicit F10 changes use the platform user-data directory. | -| Windows UI shell | Original gameplay surface with native host shell | Win16 window ownership and GDI calls are replaced by a fixed native window, but the visible game, help resources, automatic per-player high-score flow, original keys, and 640x460 logical pixels are retained. Idle mode advances on the selected detail callback and reproduces `idle_transition_tick`: target/word-quad/record-strip chases, magnetic flashes, four-point chase, DAT801-809 item progression, and the circular BITMAP500 `WELCOME TO THE MACHINE` marquee composited over exact DAT997 regions. F2/F3 remain unassigned as in normal original operation; optional portable settings and table viewers use F10/F9 and do not replace gameplay input. | +| Windows UI shell | Original gameplay surface with native host shell | Win16 window ownership and GDI calls are replaced by a fixed native window, but the visible game, help resources, automatic per-player high-score flow, original keys, and 640x460 logical pixels are retained. Idle mode advances on the selected detail callback and reproduces `idle_transition_tick`: target/word-quad/record-strip chases, magnetic flashes, four-point chase, DAT801-809 item progression, and the circular BITMAP500 `WELCOME TO THE MACHINE` marquee composited over exact DAT997 regions. The low-byte scan aliases are preserved: both Ctrl keys map left, both Enter keys map right, and keypad plus or the physical main-keyboard `0x1b` plus/right-bracket position adds players. F2/F3 remain unassigned as in normal original operation; optional portable settings and table viewers use F10/F9 and do not replace gameplay input. | ## Extracted asset inventory diff --git a/tdkpin-rs/src/app.rs b/tdkpin-rs/src/app.rs index fd8c9b4..c281864 100644 --- a/tdkpin-rs/src/app.rs +++ b/tdkpin-rs/src/app.rs @@ -11,6 +11,7 @@ use std::path::Path; const WIDTH: f32 = 640.0; const HEIGHT: f32 = 460.0; const DETAIL_TIMER_SECONDS: [f32; 5] = [0.050, 0.040, 0.030, 0.020, 0.010]; +const ADD_PLAYER_KEYS: [KeyCode; 3] = [KeyCode::KpAdd, KeyCode::RightBracket, KeyCode::Equal]; const TARGET_POSITIONS: [[(f32, f32); 5]; 6] = [ [(9.0, 41.0), (23.0, 11.0), (56.0, 14.0), (63.0, 47.0), (35.0, 63.0)], [(11.0, 45.0), (18.0, 13.0), (52.0, 11.0), (63.0, 42.0), (39.0, 64.0)], @@ -45,6 +46,10 @@ fn bumper_value_indicator_count(value: u32) -> usize { .min(BUMPER_VALUE_REGIONS.len()) } +fn add_player_pressed() -> bool { + ADD_PLAYER_KEYS.into_iter().any(is_key_pressed) +} + fn visible_score_digits(mut value: u32) -> Vec<(u8, u8)> { let mut digits = Vec::with_capacity(8); for position in 0..8 { @@ -320,7 +325,7 @@ impl App { fn update_attract(&mut self) { self.attract .update(get_frame_time(), self.saved.settings.speed); - if is_key_pressed(KeyCode::KpAdd) || is_key_pressed(KeyCode::Equal) { + if add_player_pressed() { self.game = Some(Game::new(1)); self.screen = Screen::Playing; self.assets.play(2001, self.sounds_enabled); @@ -328,7 +333,7 @@ impl App { } fn update_game(&mut self) { - if (is_key_pressed(KeyCode::KpAdd) || is_key_pressed(KeyCode::Equal)) + if add_player_pressed() && self .game .as_mut() @@ -337,10 +342,11 @@ impl App { self.assets.play(2001, self.sounds_enabled); } let left_flipper = is_key_down(KeyCode::LeftControl) + || is_key_down(KeyCode::RightControl) || is_key_down(KeyCode::A) || is_key_down(KeyCode::Left); let right_flipper = is_key_down(KeyCode::KpEnter) - || is_key_down(KeyCode::RightControl) + || is_key_down(KeyCode::Enter) || is_key_down(KeyCode::D) || is_key_down(KeyCode::Right); let nudge = if is_key_pressed(KeyCode::LeftShift) { @@ -1360,6 +1366,10 @@ mod tests { assert_eq!(bumper_value_indicator_count(2_000), 1); assert_eq!(bumper_value_indicator_count(6_000), 5); assert_eq!(bumper_value_indicator_count(u32::MAX), 5); + assert_eq!( + ADD_PLAYER_KEYS, + [KeyCode::KpAdd, KeyCode::RightBracket, KeyCode::Equal] + ); } #[test]