/* Ball substep integrator and collision dispatcher at 1000:c79c. */ #include "tdkpin_physics.h" #include "tdkpin_arithmetic.h" #include "tdkpin_collision_events.h" #include "tdkpin_collision_records.h" #include "tdkpin_gameplay_helpers.h" #include "tdkpin_player_state.h" #include "tdkpin_random.h" #include "tdkpin_real48.h" #include "tdkpin_render.h" #include "tdkpin_sound.h" #include "tdkpin_turn_state.h" enum { RECORDS_BASE = 0x095b, RECORD_BYTES = 0x53, MAX_RECORDS = 175, }; #pragma pack(push, 1) typedef struct { uint16_t record_id; BorlandReal48 normal_x; BorlandReal48 normal_y; int32_t normal_velocity; BorlandReal48 response_normal; BorlandReal48 response_tangent; BorlandReal48 response_auxiliary; BorlandReal48 response_kick; uint16_t flags; int32_t score; int32_t surface_distance; } PhysicsCollisionCandidate; #pragma pack(pop) _Static_assert(sizeof(PhysicsCollisionCandidate) == 0x34, "1000:c79c candidate layout drift"); typedef struct { int32_t velocity_x; int32_t velocity_y; BorlandReal48 spin; int32_t speed; } BallMotion; static const BorlandReal48 g_real48_thousand = {{0x8a,0,0,0,0,0x7a}}; static const BorlandReal48 g_real48_one = {{0x81,0,0,0,0,0}}; static const BorlandReal48 g_real48_point_nine = {{0x80,0x66,0x66,0x66,0x66,0x66}}; static const BorlandReal48 g_real48_point_six = {{0x80,0x9a,0x99,0x99,0x99,0x19}}; static const BorlandReal48 g_real48_point_four = {{0x7f,0xcd,0xcc,0xcc,0xcc,0x4c}}; static const BorlandReal48 g_real48_point_three = {{0x7f,0x9a,0x99,0x99,0x99,0x19}}; static const BorlandReal48 g_real48_point_zero_two = {{0x7b,0x71,0x3d,0x0a,0xd7,0x23}}; static const BorlandReal48 g_real48_twenty = {{0x85,0,0,0,0,0x20}}; 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 record_pointer(uint16_t id) { return win16_dgroup_pointer((uint16_t)(RECORDS_BASE + id * RECORD_BYTES)); } static int32_t read_i32(Win16FarPtr base, uint16_t offset) { return (int32_t)win16_read_u32(win16_far_add_offset(base, offset)); } static void write_i32(Win16FarPtr base, uint16_t offset, int32_t value) { win16_write_u32(base, offset, (uint32_t)value); } static BorlandReal48 read_real48(Win16FarPtr base, uint16_t offset) { BorlandReal48 value; for (uint16_t index = 0; index < sizeof(value.bytes); index++) { value.bytes[index] = win16_read_u8(win16_far_add_offset( base, (uint16_t)(offset + index))); } return value; } static void write_real48( Win16FarPtr base, uint16_t offset, BorlandReal48 value) { for (uint16_t index = 0; index < sizeof(value.bytes); index++) { win16_write_u8( win16_far_add_offset(base, (uint16_t)(offset + index)), value.bytes[index]); } } static int32_t dgroup_i32(uint16_t offset) { return (int32_t)win16_read_u32(win16_dgroup_pointer(offset)); } static void write_dgroup_i32(uint16_t offset, int32_t value) { win16_write_u32(win16_dgroup_pointer(offset), 0, (uint32_t)value); } static int32_t add_wrap(int32_t left, int32_t right) { return (int32_t)((uint32_t)left + (uint32_t)right); } static int32_t subtract_wrap(int32_t left, int32_t right) { return (int32_t)((uint32_t)left - (uint32_t)right); } static int32_t negate_wrap(int32_t value) { return (int32_t)(0u - (uint32_t)value); } static int32_t absolute_wrap(int32_t value) { return value < 0 ? negate_wrap(value) : value; } static BorlandReal48 negate_real48(BorlandReal48 value) { if (value.bytes[0] != 0) { value.bytes[5] ^= 0x80; } return value; } static BorlandReal48 absolute_real48(BorlandReal48 value) { value.bytes[5] &= 0x7f; return value; } static int32_t distance_milli(int32_t dx, int32_t dy) { BorlandReal48 real_x = borland_real48_divide( borland_i32_to_real48(dx), g_real48_thousand); BorlandReal48 real_y = borland_real48_divide( borland_i32_to_real48(dy), g_real48_thousand); return borland_real48_round_to_i32(borland_real48_multiply( borland_real48_sqrt(borland_real48_add( borland_real48_square(real_x), borland_real48_square(real_y))), g_real48_thousand)); } static int32_t normal_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_subtract( borland_real48_multiply( borland_i32_to_real48(velocity_x), normal_y), borland_real48_multiply( borland_i32_to_real48(velocity_y), normal_x)), borland_i32_to_real48(normal_length))); } static int32_t normal_velocity_checked( int32_t velocity_x, int32_t velocity_y, BorlandReal48 normal_x, BorlandReal48 normal_y, int32_t normal_length) { if (normal_length <= 0) { return 0; } return normal_velocity_unchecked( velocity_x, velocity_y, normal_x, normal_y, normal_length); } static int32_t tangent_velocity_unchecked( int32_t velocity_x, int32_t velocity_y, BorlandReal48 normal_x, BorlandReal48 normal_y, int32_t normal_length) { return negate_wrap(borland_real48_round_to_i32(borland_real48_divide( borland_real48_add( borland_real48_multiply( borland_i32_to_real48(velocity_x), normal_y), borland_real48_multiply( borland_i32_to_real48(velocity_y), normal_x)), 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); } static int32_t scale_real48(int32_t value, BorlandReal48 factor) { return borland_real48_round_to_i32(borland_real48_multiply( borland_i32_to_real48(value), factor)); } static int32_t clamp_motion_speed( Win16FarPtr window, int32_t *velocity_x, int32_t *velocity_y) { int32_t speed = speed_milli(*velocity_x, *velocity_y); int32_t maximum = read_i32(window, 0x56); if (speed > maximum) { BorlandReal48 excess_fraction = borland_real48_divide( borland_i32_to_real48(subtract_wrap(speed, maximum)), borland_i32_to_real48(speed)); *velocity_x = subtract_wrap( *velocity_x, scale_real48(*velocity_x, excess_fraction)); *velocity_y = subtract_wrap( *velocity_y, scale_real48(*velocity_y, excess_fraction)); speed = maximum; } return speed; } static bool broadphase_contains_predicted_ball( const TdkpinCollisionRecord *record, uint16_t caller_bp) { return (uint8_t)tdkpin_predicted_ball_in_bounds( caller_bp, record->bounds_max_y_milli, record->bounds_max_x_milli, record->bounds_min_y_milli, record->bounds_min_x_milli) != 0; } static void load_record(uint16_t id, TdkpinCollisionRecord *record) { Win16FarPtr source = record_pointer(id); uint8_t *bytes = (uint8_t *)record; for (uint16_t index = 0; index < sizeof(*record); index++) { bytes[index] = win16_read_u8(win16_far_add_offset(source, index)); } } static void make_candidate( PhysicsCollisionCandidate *candidate, uint16_t id, const TdkpinCollisionRecord *record, BorlandReal48 normal_x, BorlandReal48 normal_y, int32_t collision_velocity, int32_t surface_distance) { *candidate = (PhysicsCollisionCandidate){ .record_id = id, .normal_x = normal_x, .normal_y = normal_y, .normal_velocity = collision_velocity, .response_normal = record->response_normal, .response_tangent = record->response_tangent, .response_auxiliary = record->response_auxiliary, .response_kick = record->response_kick, .flags = record->flags, .score = (int32_t)((uint32_t)record->score_low | ((uint32_t)record->score_high << 16)), .surface_distance = surface_distance, }; } static bool detect_circle_collision( const TdkpinCollisionRecord *record, uint16_t id, uint16_t ball, int32_t previous_x, int32_t previous_y, int32_t predicted_x, int32_t predicted_y, const BallMotion *motion, PhysicsCollisionCandidate *candidate) { if (id == (uint16_t)(175u - ball)) { return false; } int32_t radius = borland_real48_round_to_i32(record->radius_milli); int32_t surface_distance = subtract_wrap( absolute_wrap(distance_milli( subtract_wrap(record->point1_x_milli, previous_x), subtract_wrap(record->point1_y_milli, previous_y))), radius); if ((record->subtype == 1 && surface_distance > motion->speed) || (record->subtype == 2 && negate_wrap(surface_distance) > motion->speed) || (record->subtype != 1 && record->subtype != 2)) { return false; } int32_t middle_x = borland_divide_i32( add_wrap(previous_x, predicted_x), 2).quotient; int32_t middle_y = borland_divide_i32( add_wrap(previous_y, predicted_y), 2).quotient; BorlandReal48 normal_x = borland_i32_to_real48( subtract_wrap(record->point1_y_milli, middle_y)); BorlandReal48 normal_y = borland_i32_to_real48( subtract_wrap(middle_x, record->point1_x_milli)); if (record->subtype == 2) { normal_x = negate_real48(normal_x); normal_y = negate_real48(normal_y); } int32_t length = distance_milli( borland_real48_round_to_i32(normal_x), borland_real48_round_to_i32(normal_y)); int32_t collision_velocity = normal_velocity_checked( motion->velocity_x, motion->velocity_y, normal_x, normal_y, length); if (collision_velocity >= 0 || absolute_wrap(surface_distance) > absolute_wrap(collision_velocity)) { return false; } surface_distance = add_wrap(surface_distance, 3000); make_candidate( candidate, id, record, normal_x, normal_y, collision_velocity, surface_distance); return true; } static int32_t cross_at_endpoint( int32_t point_x, int32_t point_y, int32_t current_x, int32_t current_y, int32_t predicted_x, int32_t predicted_y) { return subtract_wrap( borland_multiply_i32( subtract_wrap(predicted_x, point_x), subtract_wrap(point_y, current_y)), borland_multiply_i32( subtract_wrap(predicted_y, point_y), subtract_wrap(point_x, current_x))); } static bool detect_segment_collision( const TdkpinCollisionRecord *record, uint16_t id, int32_t previous_x, int32_t previous_y, int32_t predicted_x, int32_t predicted_y, const BallMotion *motion, PhysicsCollisionCandidate *candidate) { int32_t segment_x = subtract_wrap( record->point2_x_milli, record->point1_x_milli); int32_t segment_y = subtract_wrap( record->point2_y_milli, record->point1_y_milli); BorlandReal48 normal_x = borland_i32_to_real48(segment_x); BorlandReal48 normal_y = borland_i32_to_real48(segment_y); int32_t length = distance_milli(segment_x, segment_y); BorlandReal48 length_units = borland_real48_divide( borland_i32_to_real48(length), g_real48_thousand); BorlandReal48 segment_x_units = borland_real48_divide( normal_x, g_real48_thousand); BorlandReal48 segment_y_units = borland_real48_divide( normal_y, g_real48_thousand); int32_t distance = borland_real48_round_to_i32(absolute_real48( borland_real48_divide( borland_real48_subtract( borland_real48_multiply( borland_i32_to_real48(subtract_wrap( previous_x, record->point1_x_milli)), segment_y_units), borland_real48_multiply( borland_i32_to_real48(subtract_wrap( previous_y, record->point1_y_milli)), segment_x_units)), length_units))); int32_t collision_velocity = borland_real48_round_to_i32( borland_real48_divide( borland_real48_subtract( borland_real48_multiply( borland_i32_to_real48(motion->velocity_x), segment_y_units), borland_real48_multiply( borland_i32_to_real48(motion->velocity_y), segment_x_units)), length_units)); if (distance > motion->speed || collision_velocity > 10 || subtract_wrap(distance, 10) > absolute_wrap(collision_velocity) || cross_at_endpoint( record->point1_x_milli, record->point1_y_milli, previous_x, previous_y, predicted_x, predicted_y) < -10 || cross_at_endpoint( record->point2_x_milli, record->point2_y_milli, previous_x, previous_y, predicted_x, predicted_y) > 10) { return false; } make_candidate( candidate, id, record, normal_x, normal_y, collision_velocity, distance); return true; } static void add_velocity_toward( Win16FarPtr window, int32_t target_x, int32_t target_y, int32_t ball_x, int32_t ball_y, BallMotion *motion) { int32_t acceleration = borland_multiply_i32(read_i32(window, 0x5a), 10); motion->velocity_x = scale_real48( motion->velocity_x, g_real48_point_nine); motion->velocity_y = scale_real48( motion->velocity_y, g_real48_point_nine); motion->velocity_x = target_x > ball_x ? add_wrap(motion->velocity_x, acceleration) : subtract_wrap(motion->velocity_x, acceleration); motion->velocity_y = target_y > ball_y ? add_wrap(motion->velocity_y, acceleration) : subtract_wrap(motion->velocity_y, acceleration); } static bool update_target_set_award( uint16_t caller_bp, Win16FarPtr window) { uint8_t player = win16_read_u8(win16_dgroup_pointer(0x07d1)); int32_t award = 5000; for (uint16_t id = 129; id <= 133; id++) { if (win16_read_u16( win16_far_add_offset(record_pointer(id), 0x43)) != 0) { award = borland_multiply_i32(award, 2); } } Win16FarPtr accumulated = win16_dgroup_pointer( (uint16_t)(0x098e + player * 4u)); win16_write_u32( accumulated, 0, win16_read_u32(accumulated) + (uint32_t)award); if (award == 160000) { uint8_t multiplier = win16_read_u8(win16_dgroup_pointer( (uint16_t)(0x09a1 + player))); tdkpin_add_score_from_caller_frame( caller_bp, borland_multiply_i32( (int32_t)win16_read_u32(accumulated), multiplier)); win16_write_u32(accumulated, 0, 0); win16_write_u8( win16_dgroup_pointer((uint16_t)(0x09a1 + player)), (uint8_t)(multiplier + 1u)); Win16FarPtr markers = win16_dgroup_pointer( (uint16_t)(0x081f + player * 2u)); win16_write_u16(markers, 0, (uint16_t)(win16_read_u16(markers) + 1u)); tdkpin_refresh_player_progress_markers(caller_bp); HWND16 handle = win16_read_u16(win16_far_add_offset(window, 4)); HDC16 dc = GetDC16(handle); (void)ReleaseDC16(handle, dc); if (win16_read_u8(win16_dgroup_pointer(0x07c7)) == 0 && win16_read_u8(win16_dgroup_pointer(0x07c8)) == 0 && win16_read_u8(win16_dgroup_pointer(0x0874)) == 0) { write_dgroup_i32(0x0851, 0); win16_write_u8(win16_dgroup_pointer(0x07c8), 1); win16_write_u8(win16_far_add_offset(window, 0x0bdd), 0); } win16_write_u8(win16_dgroup_pointer(0x0874), 0); return true; } return false; } static void start_claw_capture(Win16FarPtr window, BallMotion *motion) { win16_write_u8(win16_dgroup_pointer(0x07ca), 1); static const uint16_t target_frames[4] = {1, 6, 7, 18}; win16_write_u16( win16_dgroup_pointer(0x086f), 0, target_frames[borland_random_below(4)]); win16_write_u8(win16_dgroup_pointer(0x0873), 1); win16_write_u8(win16_far_add_offset(window, 0x0bdf), 1); motion->spin = (BorlandReal48){{0,0,0,0,0,0}}; for (uint16_t id = 12; id <= 20; id++) { win16_write_u8( win16_far_add_offset(record_pointer(id), 0x34), 0); } } static bool handle_capture_record( TdkpinCollisionRecord *record, uint16_t id, uint16_t ball, uint16_t ball_count, uint16_t caller_bp, Win16FarPtr window, int32_t previous_x, int32_t previous_y, int32_t ball_x, int32_t ball_y, BallMotion *motion, PhysicsCollisionCandidate *candidate) { int32_t radius = borland_real48_round_to_i32(record->radius_milli); int32_t surface_distance = subtract_wrap( absolute_wrap(distance_milli( subtract_wrap(record->point1_x_milli, ball_x), subtract_wrap( subtract_wrap(record->point1_y_milli, ball_y), 2000))), radius); Win16FarPtr live = record_pointer(id); uint16_t contact = win16_read_u16(win16_far_add_offset(live, 0x43)); bool contact_allowed = contact == 0 || (contact != 99 && ball_count == 1) || contact == ball; if (surface_distance < -11000 && contact_allowed && (id != 148 || win16_read_u8(win16_dgroup_pointer(0x0875)) == 0)) { int32_t capture_age = read_i32(window, 0x0bca); if (capture_age < 300) { bool stationary = absolute_wrap(subtract_wrap( record->point1_x_milli, previous_x)) < 500 && absolute_wrap(subtract_wrap( record->point1_y_milli, previous_y)) < 500 && absolute_wrap(motion->velocity_x) < 500 && absolute_wrap(motion->velocity_y) < 500; if (stationary) { motion->velocity_x = 0; motion->velocity_y = 0; if (capture_age == 0) { win16_write_u16(live, 0x43, ball); tdkpin_play_sound_if_not_tilted(15); } write_i32(window, 0x0bca, id == 89 ? 300 : add_wrap(capture_age, 5)); } else { add_velocity_toward( window, record->point1_x_milli, record->point1_y_milli, ball_x, ball_y, motion); } return false; } if (id == 89 && win16_read_u8(win16_dgroup_pointer(0x0874)) == 0) { start_claw_capture(window, motion); return false; } if (id >= 129 && id <= 133) { bool complete = update_target_set_award(caller_bp, window); if (!complete) { uint8_t player = win16_read_u8(win16_dgroup_pointer(0x07d1)); win16_write_u8( win16_far_add_offset( window, (uint16_t)(player * 0x14u + (id - 129u) + 0x0476u)), 1); tdkpin_restore_record_or_item_a( caller_bp, (uint16_t)(id - 129u + 1015u)); } uint8_t player = win16_read_u8(win16_dgroup_pointer(0x07d1)); int32_t accumulated = dgroup_i32((uint16_t)(0x098e + player * 4u)); uint8_t multiplier = win16_read_u8(win16_dgroup_pointer( (uint16_t)(0x09a1 + player))); tdkpin_draw_score_from_caller_frame( caller_bp, (uint16_t)borland_multiply_i32(accumulated, multiplier), (uint16_t)((uint32_t)borland_multiply_i32( accumulated, multiplier) >> 16)); } else if (id == 148) { uint8_t player = win16_read_u8(win16_dgroup_pointer(0x07d1)); win16_write_u8( win16_far_add_offset( window, (uint16_t)(player * 0x14u + 0x047b)), 1); tdkpin_restore_record_or_item_a(caller_bp, 1020); } write_i32(window, 0x0bca, 0); if (ball_count == 1) { win16_write_u16(live, 0x43, id == 148 ? 2 : 99); if (win16_read_u8(win16_dgroup_pointer(0x07c6)) == 0) { win16_write_u8(win16_far_add_offset(window, 0x0be2), 1); } else { tdkpin_finish_ball_or_advance_player(caller_bp); } } else { win16_write_u8(win16_far_add_offset(window, 0x0be1), 1); win16_write_u8( win16_far_add_offset( record_pointer((uint16_t)(ball + 173u)), 0x34), 0); win16_write_u16(live, 0x43, 2); } return false; } if (surface_distance > -11000 && contact == ball) { win16_write_u16(live, 0x43, 0); write_i32(window, 0x0bca, 0); return false; } if (surface_distance > motion->speed || surface_distance < -11000 || contact == 0) { return false; } BorlandReal48 normal_x = borland_i32_to_real48( subtract_wrap(record->point1_y_milli, previous_y)); BorlandReal48 normal_y = borland_i32_to_real48( subtract_wrap(previous_x, record->point1_x_milli)); int32_t length = distance_milli( borland_real48_round_to_i32(normal_x), borland_real48_round_to_i32(normal_y)); int32_t collision_velocity = normal_velocity_checked( motion->velocity_x, motion->velocity_y, normal_x, normal_y, length); if (collision_velocity >= 0 || absolute_wrap(collision_velocity) < absolute_wrap(surface_distance)) { return false; } make_candidate( candidate, id, record, normal_x, normal_y, collision_velocity, surface_distance); return true; } static void handle_trigger_record( TdkpinCollisionRecord *record, uint16_t id, uint16_t ball, uint16_t caller_bp, Win16FarPtr window, int32_t ball_x, int32_t ball_y, BallMotion *motion) { if (win16_read_u8(win16_dgroup_pointer(0x07c6)) != 0) { return; } win16_write_u8(win16_dgroup_pointer(0x0875), 0); int32_t radius = borland_real48_round_to_i32(record->radius_milli); if (radius <= 0) { if ((uint8_t)tdkpin_receiver_point_in_bounds( caller_bp, record->point2_y_milli, record->point2_x_milli, record->point1_y_milli, record->point1_x_milli) != 0 && (id == 153 || id == 154 || id == 6)) { motion->velocity_x = scale_real48( motion->velocity_x, g_real48_point_nine); BorlandReal48 random_component = borland_real48_multiply( borland_random_real48_registers(), g_real48_point_three); motion->velocity_y = negate_wrap(scale_real48( read_i32(window, 0x56), borland_real48_subtract(g_real48_one, random_component))); write_dgroup_i32( 0x07db, add_wrap(ball_x, motion->velocity_x)); write_dgroup_i32( 0x07df, add_wrap(ball_y, motion->velocity_y)); if ((uint8_t)tdkpin_predicted_ball_in_bounds( 0, record->point2_y_milli, record->point2_x_milli, record->point1_y_milli, record->point1_x_milli) == 0) { win16_write_u8( win16_far_add_offset(record_pointer(id), 0x34), 0); } tdkpin_restore_record_b(caller_bp, id); } return; } int32_t current_distance = absolute_wrap(distance_milli( subtract_wrap(record->point1_x_milli, ball_x), subtract_wrap(record->point1_y_milli, ball_y))); int32_t predicted_distance = absolute_wrap(distance_milli( subtract_wrap(record->point1_x_milli, dgroup_i32(0x07db)), subtract_wrap(record->point1_y_milli, dgroup_i32(0x07df)))); Win16FarPtr live = record_pointer(id); if (predicted_distance <= radius) { uint16_t flags = win16_read_u16(win16_far_add_offset(live, 0x41)); if ((flags & 0x4000u) == 0) { flags = (uint16_t)(flags | 0x4000u); win16_write_u16(live, 0x41, flags); tdkpin_play_sound_if_not_tilted(4); uint8_t player = win16_read_u8(win16_dgroup_pointer(0x07d1)); uint8_t score_factor = win16_read_u8(win16_dgroup_pointer( (uint16_t)(0x081a + player))); int32_t score = (int32_t)((uint32_t)record->score_low | ((uint32_t)record->score_high << 16)); tdkpin_add_score_from_caller_frame( caller_bp, borland_multiply_i32(score_factor, score)); if (id == 149) { win16_write_u8(win16_far_add_offset(live, 0x34), 0); win16_write_u8( win16_far_add_offset( window, (uint16_t)(player * 0x00afu + id + 0x61u)), 0); tdkpin_restore_record_b(caller_bp, id); tdkpin_apply_player_sprite_effect(caller_bp, ball); HWND16 handle = win16_read_u16(win16_far_add_offset(window, 4)); HDC16 dc = GetDC16(handle); win16_write_u16( win16_dgroup_pointer((uint16_t)(0x09a4 + player * 2u)), 0, 0); tdkpin_composite_player_sprite(0, dc); tdkpin_present_background_region(123, 42, 79, 55, dc); (void)ReleaseDC16(handle, dc); } tdkpin_handle_collision_events(caller_bp, flags, id); static const BorlandReal48 random_span = {{0x7d,0x71,0x3d,0x0a,0xd7,0x23}}; static const BorlandReal48 random_offset = {{0x81,0x71,0x3d,0x0a,0xd7,0x03}}; motion->velocity_x = scale_real48( motion->velocity_x, borland_real48_subtract( random_offset, borland_real48_multiply( borland_random_real48_registers(), random_span))); motion->velocity_y = scale_real48( motion->velocity_y, borland_real48_subtract( random_offset, borland_real48_multiply( borland_random_real48_registers(), random_span))); } } else { win16_write_u16( live, 0x41, (uint16_t)(win16_read_u16(win16_far_add_offset(live, 0x41)) & 0xbfffu)); } (void)current_distance; } static bool scan_collision_records( uint16_t caller_bp, Win16FarPtr window, uint16_t ball, uint16_t ball_count, int32_t previous_x, int32_t previous_y, int32_t predicted_x, int32_t predicted_y, uint16_t layer, BallMotion *motion, PhysicsCollisionCandidate candidates[MAX_RECORDS + 1], uint16_t *candidate_count) { int32_t closest = 999999; (void)previous_x; (void)previous_y; (void)predicted_x; (void)predicted_y; for (uint16_t id = 1; id <= MAX_RECORDS; id++) { TdkpinCollisionRecord record; load_record(id, &record); if ((record.layer_mask & layer) == 0 || record.active != 1 || !broadphase_contains_predicted_ball(&record, caller_bp)) { continue; } PhysicsCollisionCandidate candidate; bool detected = false; int32_t record_previous_x = read_i32(window, 0x0bba); int32_t record_previous_y = read_i32(window, 0x0bbe); int32_t record_predicted_x = dgroup_i32(0x07db); int32_t record_predicted_y = dgroup_i32(0x07df); if (record.type == 1) { detected = detect_circle_collision( &record, id, ball, record_previous_x, record_previous_y, record_predicted_x, record_predicted_y, motion, &candidate); } else if (record.type == 2) { detected = detect_segment_collision( &record, id, record_previous_x, record_previous_y, record_predicted_x, record_predicted_y, motion, &candidate); } else if (record.type == 3) { detected = handle_capture_record( &record, id, ball, ball_count, caller_bp, window, record_previous_x, record_previous_y, dgroup_i32(0x07d3), dgroup_i32(0x07d7), motion, &candidate); } else if (record.type == 4) { handle_trigger_record( &record, id, ball, caller_bp, window, dgroup_i32(0x07d3), dgroup_i32(0x07d7), motion); } if (detected && candidate.surface_distance <= closest) { closest = candidate.surface_distance; (*candidate_count)++; candidates[*candidate_count] = candidate; write_i32(window, 0x0bca, 0); } } return *candidate_count != 0; } static int32_t candidate_normal_length( const PhysicsCollisionCandidate *candidate) { return distance_milli( borland_real48_round_to_i32(candidate->normal_x), borland_real48_round_to_i32(candidate->normal_y)); } static bool transfer_ball_collision_impulse( const PhysicsCollisionCandidate *candidate, int32_t normal_length) { if (candidate->record_id != 174 && candidate->record_id != 175) { return false; } uint16_t slot = (uint16_t)(candidate->record_id - 174u); if (dgroup_i32((uint16_t)(0x0803 + slot * 4u)) > 0) { return false; } int32_t delta_x = borland_real48_round_to_i32(borland_real48_divide( borland_real48_multiply( borland_i32_to_real48(candidate->normal_velocity), candidate->normal_y), borland_i32_to_real48(normal_length))); int32_t delta_y = borland_real48_round_to_i32(borland_real48_divide( borland_real48_multiply( borland_i32_to_real48(candidate->normal_velocity), candidate->normal_x), borland_i32_to_real48(normal_length))); write_dgroup_i32( (uint16_t)(0x07f3 + slot * 4u), add_wrap(dgroup_i32((uint16_t)(0x07f3 + slot * 4u)), delta_x)); write_dgroup_i32( (uint16_t)(0x07fb + slot * 4u), subtract_wrap(dgroup_i32((uint16_t)(0x07fb + slot * 4u)), delta_y)); return true; } static void apply_collision_response( uint16_t caller_bp, Win16FarPtr window, uint16_t ball, uint16_t ball_count, PhysicsCollisionCandidate *candidate, BallMotion *motion) { if (candidate->flags != 0) { tdkpin_handle_collision_events( caller_bp, candidate->flags, candidate->record_id); } if (candidate->score != 0 && borland_real48_compare( candidate->response_kick, (BorlandReal48){{0,0,0,0,0,0}}) == 0) { tdkpin_add_score_from_caller_frame(caller_bp, candidate->score); } int32_t length = candidate_normal_length(candidate); int32_t projection = tangent_velocity_unchecked( motion->velocity_x, motion->velocity_y, candidate->normal_x, candidate->normal_y, length); BorlandReal48 spin_delta = borland_real48_multiply( borland_real48_multiply( borland_i32_to_real48(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)), borland_i32_to_real48(motion->speed)); (void)discarded; discarded = (BorlandReal48){{0,0,0,0,0,0}}; 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)), discarded), borland_i32_to_real48(motion->speed))), borland_real48_multiply( borland_i32_to_real48(projection), candidate->response_tangent))); motion->spin = borland_real48_add( borland_real48_multiply(motion->spin, g_real48_point_six), spin_delta); if (transfer_ball_collision_impulse(candidate, length)) { candidate->normal_velocity = subtract_wrap( candidate->normal_velocity, 1000); } int32_t impulse = borland_real48_round_to_i32(borland_real48_multiply( borland_i32_to_real48(candidate->normal_velocity), borland_real48_add(g_real48_one, candidate->response_normal))); if (win16_read_u8(win16_dgroup_pointer(0x07c6)) == 0 && borland_real48_compare( candidate->response_auxiliary, (BorlandReal48){{0,0,0,0,0,0}}) < 0) { int32_t threshold = scale_real48( read_i32(window, 0x56), candidate->response_auxiliary); if (threshold > candidate->normal_velocity) { if (candidate->score != 0) { tdkpin_add_score_from_caller_frame(caller_bp, candidate->score); } else { tdkpin_play_sound_if_not_tilted(19); } impulse = subtract_wrap( impulse, scale_real48(read_i32(window, 0x56), candidate->response_kick)); } } int32_t delta_velocity_x = borland_real48_round_to_i32( borland_real48_divide( borland_real48_subtract( borland_real48_multiply( borland_i32_to_real48(projection), candidate->normal_x), borland_real48_multiply( borland_i32_to_real48(impulse), candidate->normal_y)), borland_i32_to_real48(length))); int32_t delta_velocity_y = borland_real48_round_to_i32( borland_real48_divide( borland_real48_add( borland_real48_multiply( borland_i32_to_real48(impulse), candidate->normal_x), borland_real48_multiply( borland_i32_to_real48(projection), candidate->normal_y)), borland_i32_to_real48(length))); motion->velocity_x = add_wrap(motion->velocity_x, delta_velocity_x); motion->velocity_y = add_wrap(motion->velocity_y, delta_velocity_y); if (candidate->record_id == 25) { if (motion->speed < 1000) { win16_write_u8(win16_far_add_offset(window, 0x0bdd), 0); motion->velocity_y = 0; } motion->velocity_x = 0; } else if (candidate->record_id == 2) { if (ball_count < 2) { tdkpin_finish_ball_or_advance_player(caller_bp); } else { win16_write_u8( win16_far_add_offset( record_pointer((uint16_t)(ball + 173u)), 0x34), 0); win16_write_u8(win16_dgroup_pointer(0x0874), 0); } } } static bool predicted_outside_board(int32_t x, int32_t y) { return x <= 0 || x > 340000 || y <= 0 || y > 460000; } static void integrate_one_ball( uint16_t caller_bp, Win16FarPtr window, uint16_t ball, uint16_t ball_count, uint16_t substeps) { int32_t previous_x = dgroup_i32(0x07d3); int32_t previous_y = dgroup_i32(0x07d7); write_i32(window, 0x0bc2, previous_x); write_i32(window, 0x0bc6, previous_y); if (substeps == 0) { return; } for (uint16_t step = 1; step <= substeps; step++) { BallMotion motion = { .velocity_x = read_i32(window, 0x0baa), .velocity_y = add_wrap( read_i32(window, 0x0bae), read_i32(window, 0x5a)), .spin = read_real48(window, 0x0bce), .speed = 0, }; motion.speed = clamp_motion_speed( window, &motion.velocity_x, &motion.velocity_y); write_i32(window, 0x0baa, motion.velocity_x); write_i32(window, 0x0bae, motion.velocity_y); previous_x = dgroup_i32(0x07d3); previous_y = dgroup_i32(0x07d7); write_i32(window, 0x0bba, previous_x); write_i32(window, 0x0bbe, previous_y); int32_t predicted_x = add_wrap(previous_x, motion.velocity_x); int32_t predicted_y = add_wrap(previous_y, motion.velocity_y); write_dgroup_i32(0x07db, predicted_x); write_dgroup_i32(0x07df, predicted_y); if (predicted_outside_board(predicted_x, predicted_y)) { if (ball_count < 2) { tdkpin_finish_ball_or_advance_player(caller_bp); } else { win16_write_u8( win16_far_add_offset( record_pointer((uint16_t)(ball + 173u)), 0x34), 0); win16_write_u8(win16_dgroup_pointer(0x0874), 0); } } uint16_t layer = dgroup_i32(0x07d7) < 250000 ? 1 : 2; PhysicsCollisionCandidate candidates[MAX_RECORDS + 1]; uint16_t candidate_count = 0; bool collided = scan_collision_records( caller_bp, window, ball, ball_count, previous_x, previous_y, predicted_x, predicted_y, layer, &motion, candidates, &candidate_count); if (collided) { /* Raw 1000:c79c stores each later, nearer candidate at the next * 0x34-byte stack slot but resolves the fixed first slot at * SS:...d8a2. Record order therefore wins over closest distance. */ apply_collision_response( caller_bp, window, ball, ball_count, &candidates[1], &motion); } if (win16_read_u8(win16_far_add_offset(window, 0x0bdd)) != 0) { write_real48(window, 0x0bce, motion.spin); write_i32(window, 0x0baa, motion.velocity_x); write_i32(window, 0x0bae, motion.velocity_y); motion.speed = clamp_motion_speed( window, &motion.velocity_x, &motion.velocity_y); write_i32(window, 0x0baa, motion.velocity_x); write_i32(window, 0x0bae, motion.velocity_y); write_dgroup_i32( 0x07d3, add_wrap(dgroup_i32(0x07d3), motion.velocity_x)); write_dgroup_i32( 0x07d7, add_wrap(dgroup_i32(0x07d7), motion.velocity_y)); tdkpin_update_dynamic_ball_record(caller_bp, ball); } if (collided || win16_read_u8(win16_far_add_offset(window, 0x0be2)) == 1 || win16_read_u8(win16_far_add_offset(window, 0x0be1)) == 1) { break; } } } void tdkpin_simulate_ball_and_dispatch_collision(uint16_t caller_bp) { Win16FarPtr window = caller_receiver(caller_bp); win16_write_u32( window, 0x4d, win16_read_u32(win16_far_add_offset(window, 0x4d)) + 1u); uint16_t substeps = win16_read_u16(win16_dgroup_pointer(0x0871)); uint16_t ball_count = win16_read_u8(win16_far_add_offset(window, 0x61)); for (uint16_t ball = 1; ball <= ball_count; ball++) { if (win16_read_u8(win16_far_add_offset(window, 0x61)) > 1) { tdkpin_load_ball_slot(caller_bp, (uint16_t)(ball - 1u)); } integrate_one_ball(caller_bp, window, ball, ball_count, substeps); if (win16_read_u8(win16_far_add_offset(window, 0x0bdd)) != 0 && win16_read_u8(win16_far_add_offset(window, 0x0be2)) == 0) { tdkpin_render_ball_swept_region(caller_bp); } if (win16_read_u8(win16_far_add_offset(window, 0x61)) > 1) { tdkpin_save_ball_slot(caller_bp, (uint16_t)(ball - 1u)); } } }