Files
tdkpin/original/reconstructed/tests/test_physics.c
T
ddidderr 3dce159965 fix(physics): match live tangent response transitions
Correct the common projection to spin*0.4 + normal_velocity*response_tangent and seal line/circle behavior against current Wine one-substep traces. Preserve record-two's in-response special-respawn continuation, producing the live (16698,21191)/(-302,-1809) state after publishing (17000,23000)/(0,3040), and update the readable C evidence.

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
- rumdl check original/C_RECONSTRUCTION_FINAL_AUDIT.md tdkpin-rs/CHANGELOG.md tdkpin-rs/RECONSTRUCTION.md tdkpin-rs/README.md
- live Wine record 2 trace: /tmp/original-special-respawn.csv
- live Wine object 155 trace: /tmp/original-circle155-after.csv
- git diff --check
2026-08-23 19:42:23 +02:00

485 lines
14 KiB
C

#include "../tdkpin_physics.h"
#include "../tdkpin_borland_runtime.h"
#include "../tdkpin_collision_records.h"
#include "../tdkpin_real48.h"
#include <assert.h>
#include <stdlib.h>
#include <string.h>
static uint8_t g_dgroup[0x5000];
static uint8_t g_objects[0x1000];
static uint8_t g_stack[0x1000];
static Win16FarPtr g_window;
static unsigned g_finish_calls;
static unsigned g_render_calls;
static unsigned g_update_calls;
static int32_t g_last_update;
static unsigned g_restore_b_calls;
static uint16_t g_last_restore_b;
static unsigned g_event_calls;
static uint16_t g_last_event_id;
static uint16_t g_last_event_flags;
static unsigned g_sound_calls;
static uint16_t g_last_sound;
static int32_t g_score_total;
static unsigned g_load_calls;
static unsigned g_save_calls;
uint8_t g_borland_cpu_level = 2;
_Noreturn void borland_runtime_error(uint16_t code)
{
(void)code;
abort();
}
_Noreturn void win16_integer_divide_fault(void)
{
abort();
}
void tdkpin_finish_ball_or_advance_player(uint16_t caller_bp)
{
assert(caller_bp == 0x0300);
g_finish_calls++;
}
void tdkpin_render_ball_swept_region(uint16_t caller_bp)
{
assert(caller_bp == 0x0300);
g_render_calls++;
}
void tdkpin_update_dynamic_ball_record(
uint16_t caller_bp, int32_t displacement)
{
assert(caller_bp == 0x0300);
g_update_calls++;
g_last_update = displacement;
}
void tdkpin_restore_record_b(uint16_t caller_bp, uint16_t id)
{
assert(caller_bp == 0x0300);
g_restore_b_calls++;
g_last_restore_b = id;
}
void tdkpin_restore_record_or_item_a(uint16_t caller_bp, uint16_t id)
{
(void)caller_bp;
(void)id;
}
void tdkpin_handle_collision_events(
uint16_t caller_bp, uint16_t event_flags, uint16_t record_id)
{
assert(caller_bp == 0x0300);
g_event_calls++;
g_last_event_flags = event_flags;
g_last_event_id = record_id;
}
void tdkpin_play_sound_if_not_tilted(uint16_t sound_number)
{
g_sound_calls++;
g_last_sound = sound_number;
}
void tdkpin_add_score_from_caller_frame(uint16_t caller_bp, int32_t delta)
{
assert(caller_bp == 0x0300);
g_score_total += delta;
}
void tdkpin_draw_score_from_caller_frame(
uint16_t caller_bp, uint16_t value_low, uint16_t value_high)
{
(void)caller_bp;
(void)value_low;
(void)value_high;
}
void tdkpin_refresh_player_progress_markers(uint16_t caller_bp)
{
(void)caller_bp;
}
void tdkpin_apply_player_sprite_effect(
uint16_t caller_bp, int16_t argument)
{
(void)caller_bp;
(void)argument;
}
void tdkpin_composite_player_sprite(uint16_t sprite_index, HDC16 reference_dc)
{
(void)sprite_index;
(void)reference_dc;
}
void tdkpin_present_background_region(
INT16 height, INT16 width, INT16 y, INT16 x, HDC16 dc)
{
(void)height;
(void)width;
(void)y;
(void)x;
(void)dc;
}
HDC16 GetDC16(HWND16 window)
{
(void)window;
return 0x3333;
}
INT16 ReleaseDC16(HWND16 window, HDC16 dc)
{
(void)window;
(void)dc;
return 1;
}
void tdkpin_load_ball_slot(uint16_t caller_bp, uint16_t slot)
{
(void)caller_bp;
(void)slot;
g_load_calls++;
}
void tdkpin_save_ball_slot(uint16_t caller_bp, uint16_t slot)
{
(void)caller_bp;
(void)slot;
g_save_calls++;
}
uint16_t borland_random_below(uint16_t upper_bound)
{
assert(upper_bound == 4);
return 0;
}
BorlandReal48 borland_random_real48_registers(void)
{
return (BorlandReal48){{0x80,0,0,0,0,0}};
}
static bool in_bounds(
int32_t x,
int32_t y,
int32_t maximum_y,
int32_t maximum_x,
int32_t minimum_y,
int32_t minimum_x)
{
return x >= minimum_x && x <= maximum_x &&
y >= minimum_y && y <= maximum_y;
}
uint16_t tdkpin_predicted_ball_in_bounds(
uint16_t ignored_static_link,
int32_t maximum_y,
int32_t maximum_x,
int32_t minimum_y,
int32_t minimum_x)
{
(void)ignored_static_link;
return in_bounds(
(int32_t)win16_read_u32(win16_dgroup_pointer(0x07db)),
(int32_t)win16_read_u32(win16_dgroup_pointer(0x07df)),
maximum_y, maximum_x, minimum_y, minimum_x);
}
uint16_t tdkpin_receiver_point_in_bounds(
uint16_t caller_bp,
int32_t maximum_y,
int32_t maximum_x,
int32_t minimum_y,
int32_t minimum_x)
{
assert(caller_bp == 0x0300);
return in_bounds(
(int32_t)win16_read_u32(win16_far_add_offset(g_window, 0x0bba)),
(int32_t)win16_read_u32(win16_far_add_offset(g_window, 0x0bbe)),
maximum_y, maximum_x, minimum_y, minimum_x);
}
static Win16FarPtr record_pointer(uint16_t id)
{
return win16_dgroup_pointer((uint16_t)(
0x095b + id * sizeof(TdkpinCollisionRecord)));
}
static void write_record(
uint16_t id, const TdkpinCollisionRecord *record)
{
const uint8_t *bytes = (const uint8_t *)record;
for (uint16_t index = 0; index < sizeof(*record); index++) {
win16_write_u8(
win16_far_add_offset(record_pointer(id), index), bytes[index]);
}
}
static TdkpinCollisionRecord base_record(uint8_t type)
{
TdkpinCollisionRecord record = {0};
record.type = type;
record.subtype = 1;
record.active = 1;
record.bounds_min_x_milli = 0;
record.bounds_min_y_milli = 0;
record.bounds_max_x_milli = 200000;
record.bounds_max_y_milli = 200000;
record.layer_mask = 1;
return record;
}
static void prepare_fixture(void)
{
memset(g_dgroup, 0, sizeof(g_dgroup));
memset(g_objects, 0, sizeof(g_objects));
memset(g_stack, 0, sizeof(g_stack));
win16_reset_segment_bindings();
win16_bind_segment(0x7000, g_objects, sizeof(g_objects), true);
win16_bind_segment(0x7200, g_dgroup, sizeof(g_dgroup), true);
win16_bind_segment(0x7300, g_stack, sizeof(g_stack), true);
win16_set_dgroup_selector(0x7200);
win16_set_stack_state(0x7300, 0);
g_window = win16_make_far_pointer(0x7000, 0);
win16_write_stack_u16(0x0306, win16_far_offset(g_window));
win16_write_stack_u16(0x0308, win16_far_selector(g_window));
win16_write_u16(g_window, 4, 0x2222);
win16_write_u8(win16_far_add_offset(g_window, 0x61), 1);
win16_write_u8(win16_far_add_offset(g_window, 0x0bdd), 1);
win16_write_u16(win16_dgroup_pointer(0x0871), 0, 1);
win16_write_u32(g_window, 0x56, 10000);
g_finish_calls = g_render_calls = g_update_calls = 0;
g_restore_b_calls = g_event_calls = g_sound_calls = 0;
g_score_total = 0;
g_load_calls = g_save_calls = 0;
}
static void set_ball(
int32_t x, int32_t y, int32_t velocity_x, int32_t velocity_y)
{
win16_write_u32(win16_dgroup_pointer(0x07d3), 0, (uint32_t)x);
win16_write_u32(win16_dgroup_pointer(0x07d7), 0, (uint32_t)y);
win16_write_u32(g_window, 0x0baa, (uint32_t)velocity_x);
win16_write_u32(g_window, 0x0bae, (uint32_t)velocity_y);
}
static void test_free_integration_and_speed_clamp(void)
{
prepare_fixture();
set_ball(100000, 100000, 1000, 2000);
win16_write_u32(g_window, 0x5a, 100);
tdkpin_simulate_ball_and_dispatch_collision(0x0300);
assert((int32_t)win16_read_u32(win16_dgroup_pointer(0x07d3)) == 101000);
assert((int32_t)win16_read_u32(win16_dgroup_pointer(0x07d7)) == 102100);
assert(g_update_calls == 1 && g_last_update == 1);
assert(g_render_calls == 1);
assert(win16_read_u32(win16_far_add_offset(g_window, 0x4d)) == 1);
prepare_fixture();
set_ball(100000, 100000, 3000, 4000);
win16_write_u32(g_window, 0x56, 1000);
tdkpin_simulate_ball_and_dispatch_collision(0x0300);
assert((int32_t)win16_read_u32(
win16_far_add_offset(g_window, 0x0baa)) == 600);
assert((int32_t)win16_read_u32(
win16_far_add_offset(g_window, 0x0bae)) == 800);
}
static void test_circle_and_segment_collisions(void)
{
prepare_fixture();
set_ball(100000, 100000, 1000, 0);
TdkpinCollisionRecord circle = base_record(1);
circle.point1_x_milli = 103000;
circle.point1_y_milli = 100000;
circle.radius_milli = borland_i32_to_real48(4000);
write_record(1, &circle);
tdkpin_simulate_ball_and_dispatch_collision(0x0300);
assert((int32_t)win16_read_u32(
win16_far_add_offset(g_window, 0x0baa)) == 0);
assert((int32_t)win16_read_u32(win16_dgroup_pointer(0x07d3)) == 100000);
prepare_fixture();
set_ball(100000, 100000, 0, 1000);
TdkpinCollisionRecord segment = base_record(2);
segment.point1_x_milli = 50000;
segment.point1_y_milli = 101000;
segment.point2_x_milli = 150000;
segment.point2_y_milli = 101000;
write_record(1, &segment);
tdkpin_simulate_ball_and_dispatch_collision(0x0300);
assert((int32_t)win16_read_u32(
win16_far_add_offset(g_window, 0x0bae)) == 0);
assert((int32_t)win16_read_u32(win16_dgroup_pointer(0x07d7)) == 100000);
prepare_fixture();
set_ball(100000, 100000, 0, 1000);
segment = base_record(2);
segment.point1_x_milli = 50000;
segment.point1_y_milli = 101000;
segment.point2_x_milli = 150000;
segment.point2_y_milli = 101000;
segment.response_tangent = (BorlandReal48){{0x7d,0xcd,0xcc,0xcc,0xcc,0x4c}};
write_record(1, &segment);
tdkpin_simulate_ball_and_dispatch_collision(0x0300);
assert((int32_t)win16_read_u32(
win16_far_add_offset(g_window, 0x0baa)) == -100);
assert((int32_t)win16_read_u32(
win16_far_add_offset(g_window, 0x0bae)) == 0);
prepare_fixture();
set_ball(100000, 100000, 1000, 0);
circle = base_record(1);
circle.point1_x_milli = 103000;
circle.point1_y_milli = 100000;
circle.radius_milli = borland_i32_to_real48(4000);
circle.response_normal = borland_i32_to_real48(1);
circle.flags = 0x0020;
circle.score_low = 100;
write_record(1, &circle);
tdkpin_simulate_ball_and_dispatch_collision(0x0300);
assert((int32_t)win16_read_u32(
win16_far_add_offset(g_window, 0x0baa)) == -1000);
assert(g_event_calls == 1 && g_last_event_flags == 0x0020);
assert(g_score_total == 100);
prepare_fixture();
set_ball(100000, 100000, 0, 1000);
segment = base_record(2);
segment.point1_x_milli = 50000;
segment.point1_y_milli = 101000;
segment.point2_x_milli = 150000;
segment.point2_y_milli = 101000;
write_record(1, &segment);
segment.point1_y_milli = 100500;
segment.point2_y_milli = 100500;
segment.response_normal = borland_i32_to_real48(1);
write_record(2, &segment);
tdkpin_simulate_ball_and_dispatch_collision(0x0300);
assert((int32_t)win16_read_u32(
win16_far_add_offset(g_window, 0x0bae)) == 0);
}
static void test_capture_and_trigger_records(void)
{
prepare_fixture();
set_ball(100000, 100000, 0, 0);
TdkpinCollisionRecord capture = base_record(3);
capture.point1_x_milli = 100000;
capture.point1_y_milli = 100000;
capture.radius_milli = borland_i32_to_real48(20000);
write_record(50, &capture);
tdkpin_simulate_ball_and_dispatch_collision(0x0300);
assert(win16_read_u16(win16_far_add_offset(record_pointer(50), 0x43)) == 1);
assert(win16_read_u32(win16_far_add_offset(g_window, 0x0bca)) == 5);
assert(g_sound_calls == 1 && g_last_sound == 15);
prepare_fixture();
set_ball(100000, 100000, 1000, 0);
TdkpinCollisionRecord trigger = base_record(4);
trigger.point1_x_milli = 0;
trigger.point1_y_milli = 0;
trigger.point2_x_milli = 200000;
trigger.point2_y_milli = 200000;
write_record(6, &trigger);
tdkpin_simulate_ball_and_dispatch_collision(0x0300);
assert((int32_t)win16_read_u32(
win16_far_add_offset(g_window, 0x0baa)) == 900);
assert((int32_t)win16_read_u32(
win16_far_add_offset(g_window, 0x0bae)) == -8500);
assert(g_restore_b_calls == 1 && g_last_restore_b == 6);
prepare_fixture();
set_ball(100000, 100000, 1000, 0);
win16_write_u8(win16_dgroup_pointer(0x07d1), 1);
win16_write_u8(win16_dgroup_pointer(0x081b), 2);
trigger = base_record(4);
trigger.point1_x_milli = 101000;
trigger.point1_y_milli = 100000;
trigger.radius_milli = borland_i32_to_real48(5000);
trigger.flags = 0x0040;
trigger.score_low = 100;
write_record(10, &trigger);
tdkpin_simulate_ball_and_dispatch_collision(0x0300);
assert((win16_read_u16(
win16_far_add_offset(record_pointer(10), 0x41)) & 0x4000) != 0);
assert(g_sound_calls == 1 && g_last_sound == 4);
assert(g_event_calls == 1 && g_last_event_id == 10 &&
g_last_event_flags == 0x4040);
assert(g_score_total == 200);
}
static void test_capture_boundary_and_claw(void)
{
prepare_fixture();
set_ball(100000, 100000, 1000, 0);
TdkpinCollisionRecord capture = base_record(3);
capture.point1_x_milli = 103000;
capture.point1_y_milli = 102000;
capture.radius_milli = borland_i32_to_real48(3800);
capture.contact_state = 99;
capture.flags = 0x0010;
write_record(50, &capture);
tdkpin_simulate_ball_and_dispatch_collision(0x0300);
assert(g_event_calls == 1 && g_last_event_id == 50);
assert((int32_t)win16_read_u32(
win16_far_add_offset(g_window, 0x0baa)) != 1000);
prepare_fixture();
set_ball(100000, 100000, 0, 0);
capture = base_record(3);
capture.point1_x_milli = 100000;
capture.point1_y_milli = 100000;
capture.radius_milli = borland_i32_to_real48(20000);
capture.contact_state = 1;
write_record(89, &capture);
win16_write_u32(g_window, 0x0bca, 300);
for (uint16_t id = 12; id <= 20; id++) {
TdkpinCollisionRecord gate = base_record(4);
write_record(id, &gate);
}
tdkpin_simulate_ball_and_dispatch_collision(0x0300);
assert(win16_read_u8(win16_dgroup_pointer(0x07ca)) == 1);
assert(win16_read_u16(win16_dgroup_pointer(0x086f)) == 1);
assert(win16_read_u8(win16_dgroup_pointer(0x0873)) == 1);
assert(win16_read_u8(win16_far_add_offset(g_window, 0x0bdf)) == 1);
for (uint16_t id = 12; id <= 20; id++) {
assert(win16_read_u8(
win16_far_add_offset(record_pointer(id), 0x34)) == 0);
}
}
static void test_out_of_bounds_and_zero_substeps_multiball(void)
{
prepare_fixture();
set_ball(340000, 100000, 1001, 0);
tdkpin_simulate_ball_and_dispatch_collision(0x0300);
assert(g_finish_calls == 1);
prepare_fixture();
win16_write_u8(win16_far_add_offset(g_window, 0x61), 2);
win16_write_u16(win16_dgroup_pointer(0x0871), 0, 0);
tdkpin_simulate_ball_and_dispatch_collision(0x0300);
assert(g_load_calls == 2 && g_save_calls == 2);
assert(g_render_calls == 2 && g_update_calls == 0);
}
int main(void)
{
test_free_integration_and_speed_clamp();
test_circle_and_segment_collisions();
test_capture_and_trigger_records();
test_capture_boundary_and_claw();
test_out_of_bounds_and_zero_substeps_multiball();
return 0;
}