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
+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;
}