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
+54 -34
View File
@@ -1,9 +1,13 @@
//! Moving-flipper collision response reconstructed from `1000:7ed9`.
use crate::original_physics::MilliVec;
use crate::{original_physics::MilliVec, real48::Real48};
const SEARCH_RADIUS: i32 = 54_000;
const RESPONSE_RADIUS: f64 = 44_000.0;
const ONE: Real48 = Real48::from_bytes([0x81, 0, 0, 0, 0, 0]);
const TWO: Real48 = Real48::from_bytes([0x82, 0, 0, 0, 0, 0]);
const TWO_FIFTHS: Real48 =
Real48::from_bytes([0x7f, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c]);
const THOUSAND: Real48 = Real48::from_bytes([0x8a, 0, 0, 0, 0, 0x7a]);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FlipperSide {
@@ -85,15 +89,14 @@ fn cross_for_edge(edge: MilliVec, pivot: MilliVec, ball: MilliVec) -> i32 {
.wrapping_sub(edge_from_ball_x.wrapping_mul(edge_from_pivot_y))
}
#[allow(clippy::cast_possible_truncation)]
fn rounded(value: f64) -> i32 {
value.round() as i32
}
fn collision_distance(ball: MilliVec, pivot: MilliVec) -> i32 {
rounded(f64::from(ball.x.wrapping_sub(pivot.x)).hypot(f64::from(
ball.y.wrapping_sub(pivot.y),
)))
let dx = Real48::from_i32(ball.x.wrapping_sub(pivot.x)).divide(THOUSAND);
let dy = Real48::from_i32(ball.y.wrapping_sub(pivot.y)).divide(THOUSAND);
dx.square()
.add(dy.square())
.sqrt()
.multiply(THOUSAND)
.round_i32()
}
fn contains(geometry: Geometry, side: FlipperSide, delta: i32, ball: MilliVec) -> Option<i32> {
@@ -167,7 +170,11 @@ fn penetration(geometry: Geometry, delta: i32, ball: MilliVec) -> i32 {
geometry
.pivot
.y
.wrapping_sub(rounded(f64::from(numerator) / f64::from(edge_dx)))
.wrapping_sub(
Real48::from_i32(numerator)
.divide(Real48::from_i32(edge_dx))
.round_i32(),
)
} else {
geometry.positive_edge.y.wrapping_add(43_000)
}
@@ -185,7 +192,7 @@ pub fn moving_flipper_response(
velocity: MilliVec,
delta: i32,
side: FlipperSide,
response_normal: f64,
response_normal: Real48,
maximum_speed: i32,
) -> Option<FlipperResponse> {
let geometry = Geometry::for_side(side, delta);
@@ -205,7 +212,7 @@ fn response_with_geometry(
velocity: MilliVec,
delta: i32,
side: FlipperSide,
response_normal: f64,
response_normal: Real48,
maximum_speed: i32,
geometry: Geometry,
) -> Option<FlipperResponse> {
@@ -222,20 +229,33 @@ fn response_with_geometry(
normal_y = normal_y.wrapping_neg();
}
let tangent_projection = rounded(
(f64::from(velocity.x) * f64::from(normal_y)
- f64::from(velocity.y) * f64::from(normal_x))
/ RESPONSE_RADIUS,
);
let gain = (f64::from(distance) / RESPONSE_RADIUS).sqrt() * 2.0 + 0.4;
let tangent_projection = rounded(f64::from(tangent_projection) * (1.0 + response_normal))
.wrapping_add(rounded(f64::from(maximum_speed) * gain));
let delta_velocity_x = rounded(
-f64::from(tangent_projection) * f64::from(normal_y) / RESPONSE_RADIUS,
);
let delta_velocity_y = rounded(
f64::from(tangent_projection) * f64::from(normal_x) / RESPONSE_RADIUS,
);
let normal_x = Real48::from_i32(normal_x);
let normal_y = Real48::from_i32(normal_y);
let response_radius = Real48::from_i32(44_000);
let tangent_projection = Real48::from_i32(velocity.x)
.multiply(normal_y)
.subtract(Real48::from_i32(velocity.y).multiply(normal_x))
.divide(response_radius)
.round_i32();
let gain = Real48::from_i32(distance)
.divide(response_radius)
.sqrt()
.multiply(TWO)
.add(TWO_FIFTHS);
let tangent_projection = Real48::from_i32(tangent_projection)
.multiply(ONE.add(response_normal))
.round_i32()
.wrapping_add(Real48::from_i32(maximum_speed).multiply(gain).round_i32());
let delta_velocity_x = Real48::from_i32(tangent_projection)
.multiply(normal_y)
.subtract(Real48::ZERO)
.divide(response_radius)
.round_i32()
.wrapping_neg();
let delta_velocity_y = Real48::from_i32(tangent_projection)
.multiply(normal_x)
.divide(response_radius)
.round_i32();
let velocity = MilliVec {
x: velocity.x.wrapping_add(delta_velocity_x),
y: velocity.y.wrapping_add(delta_velocity_y),
@@ -243,9 +263,9 @@ fn response_with_geometry(
if velocity.y == 0 {
return None;
}
let movement_x = rounded(
f64::from(penetration.wrapping_mul(velocity.x)) / f64::from(velocity.y),
);
let movement_x = Real48::from_i32(penetration.wrapping_mul(velocity.x))
.divide(Real48::from_i32(velocity.y))
.round_i32();
Some(FlipperResponse {
velocity,
movement: MilliVec {
@@ -267,7 +287,7 @@ mod tests {
MilliVec { x: 1_000, y: 2_000 },
-1,
FlipperSide::Left,
0.5,
Real48::from_bytes([0x80, 0, 0, 0, 0, 0]),
MilliVec { x: 1_266, y: 1_511 },
MilliVec { x: -6_703, y: -8_000 },
),
@@ -276,7 +296,7 @@ mod tests {
MilliVec { x: -1_000, y: 2_000 },
1,
FlipperSide::Right,
0.75,
Real48::from_bytes([0x80, 0, 0, 0, 0, 0x40]),
MilliVec { x: -737, y: 2_263 },
MilliVec { x: -8_549, y: 26_250 },
),
@@ -285,7 +305,7 @@ mod tests {
MilliVec { x: 1_000, y: 2_000 },
1,
FlipperSide::Left,
0.25,
Real48::from_bytes([0x7f, 0, 0, 0, 0, 0]),
MilliVec { x: 622, y: 2_320 },
MilliVec { x: 5_414, y: 20_193 },
),
@@ -294,7 +314,7 @@ mod tests {
MilliVec { x: -1_000, y: 2_000 },
-1,
FlipperSide::Right,
0.0,
Real48::ZERO,
MilliVec { x: -1_280, y: 1_560 },
MilliVec { x: 7_385, y: -9_000 },
),