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
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>
287 lines
15 KiB
Markdown
287 lines
15 KiB
Markdown
# `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** |
|