acdream/docs/research/2026-08-07-s4-pseudocode.md
Erik d73125d3b0
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
fix(physics): S4/AD-65 — the away-from-plane response snaps to the surface, as retail does
Campaign S slice S4, the half that landed. Retail's CTransition::
adjust_offset @0x0050a370 branches on dot(offset, contactPlane.N) at
0x0050a4fa: moving INTO the plane subtracts the normal component
(0x0050a529), moving AWAY calls Plane::snap_to_plane @0x00509c50 —
which preserves X and Y and re-solves ONLY Z so the offset lies in the
plane (the d terms cancel algebraically), no-op under the
0.000199999995f |N.z| epsilon. acdream ran the orthogonal projection in
BOTH directions, shrinking downhill XY travel by cos^2(theta): 25% at
30 degrees, 50% at 45 — AD-65's recorded shortfall, now retired.

The combined Opus review independently re-derived the algebra, the
branch polarity, the epsilon's bit-identity (17b75139), and the
sabotage magnitude (the re-instated projection yields X = 0.75 =
cos^2 30 exactly), and verified the delta is 4 non-comment lines with
the into-plane arm, the crease arm, and both no-plane arms untouched.
Its blast-radius sweep found the away arm exercised but NOT
discriminated by any pre-existing test — every one asserts lower
bounds the snap over-satisfies — so the two new exact-value tests are
the only discriminating coverage, recorded in the test's class doc,
and the felt 33-100% downhill speed-up is the morning gate's one row.

