fix(physics): restore relative slingshot response
A full comparison against the 175-record ledger found stale guessed coordinates in both relative slingshot chains. Circles 56/58/71/73 and lines 57/59/72/74 were displaced by up to 70 pixels. The corrected record-57 geometry then exposed a second issue: readable C and Rust had collapsed two independent binary response projections into one. Transcribe the accumulated initializer coordinates exactly. Use the swapped-component projection only to sign the current tangential response `abs(normal_velocity) * response_tangent`, and use the standard dot product for `spin_delta * 0.02 * response_tangent`. Correct the readable C evidence and Rust together, extend live tracing with the six-byte spin field, and seal the stopped-Wine transition from `(75530,354765)/(3000,15)` to `(77369,356597)/(1839,1832)` with Real48 spin `-5.28` in both harnesses. Test Plan: - stopped Wine 11.15 record-57 injection/trace -- matched position, velocity, and spin - `cargo test --workspace --all-targets --all-features` -- 123 passed - `cargo clippy --workspace --all-targets --all-features -- -D warnings` -- passed - `bash original/tools/test_reconstructed_c.sh` -- passed - `python3 original/tools/audit_reconstruction.py --require-complete` -- passed with zero incomplete or unclassified units - `python3 -m py_compile original/tools/trace_original_state.py original/tools/inject_original_state.py` -- passed - `rumdl check --flavor commonmark RECONSTRUCTION.md CHANGELOG.md` -- passed - `git diff --cached --check` -- passed
This commit is contained in:
@@ -82,13 +82,16 @@ Raw `1000:531e` further corrects the Pascal-order BitBlt interpretation: the
|
||||
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 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.
|
||||
Live one-substep rail/circle probes further correct the response transform.
|
||||
The binary keeps two projections distinct: swapped-component
|
||||
`-(vx*normal_y + vy*normal_x)/length` supplies the sign of the tangential
|
||||
response magnitude `abs(normal_velocity)*response_tangent`, which is added to
|
||||
retained `spin*0.4`. Standard dot-product
|
||||
`(vx*normal_x + vy*normal_y)/length` independently drives
|
||||
`spin_delta = projection*0.02*response_tangent`. The documented horizontal
|
||||
drain `(0,3015)->(-302,-1809)`, ordinary circle/bumper transitions, and stopped
|
||||
Wine record-57 transition `(75530,354765)/(3000,15)` to
|
||||
`(77369,356597)/(1839,1832)` with exact Real48 spin `-5.28` seal both paths.
|
||||
|
||||
## Reproducible gates
|
||||
|
||||
|
||||
@@ -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 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: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 with swapped-component sign driver `-(vx*ny+vy*nx)/length`, signed `abs(normal_velocity)*response_tangent` response, independent dot-product spin delta `(vx*nx+vy*ny)/length*0.02*response_tangent`, 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.
|
||||
|
||||
|
@@ -191,7 +191,7 @@ static int32_t normal_velocity_checked(
|
||||
velocity_x, velocity_y, normal_x, normal_y, normal_length);
|
||||
}
|
||||
|
||||
static int32_t tangent_velocity_unchecked(
|
||||
static int32_t tangent_direction_unchecked(
|
||||
int32_t velocity_x,
|
||||
int32_t velocity_y,
|
||||
BorlandReal48 normal_x,
|
||||
@@ -207,6 +207,22 @@ static int32_t tangent_velocity_unchecked(
|
||||
borland_i32_to_real48(normal_length))));
|
||||
}
|
||||
|
||||
static int32_t spin_velocity_unchecked(
|
||||
int32_t velocity_x,
|
||||
int32_t velocity_y,
|
||||
BorlandReal48 normal_x,
|
||||
BorlandReal48 normal_y,
|
||||
int32_t normal_length)
|
||||
{
|
||||
return borland_real48_round_to_i32(borland_real48_divide(
|
||||
borland_real48_add(
|
||||
borland_real48_multiply(
|
||||
borland_i32_to_real48(velocity_x), normal_x),
|
||||
borland_real48_multiply(
|
||||
borland_i32_to_real48(velocity_y), normal_y)),
|
||||
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);
|
||||
@@ -927,7 +943,16 @@ 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 = tangent_velocity_unchecked(
|
||||
/* Raw 1000:ef19..f16a keeps the contact-direction and spin projections
|
||||
* separate. The swapped-component value selects the sign of the current
|
||||
* tangential response; the standard dot product updates retained spin. */
|
||||
int32_t tangent_direction = tangent_direction_unchecked(
|
||||
motion->velocity_x,
|
||||
motion->velocity_y,
|
||||
candidate->normal_x,
|
||||
candidate->normal_y,
|
||||
length);
|
||||
int32_t spin_projection = spin_velocity_unchecked(
|
||||
motion->velocity_x,
|
||||
motion->velocity_y,
|
||||
candidate->normal_x,
|
||||
@@ -936,14 +961,9 @@ static void apply_collision_response(
|
||||
|
||||
BorlandReal48 spin_delta = borland_real48_multiply(
|
||||
borland_real48_multiply(
|
||||
borland_i32_to_real48(projection), g_real48_point_zero_two),
|
||||
borland_i32_to_real48(spin_projection),
|
||||
g_real48_point_zero_two),
|
||||
candidate->response_tangent);
|
||||
spin_delta = borland_real48_multiply(
|
||||
borland_i32_to_real48(absolute_wrap(candidate->normal_velocity)),
|
||||
spin_delta);
|
||||
if (projection < 0) {
|
||||
spin_delta = negate_real48(spin_delta);
|
||||
}
|
||||
BorlandReal48 discarded = borland_real48_divide(
|
||||
borland_real48_sqrt(borland_real48_multiply(
|
||||
absolute_real48(spin_delta), g_real48_twenty)),
|
||||
@@ -951,17 +971,21 @@ static void apply_collision_response(
|
||||
(void)discarded;
|
||||
discarded = (BorlandReal48){{0,0,0,0,0,0}};
|
||||
|
||||
projection = borland_real48_round_to_i32(borland_real48_add(
|
||||
BorlandReal48 tangent_response = borland_real48_multiply(
|
||||
borland_i32_to_real48(absolute_wrap(candidate->normal_velocity)),
|
||||
candidate->response_tangent);
|
||||
if (tangent_direction < 0) {
|
||||
tangent_response = negate_real48(tangent_response);
|
||||
}
|
||||
int32_t projection = borland_real48_round_to_i32(borland_real48_add(
|
||||
borland_real48_subtract(
|
||||
borland_real48_multiply(motion->spin, g_real48_point_four),
|
||||
borland_real48_divide(
|
||||
borland_real48_multiply(
|
||||
borland_i32_to_real48(absolute_wrap(projection)),
|
||||
borland_i32_to_real48(absolute_wrap(tangent_direction)),
|
||||
discarded),
|
||||
borland_i32_to_real48(motion->speed))),
|
||||
borland_real48_multiply(
|
||||
borland_i32_to_real48(projection),
|
||||
candidate->response_tangent)));
|
||||
tangent_response));
|
||||
motion->spin = borland_real48_add(
|
||||
borland_real48_multiply(motion->spin, g_real48_point_six),
|
||||
spin_delta);
|
||||
|
||||
@@ -473,6 +473,51 @@ static void test_out_of_bounds_and_zero_substeps_multiball(void)
|
||||
assert(g_render_calls == 2 && g_update_calls == 0);
|
||||
}
|
||||
|
||||
static void test_live_relative_record_57_transition(void)
|
||||
{
|
||||
prepare_fixture();
|
||||
set_ball(72530, 354750, 3000, 0);
|
||||
win16_write_u32(g_window, 0x5a, 15);
|
||||
win16_write_u32(g_window, 0x56, 3800);
|
||||
TdkpinCollisionRecord segment = base_record(2);
|
||||
segment.bounds_min_x_milli = 45000;
|
||||
segment.bounds_min_y_milli = 335000;
|
||||
segment.bounds_max_x_milli = 102000;
|
||||
segment.bounds_max_y_milli = 371000;
|
||||
segment.point1_x_milli = 97000;
|
||||
segment.point1_y_milli = 366000;
|
||||
segment.point2_x_milli = 50000;
|
||||
segment.point2_y_milli = 340000;
|
||||
segment.response_normal =
|
||||
(BorlandReal48){{0x80,0x00,0x00,0x00,0x00,0x00}};
|
||||
segment.response_tangent =
|
||||
(BorlandReal48){{0x7d,0xcd,0xcc,0xcc,0xcc,0x4c}};
|
||||
segment.layer_mask = 2;
|
||||
write_record(57, &segment);
|
||||
|
||||
tdkpin_simulate_ball_and_dispatch_collision(0x0300);
|
||||
assert((int32_t)win16_read_u32(
|
||||
win16_dgroup_pointer(0x07d3)) == 75530);
|
||||
assert((int32_t)win16_read_u32(
|
||||
win16_dgroup_pointer(0x07d7)) == 354765);
|
||||
tdkpin_simulate_ball_and_dispatch_collision(0x0300);
|
||||
assert((int32_t)win16_read_u32(
|
||||
win16_dgroup_pointer(0x07d3)) == 77369);
|
||||
assert((int32_t)win16_read_u32(
|
||||
win16_dgroup_pointer(0x07d7)) == 356597);
|
||||
assert((int32_t)win16_read_u32(
|
||||
win16_far_add_offset(g_window, 0x0baa)) == 1839);
|
||||
assert((int32_t)win16_read_u32(
|
||||
win16_far_add_offset(g_window, 0x0bae)) == 1832);
|
||||
static const uint8_t expected_spin[6] = {
|
||||
0x83, 0x5d, 0x8f, 0xc2, 0xf5, 0xa8};
|
||||
for (uint16_t index = 0; index < 6; index++) {
|
||||
assert(win16_read_u8(win16_far_add_offset(
|
||||
g_window, (uint16_t)(0x0bce + index))) ==
|
||||
expected_spin[index]);
|
||||
}
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_free_integration_and_speed_clamp();
|
||||
@@ -480,5 +525,6 @@ int main(void)
|
||||
test_capture_and_trigger_records();
|
||||
test_capture_boundary_and_claw();
|
||||
test_out_of_bounds_and_zero_substeps_multiball();
|
||||
test_live_relative_record_57_transition();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ from trace_original_state import (
|
||||
PHYSICS_SUBSTEPS,
|
||||
PREVIOUS_BALL_X,
|
||||
PREVIOUS_BALL_Y,
|
||||
SPIN_REAL48,
|
||||
VELOCITY_X,
|
||||
VELOCITY_Y,
|
||||
ProcessMemory,
|
||||
@@ -139,7 +140,8 @@ def main() -> None:
|
||||
memory.write(object_base + BALL_SUSPENDED, bytes(1))
|
||||
print(
|
||||
f"TDKPIN pid={pid} data={data_base:#x} object={object_base:#x} "
|
||||
f"ball=({args.x:.3f},{args.y:.3f}) velocity=({args.vx:.3f},{args.vy:.3f})"
|
||||
f"ball=({args.x:.3f},{args.y:.3f}) velocity=({args.vx:.3f},{args.vy:.3f}) "
|
||||
f"spin={int.from_bytes(memory.read(object_base + SPIN_REAL48, 6), 'little'):#014x}"
|
||||
)
|
||||
finally:
|
||||
if resume_afterward:
|
||||
|
||||
@@ -39,6 +39,7 @@ GRAVITY_PER_SUBSTEP = 0x005A
|
||||
ACTIVE_BALL_COUNT = 0x0061
|
||||
VELOCITY_X = 0x0BAA
|
||||
VELOCITY_Y = 0x0BAE
|
||||
SPIN_REAL48 = 0x0BCE
|
||||
PREVIOUS_BALL_X = 0x0BBA
|
||||
PREVIOUS_BALL_Y = 0x0BBE
|
||||
OBJECT_BALL_X = 0x0BC2
|
||||
@@ -224,6 +225,7 @@ CSV_FIELDS = [
|
||||
"next_ball_y_milli",
|
||||
"velocity_x_milli_per_tick",
|
||||
"velocity_y_milli_per_tick",
|
||||
"spin_real48_bits",
|
||||
"previous_ball_x_milli",
|
||||
"previous_ball_y_milli",
|
||||
"object_ball_x_milli",
|
||||
@@ -257,6 +259,7 @@ def sample_state(data: bytes, obj: bytes) -> tuple[int, ...]:
|
||||
unpack_i32(data, NEXT_BALL_Y),
|
||||
unpack_i32(obj, VELOCITY_X),
|
||||
unpack_i32(obj, VELOCITY_Y),
|
||||
int.from_bytes(obj[SPIN_REAL48 : SPIN_REAL48 + 6], "little"),
|
||||
unpack_i32(obj, PREVIOUS_BALL_X),
|
||||
unpack_i32(obj, PREVIOUS_BALL_Y),
|
||||
unpack_i32(obj, OBJECT_BALL_X),
|
||||
|
||||
Reference in New Issue
Block a user