From 75c2464e72cf046445d7ce6ebc028e3cd3d1fdf4 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sat, 22 Aug 2026 18:44:50 +0200 Subject: [PATCH] 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 --- original/MECHANICS_PROGRESS.md | 13 ++++-- original/TDKPIN_MECHANICS_RESTORED.c | 66 ++++++++++++++++++++++------ 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/original/MECHANICS_PROGRESS.md b/original/MECHANICS_PROGRESS.md index f3e1b90..66f5984 100644 --- a/original/MECHANICS_PROGRESS.md +++ b/original/MECHANICS_PROGRESS.md @@ -42,17 +42,22 @@ renamed. 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. +- The release-coordinate projection is `(raw + 500) / 1000` for rendering; + decoded physics centers are `(258,78)`, `(270,94)`, `(275,97)`, and + `(325,92)` for the four selectable terminal frames. +- The speed scalar is 3800 millipixels per tick. Its Borland Real48 direction + constants are decoded for all release cases in the readable C companion. ## 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. +- Differentially verify the recovered 10/20/30/40/50 ms timer choices against + live original captures; the Rust port intentionally uses the default 30 ms + claw cadence independently of render rate. - 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 +Those 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. diff --git a/original/TDKPIN_MECHANICS_RESTORED.c b/original/TDKPIN_MECHANICS_RESTORED.c index 899e00a..826af67 100644 --- a/original/TDKPIN_MECHANICS_RESTORED.c +++ b/original/TDKPIN_MECHANICS_RESTORED.c @@ -51,13 +51,18 @@ typedef struct { typedef struct { uint16_t low; int16_t high; -} Fixed32Words; +} MillipixelWords; typedef struct { - Fixed32Words x; - Fixed32Words y; + 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. @@ -154,9 +159,10 @@ static void render_claw(uint16_t frame, ClawSpriteBank bank) 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. + * 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) { @@ -186,6 +192,44 @@ static RawClawReleasePosition raw_claw_release_position(uint16_t frame) } } +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) { @@ -231,13 +275,9 @@ static void update_claw(ClawState *claw) 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 */ - } + 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);