Files
tdkpin/original/TDKPIN_MECHANICS_RESTORED.c
ddidderr 8b99e9607c feat(reconstruction): complete binary-backed C recovery
Replace the partial mechanics transcriptions with a separate, readable C11
reconstruction of the complete Win16 image while preserving the original raw
Ghidra export as immutable evidence. Cover all ordinary and overlapping entry
points, Borland runtime behavior, Win16 imports, segmented data, callbacks,
resources, indirect control flow, physics, rendering, persistence, and
startup/shutdown lifecycles.

Add deterministic extraction and audit tooling plus address-linked ledgers for
functions, imports, DGROUP ranges and objects, relocations, resources, and
callbacks. The final gate records zero raw, partial, restored, unknown,
blocked, or unclassified required units. Keep the semantic-fidelity boundary
explicit: the portable C is not claimed to reproduce a byte-identical Borland
NE build.

Add strict focused harnesses for every reconstructed C unit, exact resource
round-trip checks, and a 16-bit Borland Real48 reference probe. No Rust source
or Cargo metadata is changed in this phase.

Test Plan:
- `bash original/tools/test_reconstructed_c.sh` -- passed
- `bash original/tools/probe_real48_reference.sh` -- passed bit-for-bit
- `python3 original/tools/audit_reconstruction.py --require-complete` -- passed
- `git diff --cached --check` -- passed
- `git diff HEAD -- '*.rs' Cargo.toml Cargo.lock` -- empty
2026-08-23 16:41:17 +02:00

287 lines
8.9 KiB
C

