Files
tdkpin/original/reconstructed/tdkpin_flipper_collision.c
T
ddidderr fa3f168467 fix(flippers): match original moving-hit geometry
The clone's moving-flipper gate used the cross-product operands in the
opposite order, mirroring and narrowing the hit wedge. Right-flipper release
also used record 81's first endpoint even though the original reads its second
endpoint. Correct both geometry paths, retain the recovered swept tip bounds,
and add live-binary boundary vectors plus a dense transition regression.

Test Plan:
- `cargo test --workspace --all-targets --all-features` -- passed (134 tests)
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` -- passed
- `cargo build --profile production` -- passed
- `LSAN_OPTIONS=detect_leaks=0 ASAN_OPTIONS=detect_leaks=0 bash original/tools/test_reconstructed_c.sh` -- passed
- `python3 original/tools/audit_reconstruction.py --require-complete` -- passed
- `git diff --cached --check` -- passed
2026-08-29 09:15:25 +02:00

495 lines
17 KiB
C

/* Moving-flipper collision response at 1000:7ed9. */
#include "tdkpin_flippers.h"
#include "tdkpin_arithmetic.h"
#include "tdkpin_collision_records.h"
#include "tdkpin_gameplay_helpers.h"
#include "tdkpin_real48.h"
#include "tdkpin_sound.h"
enum {
COLLISION_RECORDS_BASE = 0x095b,
COLLISION_RECORD_BYTES = 0x53,
RECORD_RADIUS = 0x22,
RECORD_RESPONSE_NORMAL = 0x28,
COLLISION_SEARCH_RADIUS = 54000,
COLLISION_RESPONSE_RADIUS = 44000,
};
typedef struct {
int32_t radius;
int32_t pivot_x;
int32_t pivot_y;
int32_t unused_negative_point_x;
int32_t unused_positive_point_x;
int32_t negative_edge_x;
int32_t negative_edge_y;
int32_t positive_edge_x;
int32_t positive_edge_y;
uint16_t response_record;
} FlipperCollisionGeometry;
static const BorlandReal48 g_real48_one_tenth =
{{0x7d, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c}};
static const BorlandReal48 g_real48_one_half =
{{0x80, 0, 0, 0, 0, 0}};
static const BorlandReal48 g_real48_two =
{{0x82, 0, 0, 0, 0, 0}};
static const BorlandReal48 g_real48_two_fifths =
{{0x7f, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c}};
static const BorlandReal48 g_real48_one =
{{0x81, 0, 0, 0, 0, 0}};
static const BorlandReal48 g_real48_thousand =
{{0x8a, 0, 0, 0, 0, 0x7a}};
static Win16FarPtr caller_receiver(uint16_t caller_bp)
{
uint16_t offset = win16_read_stack_u16((uint16_t)(caller_bp + 6));
uint16_t selector = win16_read_stack_u16((uint16_t)(caller_bp + 8));
return win16_make_far_pointer(selector, offset);
}
static Win16FarPtr collision_record(uint16_t id)
{
return win16_dgroup_pointer((uint16_t)(
COLLISION_RECORDS_BASE + id * COLLISION_RECORD_BYTES));
}
static int32_t record_i32(uint16_t id, uint16_t offset)
{
return (int32_t)win16_read_u32(
win16_far_add_offset(collision_record(id), offset));
}
static BorlandReal48 record_real48(uint16_t id, uint16_t offset)
{
BorlandReal48 value;
Win16FarPtr source = win16_far_add_offset(collision_record(id), offset);
for (uint16_t index = 0; index < sizeof(value.bytes); index++) {
value.bytes[index] = win16_read_u8(
win16_far_add_offset(source, index));
}
return value;
}
static int32_t add_wrap_i32(int32_t left, int32_t right)
{
return (int32_t)((uint32_t)left + (uint32_t)right);
}
static int32_t subtract_wrap_i32(int32_t left, int32_t right)
{
return (int32_t)((uint32_t)left - (uint32_t)right);
}
static int32_t negate_wrap_i32(int32_t value)
{
return (int32_t)(0u - (uint32_t)value);
}
static int32_t absolute_wrap_i32(int32_t value)
{
return value < 0 ? negate_wrap_i32(value) : value;
}
static int32_t divide_by_thousand(int32_t value)
{
return borland_divide_i32(value, 1000).quotient;
}
static int32_t indeterminate_i32(void)
{
uint32_t low = win16_indeterminate_u16();
uint32_t high = win16_indeterminate_u16();
return (int32_t)(low | (high << 16));
}
static BorlandReal48 negate_real48(BorlandReal48 value)
{
if (value.bytes[0] != 0) {
value.bytes[5] ^= 0x80;
}
return value;
}
static FlipperCollisionGeometry load_flipper_geometry(
int32_t delta, uint16_t flipper)
{
FlipperCollisionGeometry geometry;
if (flipper == 1) {
geometry.radius = borland_real48_round_to_i32(
record_real48(65, RECORD_RADIUS));
geometry.pivot_x = subtract_wrap_i32(
record_i32(65, offsetof(TdkpinCollisionRecord, point1_x_milli)),
10000);
geometry.pivot_y = record_i32(
65, offsetof(TdkpinCollisionRecord, point1_y_milli));
geometry.unused_negative_point_x = record_i32(
66, offsetof(TdkpinCollisionRecord, point1_x_milli));
geometry.negative_edge_x = add_wrap_i32(
record_i32(66, offsetof(TdkpinCollisionRecord, point2_x_milli)),
20000);
geometry.negative_edge_y = record_i32(
66, offsetof(TdkpinCollisionRecord, point2_y_milli));
geometry.unused_positive_point_x = record_i32(
68, offsetof(TdkpinCollisionRecord, point2_x_milli));
geometry.positive_edge_x = add_wrap_i32(
record_i32(68, offsetof(TdkpinCollisionRecord, point1_x_milli)),
20000);
geometry.positive_edge_y = record_i32(
68, offsetof(TdkpinCollisionRecord, point1_y_milli));
geometry.response_record = delta == 1 ? 68 : 66;
} else if (flipper == 2) {
geometry.radius = borland_real48_round_to_i32(
record_real48(80, RECORD_RADIUS));
geometry.pivot_x = add_wrap_i32(
record_i32(80, offsetof(TdkpinCollisionRecord, point1_x_milli)),
10000);
geometry.pivot_y = record_i32(
80, offsetof(TdkpinCollisionRecord, point1_y_milli));
geometry.unused_negative_point_x = record_i32(
83, offsetof(TdkpinCollisionRecord, point1_x_milli));
geometry.negative_edge_x = subtract_wrap_i32(
record_i32(83, offsetof(TdkpinCollisionRecord, point1_x_milli)),
20000);
geometry.negative_edge_y = record_i32(
83, offsetof(TdkpinCollisionRecord, point1_y_milli));
geometry.unused_positive_point_x = record_i32(
81, offsetof(TdkpinCollisionRecord, point1_x_milli));
geometry.positive_edge_x = subtract_wrap_i32(
record_i32(81, offsetof(TdkpinCollisionRecord, point2_x_milli)),
20000);
geometry.positive_edge_y = record_i32(
81, offsetof(TdkpinCollisionRecord, point2_y_milli));
geometry.response_record = delta == 1 ? 81 : 83;
} else {
geometry = (FlipperCollisionGeometry){
.radius = indeterminate_i32(),
.pivot_x = indeterminate_i32(),
.pivot_y = indeterminate_i32(),
.unused_negative_point_x = indeterminate_i32(),
.unused_positive_point_x = indeterminate_i32(),
.negative_edge_x = indeterminate_i32(),
.negative_edge_y = indeterminate_i32(),
.positive_edge_x = indeterminate_i32(),
.positive_edge_y = indeterminate_i32(),
.response_record = win16_indeterminate_u16(),
};
}
geometry.pivot_y = add_wrap_i32(
geometry.pivot_y,
borland_multiply_i32(delta, geometry.radius));
(void)geometry.unused_negative_point_x;
(void)geometry.unused_positive_point_x;
return geometry;
}
static int32_t cross_product_for_edge(
int32_t edge_x,
int32_t edge_y,
int32_t pivot_x,
int32_t pivot_y,
int32_t ball_x,
int32_t ball_y)
{
int32_t edge_from_ball_x = divide_by_thousand(
subtract_wrap_i32(edge_x, ball_x));
int32_t edge_from_pivot_y = divide_by_thousand(
subtract_wrap_i32(edge_y, pivot_y));
int32_t edge_from_ball_y = divide_by_thousand(
subtract_wrap_i32(edge_y, ball_y));
int32_t edge_from_pivot_x = divide_by_thousand(
subtract_wrap_i32(edge_x, pivot_x));
return subtract_wrap_i32(
borland_multiply_i32(edge_from_ball_x, edge_from_pivot_y),
borland_multiply_i32(edge_from_ball_y, edge_from_pivot_x));
}
static int32_t radial_distance_milli(
int32_t ball_x, int32_t ball_y, int32_t pivot_x, int32_t pivot_y)
{
BorlandReal48 dx = borland_real48_divide(
borland_i32_to_real48(subtract_wrap_i32(ball_x, pivot_x)),
g_real48_thousand);
BorlandReal48 dy = borland_real48_divide(
borland_i32_to_real48(subtract_wrap_i32(ball_y, pivot_y)),
g_real48_thousand);
BorlandReal48 squared = borland_real48_add(
borland_real48_square(dx), borland_real48_square(dy));
return borland_real48_round_to_i32(
borland_real48_multiply(
borland_real48_sqrt(squared), g_real48_thousand));
}
static bool point_in_moving_flipper(
const FlipperCollisionGeometry *geometry,
int32_t delta,
uint16_t flipper,
uint16_t mode,
int32_t ball_x,
int32_t ball_y,
int32_t *distance)
{
int32_t first_cross;
int32_t second_cross;
if (delta == -1) {
first_cross = cross_product_for_edge(
geometry->negative_edge_x,
subtract_wrap_i32(geometry->negative_edge_y, 43000),
geometry->pivot_x,
geometry->pivot_y,
ball_x,
ball_y);
second_cross = cross_product_for_edge(
geometry->negative_edge_x,
add_wrap_i32(geometry->negative_edge_y, 12000),
geometry->pivot_x,
geometry->pivot_y,
ball_x,
ball_y);
} else {
first_cross = cross_product_for_edge(
geometry->positive_edge_x,
subtract_wrap_i32(geometry->positive_edge_y, 12000),
geometry->pivot_x,
geometry->pivot_y,
ball_x,
ball_y);
second_cross = cross_product_for_edge(
geometry->positive_edge_x,
add_wrap_i32(geometry->positive_edge_y, 43000),
geometry->pivot_x,
geometry->pivot_y,
ball_x,
ball_y);
}
int32_t absolute_x = absolute_wrap_i32(
subtract_wrap_i32(ball_x, geometry->pivot_x));
int32_t absolute_y = absolute_wrap_i32(
subtract_wrap_i32(ball_y, geometry->pivot_y));
if (absolute_x >= COLLISION_SEARCH_RADIUS ||
absolute_y >= COLLISION_SEARCH_RADIUS) {
return false;
}
*distance = radial_distance_milli(
ball_x, ball_y, geometry->pivot_x, geometry->pivot_y);
if (*distance > COLLISION_SEARCH_RADIUS || mode != 0) {
return false;
}
if (flipper == 1) {
return first_cross > -1000 && second_cross < 1000 &&
ball_x > geometry->pivot_x;
}
if (flipper == 2) {
return first_cross < 1000 && second_cross > -1000 &&
ball_x < geometry->pivot_x;
}
return false;
}
static int32_t collision_penetration(
const FlipperCollisionGeometry *geometry,
int32_t delta,
int32_t ball_x,
int32_t ball_y)
{
int32_t collision_y;
if (delta == -1) {
int32_t edge_dx = absolute_wrap_i32(subtract_wrap_i32(
geometry->negative_edge_x, geometry->pivot_x));
if (borland_real48_compare(
borland_i32_to_real48(edge_dx), g_real48_one_tenth) > 0) {
collision_y = add_wrap_i32(
subtract_wrap_i32(
geometry->pivot_y,
absolute_wrap_i32(subtract_wrap_i32(
ball_x, geometry->pivot_x))),
5000);
} else {
collision_y = subtract_wrap_i32(
geometry->negative_edge_y, 43000);
}
} else {
int32_t edge_dx = subtract_wrap_i32(
geometry->positive_edge_x, geometry->pivot_x);
if (borland_real48_compare(
borland_i32_to_real48(absolute_wrap_i32(edge_dx)),
g_real48_one_tenth) > 0) {
int32_t numerator = borland_multiply_i32(
subtract_wrap_i32(ball_x, geometry->pivot_x),
subtract_wrap_i32(
add_wrap_i32(geometry->positive_edge_y, 43000),
geometry->pivot_y));
int32_t interpolation = borland_real48_round_to_i32(
borland_real48_divide(
borland_i32_to_real48(numerator),
borland_i32_to_real48(edge_dx)));
collision_y = subtract_wrap_i32(
geometry->pivot_y, interpolation);
} else {
collision_y = add_wrap_i32(
geometry->positive_edge_y, 43000);
}
}
int32_t penetration = subtract_wrap_i32(collision_y, ball_y);
if (penetration < 0) {
return negate_wrap_i32(
borland_multiply_i32(penetration, delta));
}
return 0;
}
static void apply_flipper_response(
Win16FarPtr receiver,
const FlipperCollisionGeometry *geometry,
int32_t delta,
uint16_t flipper,
int32_t ball_x,
int32_t ball_y,
int32_t distance,
int32_t penetration,
uint16_t caller_bp)
{
int32_t normal_x_integer = borland_multiply_i32(
subtract_wrap_i32(ball_x, geometry->pivot_x), delta);
int32_t normal_y_integer = borland_multiply_i32(
add_wrap_i32(
subtract_wrap_i32(ball_y, geometry->pivot_y), 4000),
delta);
BorlandReal48 normal_x = borland_i32_to_real48(normal_x_integer);
BorlandReal48 normal_y = borland_i32_to_real48(normal_y_integer);
if (flipper == 2) {
normal_x = negate_real48(normal_x);
normal_y = negate_real48(normal_y);
}
int32_t velocity_x = (int32_t)win16_read_u32(
win16_far_add_offset(receiver, 0x0baa));
int32_t velocity_y = (int32_t)win16_read_u32(
win16_far_add_offset(receiver, 0x0bae));
BorlandReal48 response_radius =
borland_i32_to_real48(COLLISION_RESPONSE_RADIUS);
/* 1000:94ce-9553 computes this projection and then deliberately clears it. */
BorlandReal48 discarded_projection_real = borland_real48_divide(
borland_real48_multiply(
borland_real48_add(
borland_real48_multiply(
borland_i32_to_real48(velocity_y), normal_y),
borland_real48_multiply(
borland_i32_to_real48(velocity_x), normal_x)),
g_real48_one_half),
response_radius);
(void)borland_real48_round_to_i32(discarded_projection_real);
int32_t normal_projection = 0;
int32_t tangent_projection = borland_real48_round_to_i32(
borland_real48_divide(
borland_real48_subtract(
borland_real48_multiply(
borland_i32_to_real48(velocity_x), normal_y),
borland_real48_multiply(
borland_i32_to_real48(velocity_y), normal_x)),
response_radius));
BorlandReal48 gain = borland_real48_add(
borland_real48_multiply(
borland_real48_sqrt(borland_real48_divide(
borland_i32_to_real48(distance), response_radius)),
g_real48_two),
g_real48_two_fifths);
BorlandReal48 record_response = record_real48(
geometry->response_record, RECORD_RESPONSE_NORMAL);
tangent_projection = add_wrap_i32(
borland_real48_round_to_i32(
borland_real48_multiply(
borland_i32_to_real48(tangent_projection),
borland_real48_add(g_real48_one, record_response))),
borland_real48_round_to_i32(
borland_real48_multiply(
borland_i32_to_real48((int32_t)win16_read_u32(
win16_far_add_offset(receiver, 0x56))),
gain)));
int32_t delta_velocity_x = borland_real48_round_to_i32(
borland_real48_divide(
borland_real48_subtract(
borland_real48_multiply(
borland_i32_to_real48(normal_projection), normal_x),
borland_real48_multiply(
borland_i32_to_real48(tangent_projection), normal_y)),
response_radius));
velocity_x = add_wrap_i32(velocity_x, delta_velocity_x);
win16_write_u32(receiver, 0x0baa, (uint32_t)velocity_x);
int32_t delta_velocity_y = borland_real48_round_to_i32(
borland_real48_divide(
borland_real48_add(
borland_real48_multiply(
borland_i32_to_real48(tangent_projection), normal_x),
borland_real48_multiply(
borland_i32_to_real48(normal_projection), normal_y)),
response_radius));
velocity_y = add_wrap_i32(velocity_y, delta_velocity_y);
win16_write_u32(receiver, 0x0bae, (uint32_t)velocity_y);
int32_t movement_x = borland_real48_round_to_i32(
borland_real48_divide(
borland_i32_to_real48(
borland_multiply_i32(penetration, velocity_x)),
borland_i32_to_real48(velocity_y)));
tdkpin_move_ball_and_render(
caller_bp, movement_x, penetration);
}
void tdkpin_move_flipper_collision_geometry(
uint16_t caller_bp,
int32_t delta,
uint16_t flipper,
uint16_t mode)
{
tdkpin_play_sound_if_not_tilted(21);
Win16FarPtr receiver = caller_receiver(caller_bp);
FlipperCollisionGeometry geometry =
load_flipper_geometry(delta, flipper);
uint16_t ball_count = win16_read_u8(
win16_far_add_offset(receiver, 0x61));
for (uint16_t ball = 1; ball <= ball_count; ball++) {
if (win16_read_u8(win16_far_add_offset(receiver, 0x61)) > 1) {
tdkpin_load_ball_slot(caller_bp, (uint16_t)(ball - 1u));
}
int32_t ball_x = (int32_t)win16_read_u32(
win16_dgroup_pointer(0x07d3));
int32_t ball_y = (int32_t)win16_read_u32(
win16_dgroup_pointer(0x07d7));
int32_t distance = 0;
if (point_in_moving_flipper(
&geometry,
delta,
flipper,
mode,
ball_x,
ball_y,
&distance)) {
int32_t penetration = collision_penetration(
&geometry, delta, ball_x, ball_y);
apply_flipper_response(
receiver,
&geometry,
delta,
flipper,
ball_x,
ball_y,
distance,
penetration,
caller_bp);
}
if (win16_read_u8(win16_far_add_offset(receiver, 0x61)) > 1) {
tdkpin_save_ball_slot(caller_bp, (uint16_t)(ball - 1u));
}
}
}