AD-66 (the push-out's bare radius) is WITHHELD: byte-confirmed twice,
implemented, then pulled after the same clean-room binaries measured
contradictory absorbed-tick outcomes flipping with nothing but test
assert shape — issue #341 carries the observation matrix and the
apparatus plan; its two exact-value tests are [Skip]-ed; the retained
substitution's rationale is restored at the site per review F1, with
the review's remaining findings (F2/F3/F4/F5/F6) applied and F8 filed
as #342. AD-69 filed: the same block omits retail's get_block_offset
seam-frame correction, deferred to the AD-66 relanding for
attributability. #340 filed: a fifth load-sensitive flake.

Review verdict: PASS. AD-65 is provably unable to reach the #341
anomaly's code path (the absorb scenario takes the crease arm).
Clean-room suite: 11,239 passed / 6 skipped / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 02:45:03 +02:00

15 KiB

CTransition::adjust_offset — full branch-tree pseudocode (Campaign S, S4)

OUTCOME NOTE (2026-08-07, appended by the session lead): AD-66 was WITHHELD after this doc was written — the production code retains the radius * N.z substitution and register row AD-66 stays ACTIVE; see issue #341 for the measurement anomaly that blocked the landing. Statements below describing the bare-radius port as applied describe the IMPLEMENTED-THEN- PULLED state, not HEAD. The disassembly itself is unaffected and remains the oracle for the relanding.

Source: docs/research/named-retail/acclient_2013_pseudo_c.txt, function at 0x0050a370, pseudo-C lines 272271-272393. Companion callee Plane::snap_to_plane at 0x00509c50, lines 271852-271869. Cross-referenced against references/ACE/Source/ACE.Server/Physics/Animation/Transition.cs:34-87 (Transition.AdjustOffset) and references/ACE/Source/ACE.Server/Physics/Extensions/PlaneExtensions.cs:31-37 (SnapToPlane), and against references/ACE/Source/ACE.Server/Physics/Common/Vector.cs:8-16 (NormalizeCheckSmall). Written under the S4 contract docs/research/2026-08-07-s4-adjustoffset-contract.md for AD-65 + AD-66.

acdream port: src/AcDream.Core/Physics/TransitionTypes.cs, Transition.AdjustOffset (private → internal as of this slice, to allow direct exact-value testing — matches the existing SlideSphereInternal precedent in the same file).

Signature

Vector3 adjust_offset(CTransition* this, Vector3 offset)

Called once per sub-step from find_transitional_position BEFORE the offset is applied to check_pos (acdream: TransitionalInsert reads CollisionInfo state left by the PREVIOUS step, then calls AdjustOffset before mutating CheckPos).

Full branch tree

adjust_offset(offset) -> Vector3:
    result = offset
    checkSlide = false

    # ---- sliding-normal gate (0x0050a398) ----
    slidingAngle = dot(result, collision_info.sliding_normal)
    if collision_info.sliding_normal_valid:
        if slidingAngle < 0:
            checkSlide = true                      # ecx_1 = 1
        else:
            collision_info.sliding_normal_valid = false

    # ---- branch on contact plane (0x0050a3de) ----
    if collision_info.contact_plane_valid:
        collisionAngle = dot(result, contact_plane.N)          # arg3 @0x0050a408
        slideOffset = cross(contact_plane.N, sliding_normal)   # @0x0050a42a onward

        if checkSlide:                              # ecx_1 != 0 (0x0050a42a)
            # ---- crease-slide arm: verified identical to acdream, NOT changed ----
            if normalize_check_small(slideOffset):    # degenerate (len <= EPSILON)
                result = Zero
            else:
                result = dot(slideOffset, result) * slideOffset

        elif collisionAngle <= 0:                     # 0x0050a505, "ah & 0x41" != 0
            # ---- INTO-plane arm (0x0050a529) — unchanged, already correct ----
            result -= contact_plane.N * collisionAngle

        else:                                          # collisionAngle > 0
            # ---- AWAY-from-plane arm (0x0050a50e) — AD-65 FIX ----
            snap_to_plane(contact_plane, &result)       # NOT the subtraction!

        # ---- safety push-out (0x0050a571) — AD-66 FIX applies inside ----
        if not contact_plane_is_water:
            if contact_plane_cell_id != 0:
                blockOffset = get_block_offset(sphere_path.check_pos.objcell_id,
                                                contact_plane_cell_id)
                globSphere = sphere_path.global_sphere[0]
                dist = dot(globSphere.center - blockOffset, contact_plane.N)
                       + contact_plane.d
                # AD-66: retail compares/divides the BARE radius, not
                # radius*N.z, at BOTH sites below.
                if dist < globSphere.radius - F_EPSILON:            # 0x0050a5cf
                    zDist = (globSphere.radius - dist) / contact_plane.N.z  # 0x0050a5df
                    if globSphere.radius > |zDist|:                 # 0x0050a5e9
                        sphere_path.add_offset_to_check_pos((0, 0, zDist))

    # ---- no contact plane (0x0050a61e) ----
    elif checkSlide:                                    # ecx_1 != 0
        slidingAngle2 = dot(result, sliding_normal)
        result -= sliding_normal * slidingAngle2
    # else: result unchanged (no contact plane, no slide)

    return result

Plane::snap_to_plane (0x00509c50) — the AD-65 target

snap_to_plane(plane, offset* /* in-out */):
    if |plane.N.z| <= F_EPSILON (0.000199999995f):
        return                              # no-op — X, Y, Z ALL unchanged

    # offset.z temporarily zeroed, then re-solved so dot(N, offset) + d == 0:
    offset.z = -(offset.x * N.x + offset.y * N.y) / N.z
    # X and Y are NEVER written — only Z changes.

Deriving the formula (the d terms cancel)

Retail's literal decompiled expression (pc:271864-271867) is:

offset.z = 0                                            # temporary
A = offset.x*N.x + offset.y*N.y                          # (z already 0, so this
                                                           #  is the full dot(N,offset))
offset.z = ( -(A + d) * (1/N.z) ) - ( (1/N.z) * -d )

Expand:

offset.z = -(A+d)/N.z + d/N.z
         = [ -(A+d) + d ] / N.z
         = [ -A - d + d ] / N.z
         = -A / N.z
         = -(offset.x*N.x + offset.y*N.y) / N.z

The d terms cancel exactly, leaving the plain XY-dot-over-N.z formula above. This matches ACE's PlaneExtensions.SnapToPlane byte-for-byte (confirmed by reading references/ACE/.../PlaneExtensions.cs:31-37, which carries the unsimplified -(...+d)*(1/N.z) - (1/N.z)*-p.D form — ACE did not even bother to algebraically simplify it, which is good corroborating evidence this is really what retail computes rather than an ACE reinterpretation).

The Binary Ninja flag-idiom ambiguity (resolve, don't guess)

Four x87 float comparisons in this function's neighborhood get turned into the same packed-flags shape by Binary Ninja:

eax = (ST0<src)<<8 | (unordered)<<10 | (ST0==src)<<14 | ...
bool p = /* test ah, <mask> */          # sometimes resolved, sometimes not

One of the four (the outer collisionAngle<=0 vs >0 branch) is resolved cleanly — Binary Ninja rendered it directly as if ((eax_4_ah & 0x41) != 0) with no /* unimplemented */ placeholder, and mask 0x41 (bits C0|C3) is the standard x87 "ST0 <= src" idiom. The divergence register's AD-65 row independently disassembled the raw bytes at this exact site (0050a4fa fcomp [0x795344] / 0050a502 test ah,0x41 / 0050a505 jne 0x50a515, 0x795344 = the float constant 0.0f) and confirms: jne on ah & 0x41 takes the SUBTRACT branch when collisionAngle <= 0 and falls through to call 0x509c50 (snap_to_plane) when collisionAngle > 0. No ambiguity here — this is the branch AD-65's headline fix depends on, and it is independently confirmed by both the contract and this doc's own reading of the pseudo-C.

The other three all use mask 0x5 (bits C0|C2) and ALL THREE are left as /* bool p = unimplemented {test ah, 0x5} */ by Binary Ninja — it could not resolve them into readable expressions:

  1. snap_to_plane's own |N.z| <= F_EPSILON guard (pc:271859-271862).
  2. AD-66's safety-push trigger comparison, dist vs radius - F_EPSILON (pc:272358-272361).
  3. normalize_check_small's degenerate-length check (pc:91421-91424, unrelated to AD-65/AD-66 but in the same neighborhood and same idiom — documented here since the sliding-normal arm cites it).

Attempting to read the polarity directly off the packed-flag pseudocode's (x87_rA < x87_rB) sub-expression is unsound for these three: the subtraction operand order recorded by the decompiler ((x87_r6 - x87_r7) in snap_to_plane vs (x87_r5 - temp0) in normalize_check_small) is not by itself sufficient to recover which operand was ST(0) in the original fcomp, and a naive literal reading of the two sites against each other produces contradictory polarities (the two reads cannot both be "ST0 < src means true" and remain self-consistent with their own surrounding code's evident purpose). This doc does not attempt to re-derive them from the disassembly-free pseudo-C. Instead, each is resolved by triangulating independent evidence:

Site Resolved polarity used Evidence
snap_to_plane epsilon guard |N.z| <= F_EPSILON → no-op; else → resolve Z (a) the S4 contract pins this explicitly ("if |N.z| <= 0.000199999995f do NOTHING; else..."); (b) domain reasoning — the resolve divides by N.z, so the guard must protect against near-zero N.z (a near-vertical wall), not near-full N.z (a floor); (c) ACE's PlaneExtensions.SnapToPlane (if (Math.Abs(p.Normal.Z) <= PhysicsGlobals.EPSILON) return;) — independently ported, agrees exactly.
AD-66 trigger (dist vs radius - F_EPSILON) compute/push branch fires when dist < radius - F_EPSILON (a) preserves the EXISTING acdream control-flow direction (push fires when penetrating) — the contract asks only to substitute the RADIUS term, not invert the comparison; (b) domain reasoning — a push-up-when-penetrating safety net must fire on LOW dist; (c) ACE's Transition.cs:77 (if (dist >= globSphere.Radius - PhysicsGlobals.EPSILON) return offset;) — the negation of exactly this condition, independently ported, agrees.
normalize_check_small degenerate check length <= F_EPSILON → return 1 (small); else → normalize, return 0 ACE's Vector.NormalizeCheckSmall (var dist = v.Length(); if (dist < PhysicsGlobals.EPSILON) return true; v *= 1/dist; return false;) — independently ported, agrees, and also confirms the length is the FULL vector length, not (as the raw decompiled this->x alone might suggest — see next section) just the X component.

All three triangulations AGREE with each other's implied "the guarded branch is the geometrically meaningful one" reading and agree with the two independently-sourced ACE ports. None of this changes what ships: (1) and (2) are exactly AD-65's and AD-66's fixes; (3) confirms NO change is needed to the sliding-normal arm.

The sliding-normal arm (0x0050a42a) — verified against acdream, NOT changed

Per the contract, this arm must be verified but is out of scope to modify unless it diverges. It does not.

Cross product. Retail computes (pc:272326-272328):

crossVec.x = sliding_normal.z * N.y - sliding_normal.y * N.z
crossVec.y = sliding_normal.x * N.z - sliding_normal.z * N.x
crossVec.z = sliding_normal.y * N.x - sliding_normal.x * N.y

This is algebraically cross(N, sliding_normal) (standard cross(a,b) = (a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x) with a=N, b=sliding_normal). acdream's Vector3.Cross(ci.ContactPlane.Normal, ci.SlidingNormal) computes the same thing. Match.

Projection. Retail (pc:272332-272339): dot = dot(crossVec, result), then result = crossVec * dot. acdream: result = Vector3.Dot(slideOffset, result) * slideOffset. Scalar-times-vector is commutative here — same value. Match.

Degenerate case (normalize_check_small returns nonzero). Retail (pc:272341-272345) is genuinely ambiguous in the raw pseudo-C: it shows x = __return_1 (the un-normalized cross-product X component, NOT zero) followed by memset(&s, 0, 0x14) which zeroes s, z, and 16 more trailing bytes of stack — it does NOT show x (the X component) being zeroed by the memset span shown. Read completely literally, this would mean X keeps a tiny nonzero leftover value while Y and Z become exactly zero, which does not match "degenerate → whole vector is zero".

This is judged to be a Binary Ninja decompilation artifact, not real retail behavior, for three independent reasons: (1) the decompiled normalize_check_small itself only reads this->x (pc:91417) as the thing compared against F_EPSILON — never this->y or this->z in a sum-of-squares — even though lines 91415-91416 (this->z; / this->y; bare, unassigned reads) show the decompiler DID emit memory-read instructions for y and z that it then failed to fold into the length expression; (2) ACE's independently-ported NormalizeCheckSmall unambiguously computes the full v.Length(); (3) acdream's own existing port (slideOffset.Length() < PhysicsGlobals.EPSILON → result = Vector3.Zero) already implements the sensible full-zero, full-length reading and there is no report of it producing wrong behavior. This matches the project's documented BN-artifact class (feedback_bn_decomp_field_names.md): a lost FPU sum-of-squares reduced to one leftover operand load. No change made. acdream's existing slideLen < EPSILON → result = Vector3.Zero stands.

The no-contact-plane branches — verified, NOT changed

  • No contact plane, no slide (0x0050a3de implicit else): result is returned unmodified. acdream: branch = "no-cp", no mutation. Match.
  • No contact plane, sliding active (0x0050a61e): result -= sliding_normal * dot(result, sliding_normal). acdream: branch = "no-cp-slide", result -= ci.SlidingNormal * slidingAngle. Match.

Observed but OUT OF SCOPE: the missing block-offset correction

Retail's safety push-out (pc:272354, LandDefs::get_block_offset) and ACE's port (Transition.cs:75, LandDefs.GetBlockOffset(SpherePath.CheckPos.ObjCellID, CollisionInfo.ContactPlaneCellID)) both re-express the sphere center into the CONTACT PLANE's cell-relative frame before computing dist, to handle the case where the contact plane was recorded in a different (landblock- adjacent) cell than check_pos's current cell. acdream's port (TransitionTypes.cs:5602-5607, both before and after this slice's fix) uses sp.GlobalSphere[0].Origin directly with no block-offset correction.

