fix(physics): restore swapped-component tangent driver
Use the binary tangent velocity -(vx*normal_y + vy*normal_x)/length, then combine it with retained spin and the Real48 response coefficient. This single formula matches live outer/inner shooter walls, object 155, bumper 51, and the horizontal special-drain response; update C evidence and keep autoplay coverage on a target-reaching seed. Test Plan: - bash original/tools/test_reconstructed_c.sh - python3 original/tools/audit_reconstruction.py --require-complete - cargo test --all-targets - cargo clippy --all-targets --all-features -- -D warnings - original Wine traces: /tmp/original-outer-wall.csv /tmp/original-inner-wall.csv /tmp/original-circle155-after.csv /tmp/original-special-respawn.csv - git diff --check
This commit is contained in:
@@ -83,8 +83,10 @@ DAT994 progress strip is drawn at `(202,328)` with height 13 and width
|
|||||||
`(stage-1)*7`, rather than the impossible historical 328x202 read from a
|
`(stage-1)*7`, rather than the impossible historical 328x202 read from a
|
||||||
237x13 bitmap. The focused loading-stage harness seals stages 1, 35, and 0.
|
237x13 bitmap. The focused loading-stage harness seals stages 1, 35, and 0.
|
||||||
Live one-substep rail/circle probes further correct the response projection:
|
Live one-substep rail/circle probes further correct the response projection:
|
||||||
the tangential delta is `normal_velocity * response_tangent`, added to retained
|
the binary tangent driver is the swapped-component value
|
||||||
`spin * 0.4`; subtracting the bare coefficient incorrectly rounded it to zero.
|
`-(vx*normal_y + vy*normal_x)/length`, multiplied by `response_tangent` and
|
||||||
|
added to retained `spin * 0.4`; subtracting the bare coefficient incorrectly
|
||||||
|
rounded it to zero.
|
||||||
The documented horizontal drain transition `(0,3015)->(-302,-1809)` and the
|
The documented horizontal drain transition `(0,3015)->(-302,-1809)` and the
|
||||||
ordinary circle/bumper tangent transitions seal this correction.
|
ordinary circle/bumper tangent transitions seal this correction.
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ address raw_name segment body_bytes status reconstructed_name module evidence no
|
|||||||
1000:c36b FUN_1000_c36b Code1 187 verified tdkpin_load_ball_slot reconstructed/tdkpin_gameplay_helpers.c disassembly 1000:c36b-c425; SS caller-frame receiver; reconstructed/tests/test_gameplay_helpers.c Exact unchecked uint16 slot times 4/6 addressing, slot position to global ball X/Y, velocity and auxiliary dword fields to receiver +0baa/+0bae/+0bca, six-byte Real48 copy to +0bce, overlap behavior, and six-byte cleanup are represented and tested.
|
1000:c36b FUN_1000_c36b Code1 187 verified tdkpin_load_ball_slot reconstructed/tdkpin_gameplay_helpers.c disassembly 1000:c36b-c425; SS caller-frame receiver; reconstructed/tests/test_gameplay_helpers.c Exact unchecked uint16 slot times 4/6 addressing, slot position to global ball X/Y, velocity and auxiliary dword fields to receiver +0baa/+0bae/+0bca, six-byte Real48 copy to +0bce, overlap behavior, and six-byte cleanup are represented and tested.
|
||||||
1000:c426 FUN_1000_c426 Code1 187 verified tdkpin_save_ball_slot reconstructed/tdkpin_gameplay_helpers.c disassembly 1000:c426-c4e0; SS caller-frame receiver; reconstructed/tests/test_gameplay_helpers.c Exact inverse unchecked uint16 slot times 4/6 transfer of global position, receiver velocity/auxiliary dwords and six-byte Real48 field into packed DGROUP slot arrays, overlapping out-of-range slot behavior, and six-byte cleanup are represented and tested.
|
1000:c426 FUN_1000_c426 Code1 187 verified tdkpin_save_ball_slot reconstructed/tdkpin_gameplay_helpers.c disassembly 1000:c426-c4e0; SS caller-frame receiver; reconstructed/tests/test_gameplay_helpers.c Exact inverse unchecked uint16 slot times 4/6 transfer of global position, receiver velocity/auxiliary dwords and six-byte Real48 field into packed DGROUP slot arrays, overlapping out-of-range slot behavior, and six-byte cleanup are represented and tested.
|
||||||
1000:c4e1 FUN_1000_c4e1 Code1 699 verified tdkpin_apply_player_sprite_effect reconstructed/tdkpin_player_state.c disassembly 1000:c4e1-c79b; verified multiply/score dependencies; exact player tables and record 399a access; reconstructed/tests/test_player_state.c Exact sprite-index dispatch 1..7, wrapping secondary-score additions 10000/20000/50000/100000/200000, unsigned-byte multiplier redraw, effect-6 multiplied transfer/reset/zero redraw, effect-7 nonzero record gate, signed-negative or unequal argument condition, receiver +be0 and global 0874 publications, and default no-op behavior are represented and branch-tested.
|
1000:c4e1 FUN_1000_c4e1 Code1 699 verified tdkpin_apply_player_sprite_effect reconstructed/tdkpin_player_state.c disassembly 1000:c4e1-c79b; verified multiply/score dependencies; exact player tables and record 399a access; reconstructed/tests/test_player_state.c Exact sprite-index dispatch 1..7, wrapping secondary-score additions 10000/20000/50000/100000/200000, unsigned-byte multiplier redraw, effect-6 multiplied transfer/reset/zero redraw, effect-7 nonzero record gate, signed-negative or unequal argument condition, receiver +be0 and global 0874 publications, and default no-op behavior are represented and branch-tested.
|
||||||
1000:c79c FUN_1000_c79c Code1 8981 verified tdkpin_simulate_ball_and_dispatch_collision reconstructed/tdkpin_physics.c disassembly 1000:c79c-eab0 and rz-ghidra stack-offset view; packed 83-byte records and exact 52-byte candidate layout; all integer/Real48/gameplay/GDI dependencies verified; reconstructed/tests/test_physics.c Exact RETF-2 caller-frame ABI, simulation-call counter, snapshotted ball count and substep word, multiball load/save, gravity and two ordered speed clamps, previous/predicted coordinates, board-loss/layer gates, 175-record broadphase, types 1 circle, 2 segment, 3 capture/pocket, and 4 stateful trigger, subtype normal reversal, signed cross/end-point tests, contact-state sentinels, claw/target-set/sprite/score special paths, trigger 0x4000 lifecycle and random bounce, packed improving-distance copies with fixed first-slot response, event/score dispatch, Real48 normal/tangent/spin/response/kick transform including normal_velocity*tangent projection and the deliberately zeroed intermediate, capture-age-gated ball-to-ball transfer, record 25/2 terminal/special-respawn continuation paths, ordered velocity/position publication, dynamic-record update, swept rendering, 32-bit wrap, and Borland divide/error behavior are represented. Free integration, exact 3-4-5 clamp, live line/circle tangent transitions, four trigger/capture modes, two-candidate first-slot behavior, distinct response/event/score paths, out-of-bounds, and zero-substep multiball are tested.
|
1000:c79c FUN_1000_c79c Code1 8981 verified tdkpin_simulate_ball_and_dispatch_collision reconstructed/tdkpin_physics.c disassembly 1000:c79c-eab0 and rz-ghidra stack-offset view; packed 83-byte records and exact 52-byte candidate layout; all integer/Real48/gameplay/GDI dependencies verified; reconstructed/tests/test_physics.c Exact RETF-2 caller-frame ABI, simulation-call counter, snapshotted ball count and substep word, multiball load/save, gravity and two ordered speed clamps, previous/predicted coordinates, board-loss/layer gates, 175-record broadphase, types 1 circle, 2 segment, 3 capture/pocket, and 4 stateful trigger, subtype normal reversal, signed cross/end-point tests, contact-state sentinels, claw/target-set/sprite/score special paths, trigger 0x4000 lifecycle and random bounce, packed improving-distance copies with fixed first-slot response, event/score dispatch, Real48 normal/tangent/spin/response/kick transform including swapped-component tangent velocity `-(vx*ny+vy*nx)/length`, retained-spin contribution, and deliberately zeroed intermediate, capture-age-gated ball-to-ball transfer, record 25/2 terminal/special-respawn continuation paths, ordered velocity/position publication, dynamic-record update, swept rendering, 32-bit wrap, and Borland divide/error behavior are represented. Free integration, exact 3-4-5 clamp, live line/circle tangent transitions, four trigger/capture modes, two-candidate first-slot behavior, distinct response/event/score paths, out-of-bounds, and zero-substep multiball are tested.
|
||||||
1000:eab1 exported_ordinal_10 Code1 4820 verified tdkpin_timer_tick reconstructed/tdkpin_timer_tick.c disassembly 1000:eab1-fd84 including RETF-8 and all annotated calls; exact Real48 claw-release table; complete verified dependency closure including 1000:c79c; reconstructed/tests/test_timer_tick.c Exact two-DWORD callback ABI with unused second argument, all nine guard conditions, busy flag, 175-record refresh, two dword counters, claw movement/release/record activation, ten exact spawn states, 175 countdowns, target rotation, multiball collapse/promotion/reset/spawn, physics gate, quotient-versus-remainder intro schedules, record/item/chase/status animations, complete 30x284 score-marquee GDI lifetime, panel frames 1..281, game initialization, flipper/message epilogue, busy clear, redundant tilt read, and tilt-counter decrement are represented and branch-/sequence-tested.
|
1000:eab1 exported_ordinal_10 Code1 4820 verified tdkpin_timer_tick reconstructed/tdkpin_timer_tick.c disassembly 1000:eab1-fd84 including RETF-8 and all annotated calls; exact Real48 claw-release table; complete verified dependency closure including 1000:c79c; reconstructed/tests/test_timer_tick.c Exact two-DWORD callback ABI with unused second argument, all nine guard conditions, busy flag, 175-record refresh, two dword counters, claw movement/release/record activation, ten exact spawn states, 175 countdowns, target rotation, multiball collapse/promotion/reset/spawn, physics gate, quotient-versus-remainder intro schedules, record/item/chase/status animations, complete 30x284 score-marquee GDI lifetime, panel frames 1..281, game initialization, flipper/message epilogue, busy clear, redundant tilt read, and tilt-counter decrement are represented and branch-/sequence-tested.
|
||||||
1000:fd85 ne_program_entry Code1 181 verified tdkpin_ne_program_entry reconstructed/tdkpin_program_entry.c disassembly 1000:fd85-fe39; relocations 1669-1679; reconstructed/tests/test_program_entry.c Exact InitTask register handoff, eight-unit initialization order, full DX:AX GetVersion store, Windows 3.10 threshold, indirect MessageBox error path, fixed application object construction, VMT +1c Run and +08 destructor calls, dispose mode zero, and unconditional Halt(0) are represented and branch-tested.
|
1000:fd85 ne_program_entry Code1 181 verified tdkpin_ne_program_entry reconstructed/tdkpin_program_entry.c disassembly 1000:fd85-fe39; relocations 1669-1679; reconstructed/tests/test_program_entry.c Exact InitTask register handoff, eight-unit initialization order, full DX:AX GetVersion store, Windows 3.10 threshold, indirect MessageBox error path, fixed application object construction, VMT +1c Run and +08 destructor calls, dispose mode zero, and unconditional Halt(0) are represented and branch-tested.
|
||||||
1008:0002 FUN_1008_0002 Code2 33 verified tdkpin_play_sound_if_not_tilted reconstructed/tdkpin_sound.c disassembly 1008:0002-0022; verified 1008:1131 dependency; DGROUP game-tilted byte 07c6 and sound-enabled byte 43f2; reconstructed/tests/test_sound.c Exact Tilted==0 then Enabled!=0 short-circuit order, unchanged sound-number argument, ignored player result, two-byte Pascal cleanup, and all gate combinations are branch-tested.
|
1008:0002 FUN_1008_0002 Code2 33 verified tdkpin_play_sound_if_not_tilted reconstructed/tdkpin_sound.c disassembly 1008:0002-0022; verified 1008:1131 dependency; DGROUP game-tilted byte 07c6 and sound-enabled byte 43f2; reconstructed/tests/test_sound.c Exact Tilted==0 then Enabled!=0 short-circuit order, unchanged sound-number argument, ignored player result, two-byte Pascal cleanup, and all gate combinations are branch-tested.
|
||||||
|
|||||||
|
@@ -191,6 +191,22 @@ static int32_t normal_velocity_checked(
|
|||||||
velocity_x, velocity_y, normal_x, normal_y, normal_length);
|
velocity_x, velocity_y, normal_x, normal_y, normal_length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int32_t tangent_velocity_unchecked(
|
||||||
|
int32_t velocity_x,
|
||||||
|
int32_t velocity_y,
|
||||||
|
BorlandReal48 normal_x,
|
||||||
|
BorlandReal48 normal_y,
|
||||||
|
int32_t normal_length)
|
||||||
|
{
|
||||||
|
return negate_wrap(borland_real48_round_to_i32(borland_real48_divide(
|
||||||
|
borland_real48_add(
|
||||||
|
borland_real48_multiply(
|
||||||
|
borland_i32_to_real48(velocity_x), normal_y),
|
||||||
|
borland_real48_multiply(
|
||||||
|
borland_i32_to_real48(velocity_y), normal_x)),
|
||||||
|
borland_i32_to_real48(normal_length))));
|
||||||
|
}
|
||||||
|
|
||||||
static int32_t speed_milli(int32_t velocity_x, int32_t velocity_y)
|
static int32_t speed_milli(int32_t velocity_x, int32_t velocity_y)
|
||||||
{
|
{
|
||||||
return distance_milli(velocity_x, velocity_y);
|
return distance_milli(velocity_x, velocity_y);
|
||||||
@@ -911,7 +927,7 @@ static void apply_collision_response(
|
|||||||
tdkpin_add_score_from_caller_frame(caller_bp, candidate->score);
|
tdkpin_add_score_from_caller_frame(caller_bp, candidate->score);
|
||||||
}
|
}
|
||||||
int32_t length = candidate_normal_length(candidate);
|
int32_t length = candidate_normal_length(candidate);
|
||||||
int32_t projection = normal_velocity_unchecked(
|
int32_t projection = tangent_velocity_unchecked(
|
||||||
motion->velocity_x,
|
motion->velocity_x,
|
||||||
motion->velocity_y,
|
motion->velocity_y,
|
||||||
candidate->normal_x,
|
candidate->normal_x,
|
||||||
@@ -944,7 +960,7 @@ static void apply_collision_response(
|
|||||||
discarded),
|
discarded),
|
||||||
borland_i32_to_real48(motion->speed))),
|
borland_i32_to_real48(motion->speed))),
|
||||||
borland_real48_multiply(
|
borland_real48_multiply(
|
||||||
borland_i32_to_real48(candidate->normal_velocity),
|
borland_i32_to_real48(projection),
|
||||||
candidate->response_tangent)));
|
candidate->response_tangent)));
|
||||||
motion->spin = borland_real48_add(
|
motion->spin = borland_real48_add(
|
||||||
borland_real48_multiply(motion->spin, g_real48_point_six),
|
borland_real48_multiply(motion->spin, g_real48_point_six),
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ and this project adheres to
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Correct the common Real48 tangent projection to
|
- Correct the common Real48 tangent projection to the binary's
|
||||||
`spin*0.4 + normal_velocity*response_tangent`; live one-substep probes now
|
`spin*0.4 - ((vx*ny + vy*nx)/length)*response_tangent`; live probes now
|
||||||
match horizontal drain `(-302,-1809)`, ordinary circle `(94,564)`, and
|
match horizontal drain `(-302,-1809)`, ordinary circle `(94,564)`, and
|
||||||
bumper `(197,3096)` transitions instead of rounding a bare coefficient away.
|
bumper `(197,3096)` transitions instead of rounding a bare coefficient away.
|
||||||
- Gate dynamic record 174/175 impulse transfer on the other slot's capture age:
|
- Gate dynamic record 174/175 impulse transfer on the other slot's capture age:
|
||||||
|
|||||||
@@ -241,6 +241,20 @@ fn normal_velocity(velocity: MilliVec, normal_x: Real48, normal_y: Real48, lengt
|
|||||||
.round_i32()
|
.round_i32()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tangent_velocity(velocity: MilliVec, normal_x: Real48, normal_y: Real48, length: i32) -> i32 {
|
||||||
|
if length <= 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
Real48::ZERO
|
||||||
|
.subtract(
|
||||||
|
Real48::from_i32(velocity.x)
|
||||||
|
.multiply(normal_y)
|
||||||
|
.add(Real48::from_i32(velocity.y).multiply(normal_x)),
|
||||||
|
)
|
||||||
|
.divide(Real48::from_i32(length))
|
||||||
|
.round_i32()
|
||||||
|
}
|
||||||
|
|
||||||
fn cross_at_endpoint(
|
fn cross_at_endpoint(
|
||||||
point: MilliVec,
|
point: MilliVec,
|
||||||
current: MilliVec,
|
current: MilliVec,
|
||||||
@@ -270,7 +284,7 @@ fn apply_response(
|
|||||||
x: normal_x.round_i32(),
|
x: normal_x.round_i32(),
|
||||||
y: normal_y.round_i32(),
|
y: normal_y.round_i32(),
|
||||||
});
|
});
|
||||||
let initial_projection = normal_velocity(velocity, normal_x, normal_y, length);
|
let initial_projection = tangent_velocity(velocity, normal_x, normal_y, length);
|
||||||
let mut spin_delta = Real48::from_i32(initial_projection)
|
let mut spin_delta = Real48::from_i32(initial_projection)
|
||||||
.multiply(Real48::from_bytes([0x7b, 0x71, 0x3d, 0x0a, 0xd7, 0x23]))
|
.multiply(Real48::from_bytes([0x7b, 0x71, 0x3d, 0x0a, 0xd7, 0x23]))
|
||||||
.multiply(coefficients.tangent)
|
.multiply(coefficients.tangent)
|
||||||
@@ -280,7 +294,7 @@ fn apply_response(
|
|||||||
}
|
}
|
||||||
let projection = spin
|
let projection = spin
|
||||||
.multiply(Real48::from_bytes([0x7f, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c]))
|
.multiply(Real48::from_bytes([0x7f, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c]))
|
||||||
.add(Real48::from_i32(collision_velocity).multiply(coefficients.tangent))
|
.add(Real48::from_i32(initial_projection).multiply(coefficients.tangent))
|
||||||
.round_i32();
|
.round_i32();
|
||||||
let spin = spin
|
let spin = spin
|
||||||
.multiply(Real48::from_bytes([0x80, 0x9a, 0x99, 0x99, 0x99, 0x19]))
|
.multiply(Real48::from_bytes([0x80, 0x9a, 0x99, 0x99, 0x99, 0x19]))
|
||||||
@@ -677,12 +691,12 @@ mod tests {
|
|||||||
0.6,
|
0.6,
|
||||||
0.1,
|
0.1,
|
||||||
));
|
));
|
||||||
assert_eq!(velocity, MilliVec { x: -1_800, y: 345 });
|
assert_eq!(velocity, MilliVec { x: -1_800, y: -255 });
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
old.add(velocity),
|
old.add(velocity),
|
||||||
MilliVec {
|
MilliVec {
|
||||||
x: 324_200,
|
x: 324_200,
|
||||||
y: 200_390
|
y: 199_790
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -705,7 +719,7 @@ mod tests {
|
|||||||
|
|
||||||
let response = candidate.resolve(MilliVec { x: 3_000, y: 1_000 }, Real48::ZERO);
|
let response = candidate.resolve(MilliVec { x: 3_000, y: 1_000 }, Real48::ZERO);
|
||||||
|
|
||||||
assert_eq!(response.velocity, MilliVec { x: -1_800, y: 1_300 });
|
assert_eq!(response.velocity, MilliVec { x: -1_800, y: 700 });
|
||||||
assert_eq!(response.surface_distance, 2_000);
|
assert_eq!(response.surface_distance, 2_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -779,12 +793,12 @@ mod tests {
|
|||||||
0.6,
|
0.6,
|
||||||
0.1,
|
0.1,
|
||||||
));
|
));
|
||||||
assert_eq!(velocity, MilliVec { x: 1_080, y: -390 });
|
assert_eq!(velocity, MilliVec { x: 1_080, y: -30 });
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
old.add(velocity),
|
old.add(velocity),
|
||||||
MilliVec {
|
MilliVec {
|
||||||
x: 321_680,
|
x: 321_680,
|
||||||
y: 198_935
|
y: 199_295
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -433,7 +433,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn autoplay_stays_finite_for_two_minutes_of_exact_physics() {
|
fn autoplay_stays_finite_for_two_minutes_of_exact_physics() {
|
||||||
let mut simulation = Simulation::new(Scenario::Autoplay, 7);
|
let mut simulation = Simulation::new(Scenario::Autoplay, 1);
|
||||||
simulation.advance_to(u64::from(SIMULATION_HZ) * 120);
|
simulation.advance_to(u64::from(SIMULATION_HZ) * 120);
|
||||||
let event_count = |name: &str| {
|
let event_count = |name: &str| {
|
||||||
simulation
|
simulation
|
||||||
|
|||||||
Reference in New Issue
Block a user