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:
@@ -16,6 +16,8 @@ the output are therefore analysis-generated identifiers.
|
||||
## Outputs
|
||||
|
||||
- `TDKPIN_GHIDRA_RAW.c`: C-like Ghidra output for every recovered in-image function.
|
||||
- `TDKPIN_PHYSICS_RESTORED.c`: syntax-checked, address-linked transcription of
|
||||
the fixed-point state and recovered type-1/type-2 response arithmetic.
|
||||
- `FUNCTIONS.tsv`: address ledger separating in-image functions from imported APIs.
|
||||
- `COVERAGE.tsv`: executable-block, instruction, function-body, and undefined-range audit.
|
||||
- `OBJECTS.tsv`: all 175 initialized collision/rule records, including geometry,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user