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