/*
* HISTORICAL PARTIAL mechanics transcription for TDKPIN.EXE.
*
* Target SHA-256:
* a9022f1894e3e6e21fc42e8f6c932f7c549ca77f63aaa0c488bb9d55d9d0174c
*
* This superseded intermediate is retained only as provenance for the earlier
* live-probe work. It is not part of the completed readable-C deliverable.
* The authoritative complete address-linked implementations are under
* reconstructed/, especially tdkpin_flipper_collision.c,
* tdkpin_flipper_bitmap_geometry.c, tdkpin_physics.c, and tdkpin_timer_tick.c.
*/
#include <stdbool.h>
#include <stdint.h>
enum {
SOUND_RESOURCE_BASE = 2000,
SOUND_CLAW_CAPTURE = SOUND_RESOURCE_BASE + 15,
SOUND_CLAW_RELEASE = SOUND_RESOURCE_BASE + 16,
SOUND_FLIPPER_MOVE = SOUND_RESOURCE_BASE + 21,
};
typedef struct {
int16_t x;
int16_t y;
int16_t width;
int16_t height;
} RectI16;
typedef struct {
bool left_control_down; /* object +0xbda; scan code 0x1d */
bool keypad_enter_down; /* object +0xbdb; scan code 0x1c */
int16_t left_position; /* object +0xbd4; recovered range 0..1 */
int16_t right_position; /* object +0xbd6; recovered range 0..1 */
} FlipperState;
typedef enum {
CLAW_OPENING_OR_IDLE = 0,
CLAW_CLOSING = 1,
} ClawSpriteBank;
typedef struct {
bool active; /* DAT_1028_07ca */
uint16_t frame; /* DAT_1028_086d; initialized to 10 */
uint16_t target_frame; /* DAT_1028_086f; initialized to 10 */
ClawSpriteBank bank; /* DAT_1028_0873 */
bool animation_blocked; /* DAT_1028_0874 */
bool ball_suspended; /* game object +0xbdf */
} ClawState;
typedef struct {
uint16_t low;
int16_t high;
} MillipixelWords;
typedef struct {
MillipixelWords x;
MillipixelWords y;
} RawClawReleasePosition;
typedef struct {
float x;
float y;
} Vec2F;
/*
* FUN_1008_0002 at 1008:0002.
* The called loader/player resolves argument N as Win16 WAVE resource 2000+N.
*/
static void play_sound_number(uint16_t number)
{
if (sound_is_not_muted() && sound_device_is_available()) {
play_wave_resource(SOUND_RESOURCE_BASE + number);
}
}
/*
* FUN_1000_638e / FUN_1000_6bf8 (keyboard release/press branches).
* The original controls are binary input flags. Motion happens later in
* update_flippers(), once per changed edge.
*/
static void set_flipper_key(FlipperState *state, uint8_t scan_code, bool down)
{
if (scan_code == 0x1d) {
state->left_control_down = down;
} else if (scan_code == 0x1c) {
state->keypad_enter_down = down;
}
}
/*
* FUN_1000_7ed9 at 1000:7ed9 and FUN_1000_8b0d at 1000:8b0d.
* delta is -1 for key-down and +1 for key-up. The first routine updates
* the live collision geometry and always starts sound 2021; the second moves
* the matching rendered flipper geometry.
*/
static void move_flipper(int flipper_number, int delta)
{
play_sound_number(21);
update_flipper_collision_geometry(flipper_number, delta);
update_flipper_bitmap_geometry(flipper_number, delta);
}
/* FUN_1000_99f0 at 1000:99f0. */
static void update_flippers(FlipperState *state)
{
if (state->left_control_down && state->left_position == 0) {
state->left_position++;
move_flipper(1, -1);
} else if (!state->left_control_down && state->left_position != 0) {
state->left_position--;
move_flipper(1, +1);
}
if (state->keypad_enter_down && state->right_position == 0) {
state->right_position++;
move_flipper(2, -1);
} else if (!state->keypad_enter_down && state->right_position != 0) {
state->right_position--;
move_flipper(2, +1);
}
}
/*
* FUN_1008_0e08 at 1008:0e08.
* Resource 900 is a 900x296 sheet: nine 100x74 frames in each of four rows.
* Closing uses rows 0/74; opening and returning use rows 148/222. The
* destination is the logical playfield rectangle x=238, y=47, w=100, h=74.
*/
static RectI16 claw_source_rect(uint16_t frame, ClawSpriteBank bank)
{
uint16_t column_frame = frame;
int16_t source_y = bank == CLAW_CLOSING ? 0 : 148;
if (column_frame > 9) {
source_y += 74;
column_frame -= 9;
}
return (RectI16){
.x = (int16_t)((column_frame - 1) * 100),
.y = source_y,
.width = 100,
.height = 74,
};
}
static void render_claw(uint16_t frame, ClawSpriteBank bank)
{
const RectI16 destination = {238, 47, 100, 74};
if (frame == 0) {
restore_playfield_background(destination);
} else {
blit_claw_sprite(claw_source_rect(frame, bank), destination);
}
}
/* The only terminal frames selected by the collision branch at 1000:c79c. */
static const uint16_t CLAW_TERMINAL_FRAMES[4] = {1, 6, 7, 18};
/*
* Raw signed 32-bit millipixel positions assigned by export ordinal 10 at
* 1000:eab1. FUN_1000_9bca adds 500, divides by 1000, then subtracts the
* eight-pixel ball radius only for bitmap placement. The physics position is
* therefore the raw value divided by 1000, not a 16.16 fixed-point number.
*/
static RawClawReleasePosition raw_claw_release_position(uint16_t frame)
{
switch (frame) {
case 1:
case 3:
return (RawClawReleasePosition){{0xefd0, 3}, {0x30b0, 1}};
case 2:
return (RawClawReleasePosition){{0xf3b8, 3}, {0x3c68, 1}};
case 4:
return (RawClawReleasePosition){{0xfb88, 3}, {0x4ff0, 1}};
case 5:
return (RawClawReleasePosition){{0x0f10, 4}, {0x5f90, 1}};
case 6:
return (RawClawReleasePosition){{0x1eb0, 4}, {0x6f30, 1}};
case 7:
return (RawClawReleasePosition){{0x3238, 4}, {0x7ae8, 1}};
case 8:
return (RawClawReleasePosition){{0x3df0, 4}, {0x7ed0, 1}};
case 9:
return (RawClawReleasePosition){{0x4d90, 4}, {0x8a88, 1}};
case 18:
return (RawClawReleasePosition){{0xf588, 4}, {0x6760, 1}};
default:
unreachable_original_state();
return (RawClawReleasePosition){{0, 0}, {0, 0}};
}
}
static int32_t join_millipixel_words(MillipixelWords value)
{
return ((int32_t)value.high << 16) | value.low;
}
static Vec2F claw_release_position_pixels(uint16_t frame)
{
RawClawReleasePosition raw = raw_claw_release_position(frame);
return (Vec2F){
join_millipixel_words(raw.x) / 1000.0f,
join_millipixel_words(raw.y) / 1000.0f,
};
}
/*
* Real48 constants loaded in 1000:eab1 after converting speed scalar 3800.
* The result is millipixels per timer tick. Only 1, 6, 7, and 18 are selected
* as random terminal frames, but all switch cases are retained here.
*/
static Vec2F claw_release_velocity_millipixels_per_tick(uint16_t frame)
{
switch (frame) {
case 1: return (Vec2F){3800.0f * -0.60f, 3800.0f * 0.00f};
case 2: return (Vec2F){3800.0f * -0.45f, 3800.0f * 0.05f};
case 3: return (Vec2F){3800.0f * -0.70f, 3800.0f * 0.30f};
case 4: return (Vec2F){3800.0f * -0.30f, 3800.0f * 0.20f};
case 5: return (Vec2F){3800.0f * -0.32f, 3800.0f * 0.22f};
case 6: return (Vec2F){3800.0f * -0.40f, 3800.0f * 0.60f};
case 7: return (Vec2F){3800.0f * -0.31f, 3800.0f * 0.70f};
case 8: return (Vec2F){3800.0f * -0.20f, 3800.0f * 0.80f};
case 9: return (Vec2F){3800.0f * -0.10f, 3800.0f * 0.90f};
case 18: return (Vec2F){0.0f, 1000.0f};
default:
unreachable_original_state();
return (Vec2F){0.0f, 0.0f};
}
}
/* Object-id 0x59 branch inside FUN_1000_c79c at 1000:c79c. */
static void begin_claw_capture(ClawState *claw, uint16_t random_0_to_3)
{
if (claw->animation_blocked) {
return;
}
claw->active = true;
claw->target_frame = CLAW_TERMINAL_FRAMES[random_0_to_3];
claw->bank = CLAW_CLOSING;
claw->ball_suspended = true;
zero_ball_motion();
/* These moving collision records are absent while the arm holds the ball. */
set_collision_objects_enabled(12, 20, false);
play_sound_number(15); /* recovered call immediately before this branch */
}
/* Claw branch of exported ordinal 10 at 1000:eab1. */
static void update_claw(ClawState *claw)
{
if (!claw->active || claw->animation_blocked) {
return;
}
if (claw->frame != claw->target_frame) {
claw->frame += claw->frame < claw->target_frame ? 1 : -1;
render_claw(claw->frame, claw->bank);
return;
}
if (claw->frame == 10) {
render_claw(0, CLAW_OPENING_OR_IDLE);
if (claw->bank == CLAW_OPENING_OR_IDLE) {
claw->active = false;
set_collision_objects_enabled(12, 20, true);
}
return;
}
/* Terminal closing frame: redraw from the opening bank, release, return. */
render_claw(claw->frame, CLAW_OPENING_OR_IDLE);
claw->target_frame = 10;
claw->bank = CLAW_OPENING_OR_IDLE;
reset_ball_forces();
set_ball_position_pixels(claw_release_position_pixels(claw->frame));
set_ball_velocity_millipixels_per_tick(
claw_release_velocity_millipixels_per_tick(claw->frame));
commit_ball_position();
play_sound_number(16);
claw->ball_suspended = false;
}