feat(reverse): isolate original physics substeps

Expose the original detail-derived integration count, active-ball count,
gravity, maximum speed, and movement flags in live traces. Allow probes to
force one physics substep so a single collision response can be observed
without the default three-step timer batch obscuring intermediate state.

Record the dynamically verified type-2 rail response: resolve velocity in the
registered line's normal/tangent basis, apply the two original Real48
coefficients, then advance from the previous position using that response.

Test Plan:
- `ruff check original/tools/trace_original_state.py original/tools/inject_original_state.py` -- passed
- one-substep probes against both vertical shooter walls -- passed
- ordinary rail responses matched 0.6 normal rebound and 0.1 tangent coupling
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-22 20:27:56 +02:00
parent 65ee3c5355
commit c50ed39a35
3 changed files with 37 additions and 1 deletions
+10 -1
View File
@@ -105,7 +105,8 @@ 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.
probes. `--substeps 1` isolates one collision integration step; omitting it
retains the original configured detail level.
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
@@ -113,3 +114,11 @@ suspended the ball while frames advanced from 10 through 18 at the configured
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.
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.
+9
View File
@@ -18,6 +18,7 @@ from trace_original_state import (
NEXT_BALL_Y,
OBJECT_BALL_X,
OBJECT_BALL_Y,
PHYSICS_SUBSTEPS,
PREVIOUS_BALL_X,
PREVIOUS_BALL_Y,
VELOCITY_X,
@@ -49,6 +50,12 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--y", type=float, required=True, help="ball center y in logical pixels")
parser.add_argument("--vx", type=float, default=0.0, help="x velocity in pixels per timer tick")
parser.add_argument("--vy", type=float, default=0.0, help="y velocity in pixels per timer tick")
parser.add_argument(
"--substeps",
type=int,
choices=range(1, 6),
help="temporarily force the original physics substep count",
)
return parser.parse_args()
@@ -106,6 +113,8 @@ def main() -> None:
(NEXT_BALL_Y, next_y),
):
memory.write(data_base + offset, value)
if args.substeps is not None:
memory.write(data_base + PHYSICS_SUBSTEPS, struct.pack("<H", args.substeps))
for offset, value in (
(VELOCITY_X, velocity_x),
(VELOCITY_Y, velocity_y),
+18
View File
@@ -31,8 +31,12 @@ CLAW_FRAME = 0x086D
CLAW_TARGET_FRAME = 0x086F
CLAW_SPRITE_BANK = 0x0873
CLAW_ANIMATION_BLOCKED = 0x0874
PHYSICS_SUBSTEPS = 0x0871
# Recovered offsets in the live game-window object.
MAXIMUM_SPEED = 0x0056
GRAVITY_PER_SUBSTEP = 0x005A
ACTIVE_BALL_COUNT = 0x0061
VELOCITY_X = 0x0BAA
VELOCITY_Y = 0x0BAE
PREVIOUS_BALL_X = 0x0BBA
@@ -44,6 +48,8 @@ RIGHT_FLIPPER_POSITION = 0x0BD6
LEFT_FLIPPER_DOWN = 0x0BDA
RIGHT_FLIPPER_DOWN = 0x0BDB
BALL_SUSPENDED = 0x0BDF
BALL_ACTIVE = 0x0BDD
COLLISION_DISABLED = 0x0BD8
OBJECT_READ_SIZE = 0x0BE0
@@ -208,6 +214,10 @@ CSV_FIELDS = [
"elapsed_us",
"player_count",
"current_player",
"physics_substeps",
"active_ball_count",
"maximum_speed_milli_per_tick",
"gravity_milli_per_substep",
"ball_x_milli",
"ball_y_milli",
"next_ball_x_milli",
@@ -222,6 +232,8 @@ CSV_FIELDS = [
"right_flipper_position",
"left_flipper_down",
"right_flipper_down",
"ball_active",
"collision_disabled",
"ball_suspended",
"claw_active",
"claw_frame",
@@ -235,6 +247,10 @@ def sample_state(data: bytes, obj: bytes) -> tuple[int, ...]:
return (
data[PLAYER_COUNT],
data[CURRENT_PLAYER],
unpack_u16(data, PHYSICS_SUBSTEPS),
obj[ACTIVE_BALL_COUNT],
unpack_i32(obj, MAXIMUM_SPEED),
unpack_i32(obj, GRAVITY_PER_SUBSTEP),
unpack_i32(data, BALL_X),
unpack_i32(data, BALL_Y),
unpack_i32(data, NEXT_BALL_X),
@@ -249,6 +265,8 @@ def sample_state(data: bytes, obj: bytes) -> tuple[int, ...]:
unpack_u16(obj, RIGHT_FLIPPER_POSITION),
obj[LEFT_FLIPPER_DOWN],
obj[RIGHT_FLIPPER_DOWN],
obj[BALL_ACTIVE],
obj[COLLISION_DISABLED],
obj[BALL_SUSPENDED],
data[CLAW_ACTIVE],
unpack_u16(data, CLAW_FRAME),