Files
tdkpin/original/MECHANICS_PROGRESS.md
T
ddidderr 8b99e9607c feat(reconstruction): complete binary-backed C recovery
Replace the partial mechanics transcriptions with a separate, readable C11
reconstruction of the complete Win16 image while preserving the original raw
Ghidra export as immutable evidence. Cover all ordinary and overlapping entry
points, Borland runtime behavior, Win16 imports, segmented data, callbacks,
resources, indirect control flow, physics, rendering, persistence, and
startup/shutdown lifecycles.

Add deterministic extraction and audit tooling plus address-linked ledgers for
functions, imports, DGROUP ranges and objects, relocations, resources, and
callbacks. The final gate records zero raw, partial, restored, unknown,
blocked, or unclassified required units. Keep the semantic-fidelity boundary
explicit: the portable C is not claimed to reproduce a byte-identical Borland
NE build.

Add strict focused harnesses for every reconstructed C unit, exact resource
round-trip checks, and a 16-bit Borland Real48 reference probe. No Rust source
or Cargo metadata is changed in this phase.

Test Plan:
- `bash original/tools/test_reconstructed_c.sh` -- passed
- `bash original/tools/probe_real48_reference.sh` -- passed bit-for-bit
- `python3 original/tools/audit_reconstruction.py --require-complete` -- passed
- `git diff --cached --check` -- passed
- `git diff HEAD -- '*.rs' Cargo.toml Cargo.lock` -- empty
2026-08-23 16:41:17 +02:00

12 KiB

Historical mechanics reconstruction notes

This file preserves the live-Wine observations from the earlier mechanics-only pass. Its former partial-status table and pending list are superseded by RECONSTRUCTION_PROGRESS.md and FUNCTION_RECONSTRUCTION.tsv: all functions named below, including the complete 1000:7ed9, 1000:8b0d, 1000:c79c, and 1000:eab1 paths, now have verified readable-C reconstructions. The runtime captures below remain useful independent evidence.

This ledger scopes the current reverse-engineering pass to the claw and flippers. The preserved executable is the authority; TDKPIN_GHIDRA_RAW.c remains immutable raw evidence, while TDKPIN_MECHANICS_RESTORED.c carries reviewed names and control flow.

Target: TDKPIN.EXE SHA-256 a9022f1894e3e6e21fc42e8f6c932f7c549ca77f63aaa0c488bb9d55d9d0174c.

Function inventory

Address Raw name Restored role Status
1000:638e FUN_1000_638e key-release flag updates reviewed
1000:6bf8 FUN_1000_6bf8 key-press flag updates reviewed
1000:7ed9 FUN_1000_7ed9 flipper collision movement and sound reviewed; inner fixed-point geometry still raw
1000:8b0d FUN_1000_8b0d flipper bitmap/collider movement reviewed; inner fixed-point geometry still raw
1000:99f0 FUN_1000_99f0 binary flipper transition state machine restored
1000:c79c FUN_1000_c79c collision dispatch, including claw object 0x59 claw branch restored
1000:eab1 exported ordinal 10 timer tick, claw release and return claw branch restored
1008:0002 FUN_1008_0002 sound-enabled dispatch restored
1008:0e08 FUN_1008_0e08 claw background restore and sprite slicing restored

Inventory coverage is 9/9 functions inspected for this mechanism pass. This is not a claim that all instructions in the two large physics functions have been renamed.

Proven behavior now available to the Rust port

  • Left Control (scan 0x1d) and keypad Enter (scan 0x1c) set input flags; the timer changes each flipper position only between 0 and 1.
  • Every flipper movement edge, including return, invokes sound number 21, Win16 WAVE resource 2021.
  • Claw collision object 0x59 suspends the ball, zeros its live motion, disables collision records 12 through 20, and selects terminal frame 1, 6, 7, or 18.
  • The claw advances exactly one frame per timer tick to its terminal, releases the ball, then advances one frame per tick back to frame 10 before clearing the overlay and restoring collision records 12 through 20.
  • Resource 900 is sliced into 100x74 frames, nine columns by four rows. The 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.

