Files
tdkpin/original/reconstructed/tests/test_timer_tick.c
T
ddidderr 7633ef9990 fix(attract): restore all remainder-driven cycles
The same raw Div32 pattern used by the item animation appears in every nested
idle phase: after division by 32, 40, or 16, the binary copies the remainder
from CX:BX before the second division. Readable C and Rust used quotients for
targets, word quads, the record strip, and the four-point chase, turning short
repeating chases into slow one-shot progressions.

Use `%32/2`, `%32/4`, `%40/4`, and `%16/4` for those cycles alongside the
already corrected `%144/8` items. Preserve the first-cycle phase-zero delay,
then repeat each incremental overlay state exactly. Add C and Rust coverage at
phase activation, reversal, deactivation, and wrap boundaries.

Test Plan:
- raw instruction review at `1000:01b7-063b` -- all remainder transfers confirmed
- `cargo test --workspace --all-targets --all-features` -- 125 passed
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` -- passed
- `bash original/tools/test_reconstructed_c.sh` -- passed
- `python3 original/tools/audit_reconstruction.py --require-complete` -- passed with zero incomplete or unclassified units
- `cargo run -- --simulate attract --step 145 --screenshot /tmp/tdkpin-attract-remainder.png` -- passed; visually inspected at 640x460
- `rumdl check --flavor commonmark RECONSTRUCTION.md CHANGELOG.md` -- passed
- `git diff --cached --check` -- passed
2026-08-23 20:53:12 +02:00

598 lines
19 KiB
C

#include "../tdkpin_timer_tick.h"
#include "../tdkpin_borland_runtime.h"
#include "../tdkpin_collision_records.h"
#include <assert.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
HDC16 destination;
INT16 x;
INT16 y;
INT16 width;
INT16 height;
HDC16 source;
INT16 source_x;
INT16 source_y;
uint32_t raster_operation;
} BlitCall;
static uint8_t g_dgroup[0x5000];
static uint8_t g_objects[0x1000];
static uint8_t g_stack[0x1000];
static Win16FarPtr g_window;
static uint16_t g_refresh_ids[200];
static size_t g_refresh_count;
static uint16_t g_restore_a_ids[32];
static size_t g_restore_a_id_count;
static uint16_t g_restore_b_ids[32];
static size_t g_restore_b_id_count;
static uint16_t g_save_slots[8];
static size_t g_save_count;
static uint16_t g_load_slots[8];
static size_t g_load_count;
static int32_t g_dynamic_records[8];
static size_t g_dynamic_count;
static uint16_t g_sounds[8];
static size_t g_sound_count;
static uint8_t g_claw_alternate;
static uint16_t g_claw_frame;
static unsigned g_claw_calls;
static unsigned g_render_ball_calls;
static unsigned g_rotate_calls;
static unsigned g_restore_previous_calls;
static unsigned g_finish_calls;
static unsigned g_simulate_calls;
static unsigned g_reset_calls;
static unsigned g_flipper_calls;
static unsigned g_drain_calls;
static unsigned g_initialize_calls;
static unsigned g_redraw_calls;
static unsigned g_progress_calls;
static unsigned g_panel_calls;
static int16_t g_last_item;
static unsigned g_item_calls;
static unsigned g_region_a_calls;
static unsigned g_region_b_calls;
static unsigned g_present_calls;
static BlitCall g_blits[16];
static size_t g_blit_count;
static unsigned g_get_dc_calls;
static unsigned g_release_dc_calls;
static unsigned g_create_dc_calls;
static unsigned g_create_bitmap_calls;
static unsigned g_select_calls;
static unsigned g_delete_object_calls;
static unsigned g_delete_dc_calls;
uint8_t g_borland_cpu_level = 2;
_Noreturn void borland_runtime_error(uint16_t code)
{
(void)code;
abort();
}
_Noreturn void win16_integer_divide_fault(void)
{
abort();
}
void tdkpin_refresh_collision_record(uint16_t caller_bp, uint16_t id)
{
assert(caller_bp == 0x0300);
g_refresh_ids[g_refresh_count++] = id;
}
void tdkpin_restore_record_or_item_a(uint16_t caller_bp, uint16_t id)
{
assert(caller_bp == 0x0300);
g_restore_a_ids[g_restore_a_id_count++] = id;
}
void tdkpin_restore_record_b(uint16_t caller_bp, uint16_t id)
{
assert(caller_bp == 0x0300);
g_restore_b_ids[g_restore_b_id_count++] = id;
}
void tdkpin_save_ball_slot(uint16_t caller_bp, uint16_t slot)
{
assert(caller_bp == 0x0300);
g_save_slots[g_save_count++] = slot;
}
void tdkpin_load_ball_slot(uint16_t caller_bp, uint16_t slot)
{
assert(caller_bp == 0x0300);
g_load_slots[g_load_count++] = slot;
}
void tdkpin_update_dynamic_ball_record(
uint16_t caller_bp, int32_t displacement)
{
assert(caller_bp == 0x0300);
g_dynamic_records[g_dynamic_count++] = displacement;
}
void tdkpin_play_sound_if_not_tilted(uint16_t sound_number)
{
g_sounds[g_sound_count++] = sound_number;
}
void tdkpin_draw_claw_frame(
uint8_t alternate, uint16_t frame, HDC16 destination)
{
assert(destination == 0x3333);
g_claw_alternate = alternate;
g_claw_frame = frame;
g_claw_calls++;
}
void tdkpin_render_ball_from_caller_frame(uint16_t caller_bp)
{
assert(caller_bp == 0x0300);
g_render_ball_calls++;
}
void tdkpin_rotate_and_draw_targets(uint16_t caller_bp)
{
assert(caller_bp == 0x0300);
g_rotate_calls++;
}
void tdkpin_restore_previous_ball_region(
uint16_t caller_bp, uint8_t suppress_present)
{
assert(caller_bp == 0x0300 && suppress_present == 0);
g_restore_previous_calls++;
}
void tdkpin_finish_ball_or_advance_player(uint16_t caller_bp)
{
assert(caller_bp == 0x0300);
g_finish_calls++;
}
void tdkpin_simulate_ball_and_dispatch_collision(uint16_t caller_bp)
{
assert(caller_bp == 0x0300);
g_simulate_calls++;
}
void tdkpin_reset_ball_state(uint16_t caller_bp)
{
assert(caller_bp == 0x0300);
g_reset_calls++;
}
void tdkpin_update_flippers(uint16_t caller_bp)
{
assert(caller_bp == 0x0300);
g_flipper_calls++;
}
void tdkpin_drain_messages_except_input(void)
{
g_drain_calls++;
}
void tdkpin_initialize_player_state(uint16_t caller_bp)
{
assert(caller_bp == 0x0300);
g_initialize_calls++;
}
void tdkpin_redraw_all_players(uint16_t caller_bp)
{
assert(caller_bp == 0x0300);
g_redraw_calls++;
}
void tdkpin_refresh_player_progress_markers(uint16_t caller_bp)
{
assert(caller_bp == 0x0300);
g_progress_calls++;
}
void tdkpin_draw_panel_animation(uint16_t caller_bp)
{
assert(caller_bp == 0x0300);
g_panel_calls++;
}
void tdkpin_draw_item_status(int16_t item, HDC16 destination)
{
assert(destination == 0x3333);
g_last_item = item;
g_item_calls++;
}
void tdkpin_restore_background_region_a(
INT16 height, INT16 width, INT16 y, INT16 x, HDC16 dc)
{
(void)height;
(void)width;
(void)y;
(void)x;
assert(dc == 0x3333);
g_region_a_calls++;
}
void tdkpin_restore_background_region_b(
INT16 height, INT16 width, INT16 y, INT16 x, HDC16 dc)
{
(void)height;
(void)width;
(void)y;
(void)x;
assert(dc == 0x3333);
g_region_b_calls++;
}
void tdkpin_present_background_region(
INT16 height, INT16 width, INT16 y, INT16 x, HDC16 dc)
{
(void)height;
(void)width;
(void)y;
(void)x;
assert(dc == 0x3333);
g_present_calls++;
}
HDC16 GetDC16(HWND16 window)
{
assert(window == 0x2222);
g_get_dc_calls++;
return 0x3333;
}
INT16 ReleaseDC16(HWND16 window, HDC16 dc)
{
assert(window == 0x2222 && dc == 0x3333);
g_release_dc_calls++;
return 1;
}
HDC16 CreateCompatibleDC16(HDC16 dc)
{
assert(dc == 0x3333);
g_create_dc_calls++;
return g_create_dc_calls == 1 ? 0x4444 : 0x5555;
}
HBITMAP16 CreateCompatibleBitmap16(HDC16 dc, INT16 width, INT16 height)
{
assert(dc == 0x3333 && width == 30 && height == 284);
g_create_bitmap_calls++;
return 0x6666;
}
HGDIOBJ16 SelectObject16(HDC16 dc, HGDIOBJ16 object)
{
assert(dc == 0x4444 || dc == 0x5555);
(void)object;
g_select_calls++;
return (HGDIOBJ16)(0x7000 + g_select_calls);
}
BOOL16 BitBlt16(
HDC16 destination,
INT16 x,
INT16 y,
INT16 width,
INT16 height,
HDC16 source,
INT16 source_x,
INT16 source_y,
uint32_t raster_operation)
{
g_blits[g_blit_count++] = (BlitCall){
destination, x, y, width, height,
source, source_x, source_y, raster_operation,
};
return 1;
}
BOOL16 DeleteObject16(HGDIOBJ16 object)
{
assert(object == 0x6666);
g_delete_object_calls++;
return 1;
}
BOOL16 DeleteDC16(HDC16 dc)
{
assert(dc == 0x4444 || dc == 0x5555);
g_delete_dc_calls++;
return 1;
}
static Win16FarPtr record(uint16_t id)
{
return win16_dgroup_pointer((uint16_t)(
0x095b + id * sizeof(TdkpinCollisionRecord)));
}
static void prepare_fixture(void)
{
memset(g_dgroup, 0, sizeof(g_dgroup));
memset(g_objects, 0, sizeof(g_objects));
memset(g_stack, 0, sizeof(g_stack));
win16_reset_segment_bindings();
win16_bind_segment(0x7000, g_objects, sizeof(g_objects), true);
win16_bind_segment(0x7200, g_dgroup, sizeof(g_dgroup), true);
win16_bind_segment(0x7300, g_stack, sizeof(g_stack), true);
win16_set_dgroup_selector(0x7200);
win16_set_stack_state(0x7300, 0x0300);
g_window = win16_make_far_pointer(0x7000, 0);
win16_write_u16(g_window, 4, 0x2222);
win16_write_u8(win16_dgroup_pointer(0x07c4), 1);
memset(g_refresh_ids, 0, sizeof(g_refresh_ids));
memset(g_restore_a_ids, 0, sizeof(g_restore_a_ids));
memset(g_restore_b_ids, 0, sizeof(g_restore_b_ids));
g_refresh_count = g_restore_a_id_count = g_restore_b_id_count = 0;
g_save_count = g_load_count = g_dynamic_count = g_sound_count = 0;
g_claw_calls = g_render_ball_calls = g_rotate_calls = 0;
g_restore_previous_calls = g_finish_calls = g_simulate_calls = 0;
g_reset_calls = g_flipper_calls = g_drain_calls = 0;
g_initialize_calls = g_redraw_calls = g_progress_calls = 0;
g_panel_calls = g_item_calls = 0;
g_region_a_calls = g_region_b_calls = g_present_calls = 0;
g_blit_count = g_get_dc_calls = g_release_dc_calls = 0;
g_create_dc_calls = g_create_bitmap_calls = g_select_calls = 0;
g_delete_object_calls = g_delete_dc_calls = 0;
}
static void test_guard_and_refresh_epilogue(void)
{
prepare_fixture();
win16_write_u8(win16_dgroup_pointer(0x07c4), 0);
tdkpin_timer_tick_with_frame(0x0300, g_window);
assert(g_flipper_calls == 0 && g_drain_calls == 0);
prepare_fixture();
win16_write_u8(win16_dgroup_pointer(0x07c5), 1);
win16_write_u16(g_window, 0x5e, 2);
tdkpin_timer_tick_with_frame(0x0300, g_window);
assert(g_refresh_count == 175);
assert(g_refresh_ids[0] == 1 && g_refresh_ids[174] == 175);
assert(win16_read_u8(win16_dgroup_pointer(0x07c5)) == 0);
assert(win16_read_u32(win16_dgroup_pointer(0x0876)) == 1);
assert(g_flipper_calls == 1 && g_drain_calls == 1);
assert(win16_read_u8(win16_far_add_offset(g_window, 0x51)) == 0);
assert(win16_read_u16(win16_far_add_offset(g_window, 0x5e)) == 1);
}
static void test_claw_release(void)
{
prepare_fixture();
win16_write_u8(win16_far_add_offset(g_window, 0x0bdd), 1);
win16_write_u8(win16_far_add_offset(g_window, 0x0bdf), 1);
win16_write_u8(win16_dgroup_pointer(0x07ca), 1);
win16_write_u16(win16_dgroup_pointer(0x086d), 0, 2);
win16_write_u16(win16_dgroup_pointer(0x086f), 0, 2);
win16_write_u32(g_window, 0x56, 1000);
tdkpin_timer_tick_with_frame(0x0300, g_window);
assert(g_claw_calls == 1 && g_claw_alternate == 0 && g_claw_frame == 2);
assert(win16_read_u16(win16_dgroup_pointer(0x086f)) == 10);
assert((int32_t)win16_read_u32(win16_dgroup_pointer(0x07d3)) == 259000);
assert((int32_t)win16_read_u32(win16_dgroup_pointer(0x07d7)) == 81000);
assert((int32_t)win16_read_u32(
win16_far_add_offset(g_window, 0x0baa)) == -450);
assert((int32_t)win16_read_u32(
win16_far_add_offset(g_window, 0x0bae)) == 50);
assert(g_save_count == 1 && g_save_slots[0] == 1);
assert(g_render_ball_calls == 1);
assert(g_sound_count == 1 && g_sounds[0] == 16);
assert(win16_read_u8(win16_far_add_offset(g_window, 0x0bdf)) == 0);
assert(g_simulate_calls == 1);
}
static void test_multiball_promotion(void)
{
prepare_fixture();
win16_write_u8(win16_far_add_offset(g_window, 0x0bdd), 1);
win16_write_u8(win16_far_add_offset(g_window, 0x0bdf), 1);
win16_write_u8(win16_far_add_offset(g_window, 0x61), 2);
win16_write_u8(win16_far_add_offset(g_window, 0x0be1), 1);
win16_write_u8(win16_dgroup_pointer(0x424c), 1);
win16_write_u32(win16_dgroup_pointer(0x07e7), 0, 0x11111111);
win16_write_u32(win16_dgroup_pointer(0x07ef), 0, 0x22222222);
win16_write_u32(win16_dgroup_pointer(0x07f7), 0, 0x33333333);
win16_write_u32(win16_dgroup_pointer(0x07ff), 0, 0x44444444);
win16_write_u32(win16_dgroup_pointer(0x0807), 0, 0x55555555);
for (uint16_t index = 0; index < 6; index++) {
win16_write_u8(
win16_dgroup_pointer((uint16_t)(0x0811 + index)),
(uint8_t)(0xa0 + index));
}
tdkpin_timer_tick_with_frame(0x0300, g_window);
assert(win16_read_u8(win16_far_add_offset(g_window, 0x61)) == 1);
assert(win16_read_u8(win16_dgroup_pointer(0x41f9)) == 1);
assert(win16_read_u8(win16_dgroup_pointer(0x424c)) == 0);
assert(win16_read_u32(win16_dgroup_pointer(0x07e3)) == 0x11111111);
assert(win16_read_u32(win16_dgroup_pointer(0x07eb)) == 0x22222222);
assert(win16_read_u32(win16_dgroup_pointer(0x07f3)) == 0x33333333);
assert(win16_read_u32(win16_dgroup_pointer(0x07fb)) == 0x44444444);
assert(win16_read_u32(win16_dgroup_pointer(0x0803)) == 0x55555555);
assert(g_load_count == 1 && g_load_slots[0] == 0);
assert(g_finish_calls == 0);
}
static void test_multiball_spawn(void)
{
prepare_fixture();
win16_write_u8(win16_far_add_offset(g_window, 0x0bdd), 1);
win16_write_u8(win16_far_add_offset(g_window, 0x0bdf), 1);
win16_write_u8(win16_far_add_offset(g_window, 0x0be0), 1);
win16_write_u8(win16_dgroup_pointer(0x07d1), 1);
win16_write_u32(win16_dgroup_pointer(0x3969), 0, 123000);
win16_write_u32(win16_dgroup_pointer(0x396d), 0, 456000);
win16_write_u32(g_window, 0x56, 1000);
tdkpin_timer_tick_with_frame(0x0300, g_window);
assert(g_save_count == 2 && g_save_slots[0] == 0 && g_save_slots[1] == 1);
assert(g_dynamic_count == 2 &&
g_dynamic_records[0] == 1 && g_dynamic_records[1] == 2);
assert(g_restore_previous_calls == 1);
assert(g_restore_b_id_count == 1 && g_restore_b_ids[0] == 1020);
assert((int32_t)win16_read_u32(win16_dgroup_pointer(0x07d3)) == 123000);
assert((int32_t)win16_read_u32(win16_dgroup_pointer(0x07d7)) == 456000);
assert((int32_t)win16_read_u32(
win16_far_add_offset(g_window, 0x0bae)) == 800);
assert(win16_read_u8(win16_far_add_offset(g_window, 0x61)) == 2);
assert(win16_read_u8(win16_dgroup_pointer(0x0874)) == 1);
assert(win16_read_u8(win16_far_add_offset(g_window, 0x0be0)) == 0);
}
static void test_intro_tick_32_and_marquee(void)
{
prepare_fixture();
win16_write_u8(win16_dgroup_pointer(0x07c9), 1);
win16_write_u32(win16_dgroup_pointer(0x0876), 0, 31);
win16_write_u16(record(90), 0x4b, 10);
win16_write_u16(record(90), 0x4d, 20);
win16_write_u16(record(90), 0x4f, 50);
win16_write_u16(record(90), 0x51, 60);
tdkpin_timer_tick_with_frame(0x0300, g_window);
assert(g_restore_a_id_count == 5);
assert(g_restore_a_ids[0] == 140);
assert(g_restore_a_ids[1] == 1001);
assert(g_restore_a_ids[2] == 153 &&
g_restore_a_ids[3] == 154 && g_restore_a_ids[4] == 6);
assert(g_restore_b_id_count == 1 && g_restore_b_ids[0] == 1002);
assert(g_region_a_calls == 1 && g_region_b_calls == 2);
assert(g_present_calls == 3);
assert(g_item_calls == 1 && g_last_item == 4);
prepare_fixture();
win16_write_u8(win16_dgroup_pointer(0x07c9), 1);
win16_write_u32(win16_dgroup_pointer(0x0876), 0, 5);
win16_write_u16(win16_dgroup_pointer(0x085f), 0, 0x7777);
win16_write_u16(win16_dgroup_pointer(0x0861), 0, 0x8888);
tdkpin_timer_tick_with_frame(0x0300, g_window);
assert(win16_read_u32(win16_dgroup_pointer(0x087a)) == 1);
assert(g_create_dc_calls == 2 && g_create_bitmap_calls == 1);
assert(g_blit_count == 4);
assert(g_blits[0].destination == 0x5555 &&
g_blits[0].width == 30 && g_blits[0].height == 100 &&
g_blits[0].source_x == 473 && g_blits[0].source_y == 277);
assert(g_blits[2].height == 237 && g_blits[2].source_x == 400);
assert(g_blits[3].destination == 0x3333 &&
g_blits[3].x == 233 && g_blits[3].y == 372);
assert(g_select_calls == 5 && g_delete_object_calls == 1);
assert(g_delete_dc_calls == 2);
static const struct {
uint32_t before_tick;
int16_t expected_item;
} item_cases[] = {
{7, 1},
{71, 9},
{79, 8},
{135, 1},
{143, 0},
};
for (size_t index = 0;
index < sizeof(item_cases) / sizeof(item_cases[0]);
index++) {
prepare_fixture();
win16_write_u8(win16_dgroup_pointer(0x07c9), 1);
win16_write_u32(
win16_dgroup_pointer(0x0876), 0,
item_cases[index].before_tick);
tdkpin_timer_tick_with_frame(0x0300, g_window);
assert(g_item_calls == 1);
assert(g_last_item == item_cases[index].expected_item);
}
}
static void test_intro_remainder_driven_cycles(void)
{
prepare_fixture();
win16_write_u8(win16_dgroup_pointer(0x07c9), 1);
win16_write_u32(win16_dgroup_pointer(0x0876), 0, 3);
tdkpin_timer_tick_with_frame(0x0300, g_window);
assert(g_restore_a_id_count == 2);
assert(g_restore_a_ids[0] == 142 && g_restore_a_ids[1] == 1002);
assert(g_restore_b_id_count == 1 && g_restore_b_ids[0] == 1001);
assert(g_region_a_calls == 2 && g_region_b_calls == 1);
assert(g_item_calls == 0);
prepare_fixture();
win16_write_u8(win16_dgroup_pointer(0x07c9), 1);
win16_write_u32(win16_dgroup_pointer(0x0876), 0, 15);
tdkpin_timer_tick_with_frame(0x0300, g_window);
assert(g_restore_a_id_count == 1 && g_restore_a_ids[0] == 1005);
assert(g_restore_b_id_count == 5);
assert(g_restore_b_ids[0] == 140 && g_restore_b_ids[1] == 1004);
assert(g_restore_b_ids[2] == 153 &&
g_restore_b_ids[3] == 154 && g_restore_b_ids[4] == 6);
assert(g_region_a_calls == 2 && g_region_b_calls == 1);
assert(g_item_calls == 1 && g_last_item == 2);
prepare_fixture();
win16_write_u8(win16_dgroup_pointer(0x07c9), 1);
win16_write_u32(win16_dgroup_pointer(0x0876), 0, 39);
tdkpin_timer_tick_with_frame(0x0300, g_window);
assert(g_restore_a_id_count == 2);
assert(g_restore_a_ids[0] == 144 && g_restore_a_ids[1] == 1003);
assert(g_restore_b_id_count == 1 && g_restore_b_ids[0] == 1002);
assert(g_region_a_calls == 2 && g_region_b_calls == 1);
assert(g_item_calls == 1 && g_last_item == 5);
}
static void test_panel_and_game_initialization(void)
{
prepare_fixture();
win16_write_u8(win16_dgroup_pointer(0x07c8), 1);
win16_write_u32(win16_dgroup_pointer(0x0851), 0, 1);
for (uint16_t id = 129; id <= 133; id++) {
win16_write_u16(record(id), 0x43, 0xffff);
}
tdkpin_timer_tick_with_frame(0x0300, g_window);
assert(win16_read_u8(win16_far_add_offset(g_window, 0x0bdf)) == 1);
assert(win16_read_u32(win16_dgroup_pointer(0x0851)) == 2);
assert(g_panel_calls == 1);
for (uint16_t id = 129; id <= 133; id++) {
assert(win16_read_u16(win16_far_add_offset(record(id), 0x43)) == 0);
}
prepare_fixture();
win16_write_u8(win16_dgroup_pointer(0x07c8), 1);
win16_write_u32(win16_dgroup_pointer(0x0851), 0, 281);
win16_write_u8(win16_dgroup_pointer(0x07d1), 1);
for (uint16_t item = 15; item <= 19; item++) {
win16_write_u8(
win16_far_add_offset(g_window, (uint16_t)(0x47b + 20 + item)), 1);
}
tdkpin_timer_tick_with_frame(0x0300, g_window);
assert(win16_read_u8(win16_dgroup_pointer(0x07c8)) == 0);
assert(win16_read_u8(win16_far_add_offset(g_window, 0x0bdf)) == 0);
assert(win16_read_u32(win16_dgroup_pointer(0x0851)) == 0);
assert(g_region_b_calls == 1 && g_present_calls == 1);
prepare_fixture();
win16_write_u16(win16_dgroup_pointer(0x081f), 0, 0xffff);
tdkpin_timer_tick_with_frame(0x0300, g_window);
assert(g_initialize_calls == 1 && g_redraw_calls == 1);
assert(g_progress_calls == 1 && g_reset_calls == 1);
assert(g_region_b_calls == 1 && g_present_calls == 1);
assert(win16_read_u32(win16_dgroup_pointer(0x0876)) == 0);
}
int main(void)
{
test_guard_and_refresh_epilogue();
test_claw_release();
test_multiball_promotion();
test_multiball_spawn();
test_intro_tick_32_and_marquee();
test_intro_remainder_driven_cycles();
test_panel_and_game_initialization();
return 0;
}