docs(reverse): restore named mechanism state machines

Transcribe the claw and flipper control flow into an address-mapped readable companion while preserving unresolved fixed-point projection as an explicit boundary.

Correct the reconstruction ledger so the current Rust claw is not described as proven parity.

Test Plan:
- git diff --cached --check
- Review restored constants and branches against TDKPIN_GHIDRA_RAW.c
This commit is contained in:
2026-08-22 18:38:07 +02:00
parent 95760b9940
commit e1dc644c76
3 changed files with 304 additions and 1 deletions
+58
View File
@@ -0,0 +1,58 @@
# Mechanics reconstruction progress
This ledger scopes the current reverse-engineering pass to the claw and
flippers. The preserved executable is the authority; `TDKPIN_GHIDRA_RAW.c`
remains immutable raw evidence, while `TDKPIN_MECHANICS_RESTORED.c` carries
reviewed names and control flow.
Target: `TDKPIN.EXE` SHA-256
`a9022f1894e3e6e21fc42e8f6c932f7c549ca77f63aaa0c488bb9d55d9d0174c`.
## Function inventory
| Address | Raw name | Restored role | Status |
| --- | --- | --- | --- |
| `1000:638e` | `FUN_1000_638e` | key-release flag updates | reviewed |
| `1000:6bf8` | `FUN_1000_6bf8` | key-press flag updates | reviewed |
| `1000:7ed9` | `FUN_1000_7ed9` | flipper collision movement and sound | reviewed; inner fixed-point geometry still raw |
| `1000:8b0d` | `FUN_1000_8b0d` | flipper bitmap/collider movement | reviewed; inner fixed-point geometry still raw |
| `1000:99f0` | `FUN_1000_99f0` | binary flipper transition state machine | restored |
| `1000:c79c` | `FUN_1000_c79c` | collision dispatch, including claw object `0x59` | claw branch restored |
| `1000:eab1` | exported ordinal 10 | timer tick, claw release and return | claw branch restored |
| `1008:0002` | `FUN_1008_0002` | sound-enabled dispatch | restored |
| `1008:0e08` | `FUN_1008_0e08` | claw background restore and sprite slicing | restored |
Inventory coverage is 9/9 functions inspected for this mechanism pass. This is
not a claim that all instructions in the two large physics functions have been
renamed.
## Proven behavior now available to the Rust port
- Left Control (scan `0x1d`) and keypad Enter (scan `0x1c`) set input flags;
the timer changes each flipper position only between 0 and 1.
- Every flipper movement edge, including return, invokes sound number 21,
Win16 WAVE resource 2021.
- Claw collision object `0x59` suspends the ball, zeros its live motion,
disables collision records 12 through 20, and selects terminal frame 1, 6,
7, or 18.
- The claw advances exactly one frame per timer tick to its terminal, releases
the ball, then advances one frame per tick back to frame 10 before clearing
the overlay and restoring collision records 12 through 20.
- Resource 900 is sliced into 100x74 frames, nine columns by four rows. The
destination is `(238,47)`; closing rows begin at source y=0 and opening rows
at source y=148.
- Claw capture/contact uses sound 2015; release uses sound 2016.
## Explicit pending evidence
- Decode the runtime/FPU conversion used to project the claw's raw 16.16
release coordinates and non-frame-18 velocity into playfield pixels.
- Recover or differentially measure the timer cadence used for each claw frame.
- Transcribe the full fixed-point flipper impulse calculation, then compare it
with the Rust floating-point collision response.
- Validate the reconstructed sequences against deterministic Rust traces and
captured frames from the original executable.
Those four items are intentionally not counted as semantic parity. The readable
C file preserves their raw constants and labels the unresolved conversion so a
later implementation cannot silently turn an estimate into claimed evidence.
+245
View File
@@ -0,0 +1,245 @@
/*
* Readable mechanics transcription for TDKPIN.EXE.
*
* Target SHA-256:
* a9022f1894e3e6e21fc42e8f6c932f7c549ca77f63aaa0c488bb9d55d9d0174c
*
* This is a named, reviewable companion to TDKPIN_GHIDRA_RAW.c. It is not
* claimed to be the vendor's source or a buildable replacement. Every
* restored routine below names its raw address, and unresolved conversions
* stay explicit instead of being filled with guessed values.
*/
#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;
} Fixed32Words;
typedef struct {
Fixed32Words x;
Fixed32Words y;
} RawClawReleasePosition;
/*
* 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 16.16 positions assigned by export ordinal 10 at 1000:eab1.
* These words are preserved until the original coordinate projection is
* fully decoded; do not substitute guessed screen pixels here.
*/
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}};
}
}
/* 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_raw(raw_claw_release_position(claw->frame));
if (claw->frame == 18) {
set_ball_velocity_raw(0, 1000);
} else {
set_ball_velocity_from_original_speed_scalar(); /* projection pending */
}
commit_ball_position();
play_sound_number(16);
claw->ball_suspended = false;
}
+1 -1
View File
@@ -25,7 +25,7 @@ implementation.
| Playfield collision layout | Recovered | All 109 active type-2 line objects and 40 static active type-1 circles are transcribed from the original 175-object registration table. The registration routine converts its sideways inputs with `screen = (y, x - 20)` and accumulates explicitly relative objects. Its circle sizes are complete center-contact extents, so the rendered ball radius is not added a second time. Object 174 is omitted because the original overwrites it with the live ball every frame. Moving flippers use equivalent native Rust bodies. |
| Ball launcher | Recovered | The initial 32-bit fixed-point coordinates decode to `(325, 413)` in the right shooter lane. Scan code `0x50` compresses the eleven frames in resource 901 while Down is held; the key-release routine activates the ball with the accumulated vertical launch velocity. |
| Physics arithmetic | Reimplemented | The Win16 fixed-point/timer engine is replaced by deterministic fixed-step floating-point integration. Restitution and impulses are tuned to the recovered table but are not instruction-for-instruction equivalents. |
| Rules | Behaviorally recovered | Player count, controls, wheel holes, magnetic saves, robot grip, four-position ball lock, target banks, increasing bumper value, nine-part TDK diamond, permanent double scoring for a completed diamond, KByte media progression, and media extra balls follow the original help and code paths. |
| Rules | Partly recovered | Player count, controls, wheel holes, magnetic saves, four-position ball lock, target banks, increasing bumper value, nine-part TDK diamond, permanent double scoring for a completed diamond, KByte media progression, and media extra balls follow the original help and code paths. The claw state machine has now been recovered in readable form, but its raw release-coordinate projection is still being decoded before the Rust behavior can claim parity. |
| Numeric scoring | Partly inferred | Visible 2000-6000 target values and recovered registration values are preserved. Some bumper, bank-completion, robot, wheel, lock, and media thresholds are best-evidence reconstructions because the decompiler did not recover meaningful names or a clean rule table. |
| High scores | Compatible import | The original 276-byte table is decoded as ten `IWIK`-XOR-obfuscated little-endian scores plus ten 22-byte names, sorted, then migrated to portable JSON. |
| Configuration | Behaviorally compatible | Sound, language, and five detail levels are retained. Storage moves from a local Win16 INI file to the platform user-data directory. |