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
495 lines
17 KiB
C
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));
|
|
}
|
|
}
|
|
}
|