# `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 */ # 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** |