Evidence that was pending in this historical pass

  • The full raw fixed-point flipper impulse calculation is now represented in reconstructed/tdkpin_flipper_collision.c and verified at 1000:7ed9.
  • Original callback batching for detail settings 1..5 is now represented in reconstructed/tdkpin_timer_tick.c and tdkpin_physics.c. Any later Rust transfer remains a separate goal and is not part of this C phase.

Those items were intentionally excluded from parity at the time of these notes; their later C closure is recorded in the address-linked main ledger.

Runtime comparison on 2026-08-22

Wine 11.15 (staging) ran the preserved 16-bit NE executable from this tree. Keypad + was sent to the real window and produced the one-player started state. Down was then held for 1.1 seconds and released; captures confirmed that the original plunger advances while held and launches only on release.

The Rust validation mode captured the corresponding started and half-charge states plus closing and release frames for claw terminal 6. That original live session did not reach the claw; the controlled tracing described below later captured an original terminal-18 sequence.

Live fixed-point tracing

Wine's remote debugger changes the Win16 exception path and is not suitable for continuous gameplay traces. On Linux, tools/run_traceable_original.sh starts the unmodified executable with a same-user ptrace opt-in. While it is waiting at the launcher, tools/trace_original_state.py locates the live DGROUP and game object by recovered binary/state signatures and records their fixed-point state:

original/tools/run_traceable_original.sh
original/tools/trace_original_state.py --duration 5 --output /tmp/launch.csv

The sampler records original millipixel positions, predicted positions, velocities, flipper inputs and states, and claw animation state whenever any of them changes. It neither patches TDKPIN.EXE nor pauses Wine. The tracing helper is Linux-only development infrastructure; it is not part of the Rust replica.

tools/inject_original_state.py updates all recovered copies of the ball position and velocity between the original 30 ms callbacks. This permits repeatable probes at individual walls, circles, gates, and mechanism triggers:

original/tools/inject_original_state.py --x 289 --y 94 --vx 0 --vy 0

The executable image remains unchanged; only the current disposable Wine process is modified. Run the injector while the ball is waiting in the launcher so it can identify the live game object, or supply the previously reported --object-base to both the injector and tracer during a sequence of active probes. --substeps 1 isolates one collision integration step; omitting it retains the original configured detail level. --stop-process is available for single probes, but repeated whole-process stops can strand Wine while a Win16 mutex is held and therefore are not the default.

A probe at (289,94) captured the original terminal-18 path. The collision suspended the ball while frames advanced from 10 through 18 at the configured 30 ms cadence. Release assigned (325000,92000) and (0,1000) millipixels, then cleared suspension during the same timer callback. The three default physics substeps each added 15 millipixels of vertical velocity, confirming the observed total gravity increment of 45 per timer tick.

Repeated clean trigger probes subsequently covered terminals 1, 6, and 7 as well. After the three default substeps their live states were respectively (251160,78090),(-2280,45), (265440,100930),(-1520,2325), and (271466,105070),(-1178,2705). Terminal 18 produced (325000,95090),(0,1045). These match the recovered release table followed by three +15 gravity increments; Rust tests now assert every raw release velocity.

A one-substep probe against the vertical shooter walls recovered the type-2 response arithmetic. For unit tangent t from the registered start to end and left normal n, an incoming contact with vn = dot(v,n) and vt = dot(v,t) uses vn' = -0.6 * vn and vt' = vt + 0.1 * vn for the ordinary rail coefficients stored in its two Real48 fields. The new position is the previous position plus this response velocity; the original does not separate a circle from the line with a modern capsule solver.

