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