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:
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user