docs(reverse): transcribe the fixed-point physics core

Add a standalone, syntax-checked C transcription of the recovered ball state,
10 ms substep, launcher repeat/release arithmetic, one-sided type-2 response,
swept type-1 response, and previous-position contact application. Link the
source to the authoritative raw function addresses and initialized object
ledger.

Keep unresolved persistent-contact and special-rule branches in the raw Ghidra
artifact rather than filling the readable reference with guesses.

Test Plan:
- strict C11 `clang -Wall -Wextra -Werror -fsyntax-only` -- passed
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-22 21:29:41 +02:00
parent 5e63772967
commit cd27fc8404
2 changed files with 291 additions and 0 deletions
+289
View File
@@ -0,0 +1,289 @@
/*
* Readable fixed-point physics transcription for TDKPIN.EXE.
*
* Authority:
* SHA-256 a9022f1894e3e6e21fc42e8f6c932f7c549ca77f63aaa0c488bb9d55d9d0174c
*
* Primary raw routine:
* FUN_1000_c79c at 1000:c79c (movement, object scan, response dispatch)
*
* Supporting routines:
* FUN_1000_638e / FUN_1000_6bf8 (launcher key release/press)
* FUN_1000_7ed9 / FUN_1000_8b0d (moving flipper records)
* FUN_1000_b476 (rule flags)
*
* This file transcribes behavior that has both static and live differential
* evidence. It is not claimed to be the vendor's source. Unrestored rule and
* persistent-contact branches remain in TDKPIN_GHIDRA_RAW.c rather than being
* replaced here with plausible code.
*/
#include <math.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
enum {
PHYSICS_SUBSTEP_MILLISECONDS = 10,
GRAVITY_MILLIPIXELS_PER_SUBSTEP = 15,
MAX_SPEED_MILLIPIXELS_PER_SUBSTEP = 3800,
LAUNCHER_IMPULSE_MILLIPIXELS = 375,
COLLISION_OBJECT_COUNT = 175,
};
typedef struct {
int32_t x;
int32_t y;
} Vec2Milli;
typedef enum {
OBJECT_TYPE_CIRCLE = 1,
OBJECT_TYPE_LINE = 2,
OBJECT_TYPE_LOCK_OR_MECHANISM = 3,
OBJECT_TYPE_TARGET_SENSOR = 4,
} CollisionObjectType;
/* Semantic view of one initialized 0x53-byte record from OBJECTS.tsv. */
typedef struct {
uint8_t id;
CollisionObjectType type; /* record +0x00 relative to field base 0x095b */
uint8_t subtype; /* +0x01 */
bool active; /* +0x34 */
Vec2Milli bounds_min; /* +0x02 / +0x06 */
Vec2Milli bounds_max; /* +0x0a / +0x0e */
Vec2Milli point1; /* +0x12 / +0x16 */
Vec2Milli point2; /* +0x1a / +0x1e */
int32_t radius; /* Real48 at +0x22, decoded to millipixels */
double normal_rebound; /* Real48 at +0x28 */
double tangent_coupling; /* Real48 at +0x2e */
double auxiliary; /* Real48 at +0x35 */
double normal_kick; /* Real48 at +0x3b */
uint16_t flags; /* +0x41 */
uint16_t contact_state; /* +0x43 */
uint32_t score; /* +0x45 / +0x47 */
uint16_t layer_mask; /* +0x49 */
} CollisionRecord;
typedef struct {
Vec2Milli position; /* DAT_1028_07d3 / DAT_1028_07d7 */
Vec2Milli predicted; /* DAT_1028_07db / DAT_1028_07df */
Vec2Milli velocity; /* window object +0xbaa / +0xbae */
Vec2Milli previous_position; /* window object +0xbba / +0xbbe */
} BallPhysics;
typedef struct {
bool hit;
double progress;
Vec2Milli response_velocity;
uint8_t object_id;
} CollisionCandidate;
static int64_t cross(Vec2Milli left, Vec2Milli right)
{
return (int64_t)left.x * right.y - (int64_t)left.y * right.x;
}
static Vec2Milli subtract(Vec2Milli left, Vec2Milli right)
{
return (Vec2Milli){left.x - right.x, left.y - right.y};
}
static Vec2Milli add(Vec2Milli left, Vec2Milli right)
{
return (Vec2Milli){left.x + right.x, left.y + right.y};
}
/* Type-2 branch inside 1000:c79c. The registered direction is significant. */
static CollisionCandidate line_candidate(
Vec2Milli old_position,
Vec2Milli velocity,
const CollisionRecord *record)
{
Vec2Milli line = subtract(record->point2, record->point1);
Vec2Milli from_ball = subtract(record->point1, old_position);
int64_t denominator = cross(velocity, line);
int64_t path_numerator;
int64_t line_numerator;
double length;
double tangent_x;
double tangent_y;
double normal_x;
double normal_y;
double normal_speed;
double tangent_speed;
double outgoing_normal;
double outgoing_tangent;
if (denominator == 0) {
return (CollisionCandidate){0};
}
path_numerator = cross(from_ball, line);
line_numerator = cross(from_ball, velocity);
if (denominator > 0) {
if (path_numerator <= 0 || path_numerator > denominator ||
line_numerator < 0 || line_numerator > denominator) {
return (CollisionCandidate){0};
}
} else if (path_numerator >= 0 || path_numerator < denominator ||
line_numerator > 0 || line_numerator < denominator) {
return (CollisionCandidate){0};
}
length = hypot((double)line.x, (double)line.y);
if (length == 0.0) {
return (CollisionCandidate){0};
}
tangent_x = line.x / length;
tangent_y = line.y / length;
normal_x = -tangent_y;
normal_y = tangent_x;
normal_speed = velocity.x * normal_x + velocity.y * normal_y;
if (normal_speed <= 0.0) { /* back side is a one-way pass */
return (CollisionCandidate){0};
}
tangent_speed = velocity.x * tangent_x + velocity.y * tangent_y;
outgoing_normal = -record->normal_rebound * normal_speed;
outgoing_tangent = tangent_speed + record->tangent_coupling * normal_speed;
return (CollisionCandidate){
.hit = true,
.progress = (double)path_numerator / (double)denominator,
.response_velocity = {
(int32_t)llround(normal_x * outgoing_normal + tangent_x * outgoing_tangent),
(int32_t)llround(normal_y * outgoing_normal + tangent_y * outgoing_tangent),
},
.object_id = record->id,
};
}
/* Type-1 branch inside 1000:c79c. */
static CollisionCandidate circle_candidate(
Vec2Milli old_position,
Vec2Milli velocity,
const CollisionRecord *record)
{
Vec2Milli offset = subtract(old_position, record->point1);
int64_t old_distance_squared =
(int64_t)offset.x * offset.x + (int64_t)offset.y * offset.y;
int64_t radius_squared = (int64_t)record->radius * record->radius;
double a;
double b;
double c;
double discriminant;
double progress;
double hit_x;
double hit_y;
double hit_length;
double normal_x;
double normal_y;
double tangent_x;
double tangent_y;
double normal_speed;
double tangent_speed;
double outgoing_normal;
double outgoing_tangent;
if (old_distance_squared <= radius_squared) {
return (CollisionCandidate){0}; /* persistent state suppresses repeats */
}
a = (double)velocity.x * velocity.x + (double)velocity.y * velocity.y;
if (a == 0.0) {
return (CollisionCandidate){0};
}
b = 2.0 * ((double)offset.x * velocity.x + (double)offset.y * velocity.y);
c = (double)old_distance_squared - (double)radius_squared;
discriminant = b * b - 4.0 * a * c;
if (discriminant < 0.0) {
return (CollisionCandidate){0};
}
progress = (-b - sqrt(discriminant)) / (2.0 * a);
if (progress <= 0.0 || progress > 1.0) {
return (CollisionCandidate){0};
}
hit_x = offset.x + velocity.x * progress;
hit_y = offset.y + velocity.y * progress;
hit_length = hypot(hit_x, hit_y);
if (hit_length == 0.0) {
return (CollisionCandidate){0};
}
normal_x = hit_x / hit_length;
normal_y = hit_y / hit_length;
tangent_x = normal_y; /* clockwise tangent */
tangent_y = -normal_x;
normal_speed = velocity.x * normal_x + velocity.y * normal_y;
if (normal_speed >= 0.0) {
return (CollisionCandidate){0};
}
tangent_speed = velocity.x * tangent_x + velocity.y * tangent_y;
outgoing_normal = -record->normal_rebound * normal_speed +
record->normal_kick * MAX_SPEED_MILLIPIXELS_PER_SUBSTEP;
outgoing_tangent = tangent_speed - record->tangent_coupling * normal_speed;
return (CollisionCandidate){
.hit = true,
.progress = progress,
.response_velocity = {
(int32_t)llround(normal_x * outgoing_normal + tangent_x * outgoing_tangent),
(int32_t)llround(normal_y * outgoing_normal + tangent_y * outgoing_tangent),
},
.object_id = record->id,
};
}
static void begin_substep(BallPhysics *ball)
{
double speed;
ball->velocity.y += GRAVITY_MILLIPIXELS_PER_SUBSTEP;
speed = hypot((double)ball->velocity.x, (double)ball->velocity.y);
if (speed > MAX_SPEED_MILLIPIXELS_PER_SUBSTEP) {
double scale = MAX_SPEED_MILLIPIXELS_PER_SUBSTEP / speed;
ball->velocity.x = (int32_t)llround(ball->velocity.x * scale);
ball->velocity.y = (int32_t)llround(ball->velocity.y * scale);
}
ball->previous_position = ball->position;
ball->predicted = add(ball->position, ball->velocity);
}
static void apply_candidate(BallPhysics *ball, CollisionCandidate candidate)
{
if (candidate.hit) {
ball->velocity = candidate.response_velocity;
}
/* The original advances from the previous position, not the hit point. */
ball->position = add(ball->previous_position, ball->velocity);
}
/* Keyboard-repeat behavior recovered from 1000:6bf8 and 1000:638e. */
static void launcher_down_event(BallPhysics *ball)
{
ball->velocity.x = 0;
ball->velocity.y -= LAUNCHER_IMPULSE_MILLIPIXELS;
}
/* random_less_than_3800 is Borland random(3800), FUN_1020_14a4. */
static void launcher_release_event(BallPhysics *ball, uint16_t random_less_than_3800)
{
launcher_down_event(ball); /* release applies one additional impulse */
if (ball->velocity.y < -MAX_SPEED_MILLIPIXELS_PER_SUBSTEP) {
ball->velocity.y = -MAX_SPEED_MILLIPIXELS_PER_SUBSTEP +
random_less_than_3800 / 40;
}
}
/* Keep pure helpers referenced for strict syntax/warning checks. */
typedef struct {
CollisionCandidate (*line)(Vec2Milli, Vec2Milli, const CollisionRecord *);
CollisionCandidate (*circle)(Vec2Milli, Vec2Milli, const CollisionRecord *);
void (*begin)(BallPhysics *);
void (*apply)(BallPhysics *, CollisionCandidate);
void (*launcher_down)(BallPhysics *);
void (*launcher_release)(BallPhysics *, uint16_t);
} RestoredPhysicsFunctions;
const RestoredPhysicsFunctions TDKPIN_RESTORED_PHYSICS = {
line_candidate,
circle_candidate,
begin_substep,
apply_candidate,
launcher_down_event,
launcher_release_event,
};