diff --git a/original/C_RECONSTRUCTION_FINAL_AUDIT.md b/original/C_RECONSTRUCTION_FINAL_AUDIT.md index e807698..8bde492 100644 --- a/original/C_RECONSTRUCTION_FINAL_AUDIT.md +++ b/original/C_RECONSTRUCTION_FINAL_AUDIT.md @@ -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 237x13 bitmap. The focused loading-stage harness seals stages 1, 35, and 0. Live one-substep rail/circle probes further correct the response projection: -the tangential delta is `normal_velocity * response_tangent`, added to retained -`spin * 0.4`; subtracting the bare coefficient incorrectly rounded it to zero. +the binary tangent driver is the swapped-component value +`-(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 ordinary circle/bumper tangent transitions seal this correction. diff --git a/original/FUNCTION_RECONSTRUCTION.tsv b/original/FUNCTION_RECONSTRUCTION.tsv index 2dc1e06..aabc45e 100644 --- a/original/FUNCTION_RECONSTRUCTION.tsv +++ b/original/FUNCTION_RECONSTRUCTION.tsv @@ -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: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: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: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. diff --git a/original/reconstructed/tdkpin_physics.c b/original/reconstructed/tdkpin_physics.c index a8642d3..9bfbf3a 100644 --- a/original/reconstructed/tdkpin_physics.c +++ b/original/reconstructed/tdkpin_physics.c @@ -191,6 +191,22 @@ static int32_t normal_velocity_checked( 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) { 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); } int32_t length = candidate_normal_length(candidate); - int32_t projection = normal_velocity_unchecked( + int32_t projection = tangent_velocity_unchecked( motion->velocity_x, motion->velocity_y, candidate->normal_x, @@ -944,7 +960,7 @@ static void apply_collision_response( discarded), borland_i32_to_real48(motion->speed))), borland_real48_multiply( - borland_i32_to_real48(candidate->normal_velocity), + borland_i32_to_real48(projection), candidate->response_tangent))); motion->spin = borland_real48_add( borland_real48_multiply(motion->spin, g_real48_point_six), diff --git a/tdkpin-rs/CHANGELOG.md b/tdkpin-rs/CHANGELOG.md index b190064..dd186b1 100644 --- a/tdkpin-rs/CHANGELOG.md +++ b/tdkpin-rs/CHANGELOG.md @@ -10,8 +10,8 @@ and this project adheres to ### Fixed -- Correct the common Real48 tangent projection to - `spin*0.4 + normal_velocity*response_tangent`; live one-substep probes now +- Correct the common Real48 tangent projection to the binary's + `spin*0.4 - ((vx*ny + vy*nx)/length)*response_tangent`; live probes now match horizontal drain `(-302,-1809)`, ordinary circle `(94,564)`, and 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: diff --git a/tdkpin-rs/src/original_physics.rs b/tdkpin-rs/src/original_physics.rs index fde7c1d..d5f1965 100644 --- a/tdkpin-rs/src/original_physics.rs +++ b/tdkpin-rs/src/original_physics.rs @@ -241,6 +241,20 @@ fn normal_velocity(velocity: MilliVec, normal_x: Real48, normal_y: Real48, lengt .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( point: MilliVec, current: MilliVec, @@ -270,7 +284,7 @@ fn apply_response( x: normal_x.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) .multiply(Real48::from_bytes([0x7b, 0x71, 0x3d, 0x0a, 0xd7, 0x23])) .multiply(coefficients.tangent) @@ -280,7 +294,7 @@ fn apply_response( } let projection = spin .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(); let spin = spin .multiply(Real48::from_bytes([0x80, 0x9a, 0x99, 0x99, 0x99, 0x19])) @@ -677,12 +691,12 @@ mod tests { 0.6, 0.1, )); - assert_eq!(velocity, MilliVec { x: -1_800, y: 345 }); + assert_eq!(velocity, MilliVec { x: -1_800, y: -255 }); assert_eq!( old.add(velocity), MilliVec { 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); - 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); } @@ -779,12 +793,12 @@ mod tests { 0.6, 0.1, )); - assert_eq!(velocity, MilliVec { x: 1_080, y: -390 }); + assert_eq!(velocity, MilliVec { x: 1_080, y: -30 }); assert_eq!( old.add(velocity), MilliVec { x: 321_680, - y: 198_935 + y: 199_295 } ); } diff --git a/tdkpin-rs/src/simulation.rs b/tdkpin-rs/src/simulation.rs index a747cf6..a253667 100644 --- a/tdkpin-rs/src/simulation.rs +++ b/tdkpin-rs/src/simulation.rs @@ -433,7 +433,7 @@ mod tests { #[test] 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); let event_count = |name: &str| { simulation