Fresh one-substep probes recovered type-1 response in the radial basis. With outward normal n, clockwise tangent t, entering speed vn < 0, and tangent speed vt, ordinary object 155 produced vn' = -0.6 * vn and vt' = vt - 0.1 * vn. Bumper 51 additionally added 0.4 * 3800 along n. The exact captured transitions were (0,-940) -> (94,564) and (0,-1970) -> (197,3096) millipixels per substep.

Live key edges recovered all moving flipper geometry. The raised left records are line 66 (98,382)->(131,369), circle 67 (133,377), and line 68 (137,384)->(116,405). The raised right records are line 81 (197,405)->(171,378), circle 82 (181,376), and line 83 (183,368)->(217,383). Resting records retain their registration-table coordinates. Edge-time position and velocity transfer is tracked separately from the record geometry. Three live left-edge probes at (115,390), (120,390), and (125,390) produced post-edge positions (128.718,365), (133.397,360), and (138.182,355) with raw velocities (1932,-3523), (2079,-4658), and (2217,-5889). The Rust upward stroke fits those states; the full raw calculation was still pending in this historical pass and is now closed by the verified 1000:7ed9 reconstruction. A separate downstroke probe at (125,390) kept position unchanged and assigned (3519,6302) before the next substep clamped speed to 3800; the Rust return edge now matches that state.

The type-4 rule dispatcher at 1000:b476 shows that records 150-152 are three independent 500-point sensors. Each sets its own ten-callback timer and enables the associated magnetic gate; there is no recovered 1,500-per-target plus 5,000 completion rule. Records 140-147 carry their exact 1,000 through 3,000 scores and use the same enter-once contact bit, which clears after the ball leaves their radius.

The collision scan at 1000:c79c does not resolve the first matching object id. It retains the smallest path-progress value while traversing all 175 records, then applies that response from the previous position. The Rust fixed-point solver now follows that ordering for recovered type-1 and type-2 objects.

1000:b476 decodes the physical bank flags in OBJECTS.tsv. Flags 0x208a on records 90-104 deactivate five complete three-line groups; clearing all five raises the bumper value and rearms them. Flags 0x2092 on records 109, 112, 115, and 118 deactivate four three-line groups; clearing all four advances one of the nine TDK diamond segments, awards the recovered segment value, and rearms records 109-120.

Controlled type-3 probes filled records 129-133 in order. Their bonus sequence was 10,000, 30,000, 70,000, and 150,000; the fifth hole transferred the full 310,000 to score and cleared bonus. Each hole returned the ball to (325,413) without consuming it and retained contact state 99. Entering record 148 then cleared all five hole states, confirming its wheel-reset role.

Top type-4 targets activate persistent field records rather than a shared timer: 150 enables 153 (left), 151 enables 6 (center), and 152 enables 154 (right). Entering active record 153 at (15,350) changed (0,2015) to (0,-3039) millipixels per substep in the live original. The field continued pulling upward inside its registered bounds and deactivated after the ball exited above them.

Collision with flagged line 84 selected effect 3 in a live probe, set the per-player effect word, and activated type-4 record 149. 1000:c4e1 maps effects 1-5 to 10,000, 20,000, 50,000, 100,000, and 200,000 bonus; effect 6 transfers the accumulated bonus to score. Entering record 149 also awards its static 500, clears the selection, and disables the record until line 84 is hit again.

After record 148 sets DAT_1028_399a, the next flagged line-84 selection uses effect 7 instead of random 1-6. The effect-7 branch in 1000:c4e1 schedules the timer's second-ball path and raises the active-ball count to two. The Rust port now keeps and renders a second live ball, applies recovered physical collisions and scores to it, and removes only the drained ball while multiball is active.

The score-addition path at 1000:bc36 indexes the initialized 32-bit threshold table at DGROUP 0x825 using media state 1-4. The exact thresholds are 140,000, 650,000, 1,300,000, and 4,000,000. Crossing each threshold increments the media state and remaining-ball count; the apparent fifth table word is the next unrelated global, not a fifth media tier.