From c50ed39a35b27a153ae5cc843fb3ea7903a5226b Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sat, 22 Aug 2026 20:27:56 +0200 Subject: [PATCH] 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 --- original/MECHANICS_PROGRESS.md | 11 ++++++++++- original/tools/inject_original_state.py | 9 +++++++++ original/tools/trace_original_state.py | 18 ++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/original/MECHANICS_PROGRESS.md b/original/MECHANICS_PROGRESS.md index c794701..141c707 100644 --- a/original/MECHANICS_PROGRESS.md +++ b/original/MECHANICS_PROGRESS.md @@ -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. diff --git a/original/tools/inject_original_state.py b/original/tools/inject_original_state.py index 8b0d816..412e0cb 100755 --- a/original/tools/inject_original_state.py +++ b/original/tools/inject_original_state.py @@ -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(" 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),