Replace the partial mechanics transcriptions with a separate, readable C11 reconstruction of the complete Win16 image while preserving the original raw Ghidra export as immutable evidence. Cover all ordinary and overlapping entry points, Borland runtime behavior, Win16 imports, segmented data, callbacks, resources, indirect control flow, physics, rendering, persistence, and startup/shutdown lifecycles. Add deterministic extraction and audit tooling plus address-linked ledgers for functions, imports, DGROUP ranges and objects, relocations, resources, and callbacks. The final gate records zero raw, partial, restored, unknown, blocked, or unclassified required units. Keep the semantic-fidelity boundary explicit: the portable C is not claimed to reproduce a byte-identical Borland NE build. Add strict focused harnesses for every reconstructed C unit, exact resource round-trip checks, and a 16-bit Borland Real48 reference probe. No Rust source or Cargo metadata is changed in this phase. Test Plan: - `bash original/tools/test_reconstructed_c.sh` -- passed - `bash original/tools/probe_real48_reference.sh` -- passed bit-for-bit - `python3 original/tools/audit_reconstruction.py --require-complete` -- passed - `git diff --cached --check` -- passed - `git diff HEAD -- '*.rs' Cargo.toml Cargo.lock` -- empty
454 lines
13 KiB
C
454 lines
13 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_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, 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);
|
|
}
|
|
|
|
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;
|
|
}
|