Files
tdkpin/original/TDKPIN_MECHANICS_RESTORED.c
T
ddidderr 75c2464e72 docs(reverse): decode claw release arithmetic
Correct the coordinate model from guessed 16.16 values to the millipixel arithmetic shown by the 16-bit division routine.

Decode the Borland Real48 direction constants and record the exact position and velocity tables used by every claw terminal.

Test Plan:
- git diff --cached --check
- Compare constants against segment 1 disassembly at 9bca and eab1
2026-08-22 18:44:50 +02:00

286 lines
8.8 KiB
C

/*
* 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;
} 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;
}