This is a THIRD potential divergence in the same safety block, but it is not AD-65 or AD-66 and is not one of the "two fixes" the S4 contract scopes — flagged here per the contract's "Anything outside AdjustOffset and its tests" OUT-of-scope clause read narrowly (in scope location, out of scope fix). Left unchanged; worth a future register row if the session lead wants it filed.

Constants

  • F_EPSILON = 0.000199999995f (retail's exact float32 bit pattern for "0.0002"). acdream's PhysicsGlobals.EPSILON = 0.0002f compiles to the identical bit pattern (both are "nearest float32 to decimal 0.0002") — no new constant needed, reused as-is.

Deliverable summary (what changes, what doesn't)

Arm Retail acdream before S4 acdream after S4
collisionAngle <= 0 (into plane) subtract full N component same unchanged
collisionAngle > 0 (away from plane) snap_to_plane: XY preserved, Z re-solved, epsilon no-op subtract full N component (AD-65 bug) fixed: snap semantics
Safety-push trigger bare radius - F_EPSILON radius*N.z - F_EPSILON (AD-66 bug) fixed: bare radius
Safety-push zDist numerator (radius - dist) / N.z (radius*N.z - dist) / N.z (AD-66 bug) fixed: bare radius
Safety-push sanity bound (radius > |zDist|) bare radius bare radius (already correct) unchanged
Sliding-normal crease arm cross + normalize + project same unchanged (verified)
No-contact-plane arms subtract or no-op same unchanged (verified)
Block-offset correction in safety push present (get_block_offset) absent unchanged — out of S4 scope, flagged above