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>
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.zsubstitution 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:
snap_to_plane's own|N.z| <= F_EPSILONguard (pc:271859-271862).- AD-66's safety-push trigger comparison,
distvsradius - F_EPSILON(pc:272358-272361). 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):
resultis 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'sPhysicsGlobals.EPSILON = 0.0002fcompiles 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 |