fix(physics): port the Borland Real48 collision core

Add a bit-exact six-byte Real48 implementation for integer conversion,
rounding, comparison, add/subtract, multiply/divide, and Newton square root.
Use the normalized Borland random-register result and route speed clamps,
segment/circle detection, moving flippers, captures, triggers, and magnetic
fields through the recovered arithmetic instead of host floating formulas.

Preserve Real48 spin per ball and feed it through the original tangent/spin
response, separating zero-spin C fixtures from retained-spin Wine traces.
Replace path-progress selection with surface-distance ordering and implement
dynamic records 174/175, including impulse transfer to the other slot,
normal_velocity-1000 response, and the second post-collision speed clamp.

Test Plan:
- `cargo test --all-targets` -- passed, 69 tests
- `cargo clippy --all-targets -- -D warnings` -- passed
- `rumdl check tdkpin-rs/CHANGELOG.md tdkpin-rs/RECONSTRUCTION.md` -- passed
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-23 17:36:03 +02:00
parent 98f21b31c0
commit 1f0085902c
9 changed files with 1004 additions and 297 deletions
+35
View File
@@ -1,6 +1,9 @@
//! Borland Win16 random-number stream used by the original executable.
use crate::real48::Real48;
const MULTIPLIER: u32 = 0x0808_8405;
#[cfg(test)]
const TWO_TO_32: f64 = 4_294_967_296.0;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
@@ -24,7 +27,30 @@ impl BorlandRandom {
(product >> 32) as u16
}
#[allow(clippy::cast_possible_truncation)]
pub fn real48(&mut self) -> Real48 {
let mut random = self.next_u32();
if random == 0 {
return Real48::ZERO;
}
let mut exponent = 0x80_u8;
while random & 0x8000_0000 == 0 {
random <<= 1;
exponent = exponent.wrapping_sub(1);
}
random &= 0x7fff_ffff;
Real48::from_bytes([
exponent,
0,
random as u8,
(random >> 8) as u8,
(random >> 16) as u8,
(random >> 24) as u8,
])
}
/// Exact host representation of the x87 `Random` result in `[0, 1)`.
#[cfg(test)]
pub fn unit_interval(&mut self) -> f64 {
f64::from(self.next_u32()) / TWO_TO_32
}
@@ -66,4 +92,13 @@ mod tests {
(f64::from(expected_seed) / TWO_TO_32).to_bits()
);
}
#[test]
fn real48_register_result_matches_the_reconstructed_normalization() {
let mut random = BorlandRandom::new(7);
assert_eq!(
random.real48(),
Real48::from_bytes([0x7e, 0, 0x90, 0x70, 0xee, 0x60])
);
}
}