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
This commit is contained in:
2026-08-22 18:44:50 +02:00
parent 99a6082b2a
commit 75c2464e72
2 changed files with 62 additions and 17 deletions
+9 -4
View File
@@ -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.
+53 -13
View File
@@ -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);