fix #172: port the retail CCylSphere collision family (platform step-up)
The Holtburg town-network portal platform (stab 0xC0A9B465, Setup 0x020019E3, CylSphere r=2.597m h=0.256m) blocked the player with an endless rim slide instead of retail's step-up-onto-top — gating the whole #137 dungeon repro. Surfaced when #149 started registering BSP-less stab CylSpheres: the collision SHAPE became right while the RESPONSE was still the hand-rolled AP-6 approximation (step-up gate + radial wall-slide only). Root cause: no cylinder-TOP support anywhere. DoStepUp's internal step-down probe needs retail's step_sphere_down (0x0053a9b0) to land on the flat top — a cylinder has no polygons for the walkable search — so every step-up onto a wide cylinder failed into StepUpSlide and the player orbited the rim (probe-confirmed: [cyl-test] result=Slid with horizontal rim normals, launch-137-repro.log). Port the full family verbatim: dispatcher intersects_sphere 0x0053b440 (placement/ethereal detection, step-down cap landing, walkable probe, grounded step_sphere_up 0x0053b310, PathClipped collide_with_point 0x0053acb0, airborne land_on_cylinder 0x0053b3d0, Collide-flag exact-TOI cap rest) + collides_with_sphere 0x0053a880 + normal_of_collision 0x0053ab50 + slide_sphere 0x0053b2a0. Pseudocode + settled BN x87 ambiguities (via ACE cross-ref) + two ACE bugs found and NOT copied (head-slide foot-disp; see doc §8): docs/research/2026-07-05-ccylsphere-collision-family-pseudocode.md Ethereal cylinders now flow through retail's Layer-2 override (pc:276961) instead of the early-OK consume — same net #150 behavior, plus retail placement-blocked-by-cylinder semantics. SlideSphere gains a sphereNum param (retail slides the head sphere by its own displacement, 0x0053b843). Register: AP-6 retired; AP-83 added (PerfectClip TOI tail decoded per ACE, dead code until missiles). Tests: CylSphereFamilyTests (grounded step-up onto the exact platform shape, tall-cylinder block, airborne top landing, ethereal guard); the #42 self-shadow control assertion updated to the retail observable (denied movement — the old ~1m radial self-push was the approximation's artifact, not retail). Suites: Core 2533 / App 713 / UI 425 / Net 385 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
67c3357246
commit
6ab269894a
6 changed files with 1027 additions and 151 deletions
|
|
@ -46,6 +46,56 @@ Copy this block when adding a new issue:
|
|||
|
||||
---
|
||||
|
||||
## #172 — Town-network portal platform blocks instead of stepping up (CCylSphere family was never ported)
|
||||
|
||||
**Status:** 🟡 FIX SHIPPED 2026-07-05 (this commit) — pending user visual gate (walk up onto the Holtburg portal platform, then the 0x0007 dungeon run).
|
||||
**Severity:** HIGH (blocks dungeon access — gates the whole #137 repro)
|
||||
**Filed:** 2026-07-05
|
||||
**Component:** physics — CylSphere object collision response
|
||||
|
||||
**Description (user):** the Holtburg town-network portal sits on a stone
|
||||
platform the player collides with instead of stepping up onto it (retail just
|
||||
walks up). Entity `0xC0A9B465` = landblock stab #0x65, Setup `0x020019E3`,
|
||||
one CylSphere **r=2.597 m, h=0.256 m** — a 26 cm disc, trivially steppable in
|
||||
retail. Surfaced the moment #149 (`4cf6eeb`) started registering BSP-less
|
||||
stab CylSpheres (before that fix the platform had NO collision at all, so the
|
||||
player clipped through it — the collision *shape* was the #149 fix; the
|
||||
collision *response* was never retail).
|
||||
|
||||
**Root cause (probe-confirmed, `launch-137-repro.log`):** the pre-port
|
||||
`CylinderCollision` was a hand-rolled approximation (AP-6): step-up gate +
|
||||
radial wall-slide only. Every contact returned `Slid` with a horizontal rim
|
||||
normal (`[cyl-test] … result=Slid`, `[resolve] … n=(0.99,-0.11,0.00)`) and
|
||||
the player orbited the rim forever. The step-up *gate* passed (clearance
|
||||
0.256 ≤ 0.6) but `DoStepUp`'s internal step-down probe could never validate a
|
||||
landing ON the cylinder top — a cylinder has no polygons, and the port had no
|
||||
`step_sphere_down` cap-landing (top-disc contact plane). Airborne landings on
|
||||
tops (`land_on_cylinder` + the Collide-flag exact-TOI branch) were missing
|
||||
too.
|
||||
|
||||
**Fix (this commit):** verbatim port of the full retail `CCylSphere` family —
|
||||
dispatcher `intersects_sphere` 0x0053b440, `collides_with_sphere` 0x0053a880,
|
||||
`normal_of_collision` 0x0053ab50, `collide_with_point` 0x0053acb0,
|
||||
`slide_sphere` 0x0053b2a0, `step_sphere_up` 0x0053b310, `land_on_cylinder`
|
||||
0x0053b3d0, `step_sphere_down` 0x0053a9b0. Pseudocode + settled BN
|
||||
ambiguities + two ACE-bug findings:
|
||||
`docs/research/2026-07-05-ccylsphere-collision-family-pseudocode.md`.
|
||||
Register: AP-6 retired, AP-83 added (PerfectClip TOI tail per ACE, dead code
|
||||
in M1.5). Conformance: `CylSphereFamilyTests` (grounded step-up-onto-top on
|
||||
the exact platform shape, tall-cylinder block, airborne top landing, ethereal
|
||||
Layer-2 guard); the #42 self-shadow control assertion updated to the retail
|
||||
observable (denied movement, not the old artifact radial push).
|
||||
|
||||
**Files:** `src/AcDream.Core/Physics/TransitionTypes.cs` (`CylinderCollision`
|
||||
+ `Cyl*` family), `tests/AcDream.Core.Tests/Physics/CylSphereFamilyTests.cs`.
|
||||
|
||||
**Acceptance:** walk straight onto the Holtburg town-network portal platform
|
||||
(no rim slide); jumping onto it also lands. Doors/torches/NPC cylinders
|
||||
unregressed (suites green; #150 open-door behavior unchanged). Likely also
|
||||
advances #137's door-foot half — re-check in the dungeon repro.
|
||||
|
||||
---
|
||||
|
||||
## #171 — Group melee: monsters interpenetrate + facing drifts (sticky melee unbound, arrival radii = 0)
|
||||
|
||||
**Status:** DONE (2026-07-04) — **user visual gate PASSED** ("Looks good, ship it")
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ accepted-divergence entries (#96, #49, #50).
|
|||
|
||||
---
|
||||
|
||||
## 3. Documented approximation (AP) — 73 rows (AP-79 retired R5-V2 — the P4 TargetTracker adapter replaced by the ported TargetManager voyeur system; AP-82 added R5-V3 — sticky deep-overlap sign pin)
|
||||
## 3. Documented approximation (AP) — 73 rows (AP-79 retired R5-V2 — the P4 TargetTracker adapter replaced by the ported TargetManager voyeur system; AP-82 added R5-V3 — sticky deep-overlap sign pin; AP-6 retired 2026-07-05 — full CCylSphere family ported verbatim, residual AP-83 added same commit)
|
||||
|
||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||
|---|---|---|---|---|---|
|
||||
|
|
@ -108,7 +108,6 @@ accepted-divergence entries (#96, #49, #50).
|
|||
| AP-3 | Step-down chain triggered only when contact is invalid OR steeper than walkable; retail's `transitional_insert` OK-path ALWAYS runs it | `src/AcDream.Core/Physics/TransitionTypes.cs:1197` | Conditional preserves the observed-to-matter cases (edge departure, steep cliff-slide) without running the chain every step (per pc:273191 agent reports) | Steps where retail runs step-down despite a valid walkable contact (bump maintenance, edge-slide arming) are skipped — float-off or missed edge slides in untested geometry | `transitional_insert` OK-path pc:273191 |
|
||||
| AP-4 | CliffSlide check moved BEFORE retail's Branch-1 (`!OnWalkable` → restore+OK) gate, compensating our L.2.3i FloorZ OnWalkable bookkeeping | `src/AcDream.Core/Physics/TransitionTypes.cs:1316` | Retail's order with our incomplete OnWalkable stops the player dead every frame on steep slopes ("stay on the roof"); reorder restores downhill drift | CliffSlide fires in states where retail's Branch 1 would restore-and-OK — body slides where retail holds, e.g. contact-plane-bearing steep geometry near edges | retail EdgeSlide dispatch order (transitional_insert step-down failure) |
|
||||
| AP-5 | Step-down skips Placement validation for the contact-maintenance call (`runPlacement=false`); ACE/retail run it unconditionally (kept for DoStepUp) | `src/AcDream.Core/Physics/TransitionTypes.cs:3393` | Residual wall-slide artifacts made Placement misfire, leaving players stuck near walls; the skip was the targeted L.2.3h fix | Step-down can settle into positions Placement would reject — slight wall embedding, or accepting a step-down through overlap geometry retail catches | `CTransition::step_down` pc:272952; ACE Transition.cs:731-741 |
|
||||
| AP-6 | Analytic swept-sphere cylinder collision (XY overlap + step-over + wall-slide) instead of retail CylSphere functions via the 6-path dispatcher; A6.P6 step-over branch ports `step_sphere_up`'s clearance check | `src/AcDream.Core/Physics/TransitionTypes.cs:2601` | Claimed to match retail for the exercised cases (trunks, NPC bodies, door foot-colliders); step-over and step_up_slide fallback retro-fitted from retail when the door phantom surfaced | Unported branches (push direction, interpenetration resolution) differ from retail against cylinder entities — the phantom-collision / sticky-NPC family | `CCylSphere::step_sphere_up` pc:324516-324538 |
|
||||
| AP-7 | `calc_friction` threshold 0.0 with retail's state gate missing; retail uses 0.25 gated by an undecoded state check | `src/AcDream.Core/Physics/PhysicsBody.cs:307` | Bumping the threshold without the gate hammered normal walking (3 → 0.16 m/s); as-read 0.0 kept; locomotion probably state-exempted in retail. Filed L.3c-followup | Friction engages under different conditions — post-landing slides, knockback decay, sledding speeds mismatch retail's deceleration | pc:276702-276705 (state gate + 0.25) |
|
||||
| AP-10 | Dry-corner water depth: retail's 0.1 m allowed sink-in collapsed to 0 | `src/AcDream.Core/Physics/TerrainSurface.cs:481` | The 0.1 offset destabilizes the feet-exactly-on-plane contact-touch check (dist > EPSILON → SetContactPlane never fires → float/fall); retail's ~10 cm sink-in is visually indistinguishable | Masks a contact-touch epsilon fragility — other water-depth values exercising the same instability could oscillate shoreline walkable validation; retail's wet/dry corner sink-in visual absent | `ObjCell.get_water_depth` / `calc_water_depth` (via ACE port) |
|
||||
| AP-11 | Hand-authored 4-keyframe fallback sky set (sunrise/noon/sunset, fog ~80–350 m) when the Region dat isn't loaded yet | `src/AcDream.Core/World/SkyState.cs:167` | A renderable sky is needed during boot before the Region dat parses; safety net on region-load failure | Any window where the fallback is active shows sky/fog lighting only roughly resembling retail's dat-driven values | SkyTimeOfDay keyframes, Region dat 0x13000000 |
|
||||
|
|
@ -180,6 +179,7 @@ accepted-divergence entries (#96, #49, #50).
|
|||
| AP-80 | **PlanFromVelocity survives for velocity-only NPC cycles** (M16): UpdatePosition-derived speed picks Ready/Walk/Run cycles for server-controlled creatures whose UMs never arrive (scripted-path NPCs); retail derives every cycle from motion messages through the motion tables (R4-V4 note; pre-existing mechanism, row added per the V4 plan) | `src/AcDream.Core/Physics/ServerControlledLocomotion.cs` (`PlanFromVelocity`); consumer `GameWindow.ApplyServerControlledVelocityCycle` | Some ACE entities move by position updates alone — without this, they slide in T-pose; constants (StopSpeed 0.2, RunThreshold 1.25) tuned against live ACE traffic | Cycle-pick thresholds are acdream inventions — a creature intended to walk fast may show run legs near the threshold | retire in R6 (root motion + full per-tick order) |
|
||||
| AP-81 | **Remote-DR gravity toggled via the Gravity STATE bit**: the jump handler sets `Body.State \|= Gravity` at VectorUpdate and both landing blocks clear it after `HitGround()`; retail keeps GRAVITY set for the object's whole life and gates gravity ACCELERATION on the Contact transient (`calc_acceleration`) (pre-existing K-fix9/K-fix15 mechanism, row added during #161 — which also fixed the ordering so `Motion.HitGround()`'s verbatim `state&0x400` gate runs BEFORE the clear) | `src/AcDream.App/Rendering/GameWindow.cs` (VectorUpdate jump handler + the two landing blocks) | The DR tick integrates gravity only for airborne remotes; the flag dance delivers exactly that without porting the full contact-gated `calc_acceleration` chain; the #161 ordering fix keeps the retail HitGround contract satisfied | Any NEW call into `Motion.HitGround`/`LeaveGround` placed after the clear silently no-ops on the gravity gate (the #161 leg-2 class); grounded remotes carry a non-retail state word (probes comparing state bits vs retail mislead) | `CPhysicsObj::calc_acceleration` (contact-gated); `set_on_walkable` 0x00511310; retire in R6 (contact-gated accel + persistent GRAVITY) |
|
||||
| AP-82 | **StickyManager deep-overlap back-off sign pin**: when the stick-gap overlap exceeds one tick's step (`speed×quantum < \|dist\|`, `dist < 0`), acdream applies `delta = −(speed×quantum)` (rate-limited back-off); ACE's literal port keeps `+delta` there — a runaway that steers INTO the target with equilibrium at centers-coincident. The BN mush (0x00555554-0x00555597) is unreadable on exactly this compare; the pin is refuted-by-evidence against ACE-literal: #171 gate-3 probe showed 1661 deep-overlap ticks all steering inward (monsters converged to centerDist≈0 — "monster inside the player") while retail side-by-side on the same ACE shows separation. ACE servers essentially never reach the branch (quantum ≥1/30 → threshold ~1 m; render-rate quanta → ~0.13 m) | `src/AcDream.Core/Physics/Motion/StickyManager.cs` (`AdjustOffset` delta clamp; conformance `StickyManagerTests.AdjustOffset_DeepOverlap_BacksOff_RateLimited`) | Minimal interpretation consistent with the mush structure AND observed retail; identical to ACE-literal in every shallow/outside case | If retail's true deep-overlap behavior differs (e.g. no movement at all), our back-off rate diverges in that rare state; verify via cdb `StickyManager::adjust_offset` trace with a forced overlap when convenient | `StickyManager::adjust_offset` 0x00555430 (x87 mush); ACE StickyManager.cs:117-121 (the literal branch this pin overrides) |
|
||||
| AP-83 | **CylCollideWithPoint PerfectClip TOI sub-branches decoded via ACE, not the binary**: the CCylSphere family port (2026-07-05, retires AP-6) reads `collide_with_point`'s PerfectClip time-of-impact math (0x0053adb6+) from ACE `CylSphere.CollideWithPoint` because the BN x87 mush is unreadable there; two ACE-verbatim quirks ported as-is (`movement.Z + radius` in the not-definite ascending case; `GlobalCurrCenter[0]` used even for head-sphere hits — the latter matches the raw decomp read). NOT exercised in M1.5: no mover sets PerfectClip (players never do; the non-PerfectClip path — SetCollisionNormal + Collided — is decomp-verified). Separately, the grounded head-sphere slide passes the HEAD disp per retail 0x0053b843 where ACE passes the foot disp — retail wins (ACE bug, not copied) | `src/AcDream.Core/Physics/TransitionTypes.cs` (`CylCollideWithPoint`; pseudocode doc `docs/research/2026-07-05-ccylsphere-collision-family-pseudocode.md` §7-8) | The load-bearing paths (non-PerfectClip Collided; the family's step-up/step-down/land) are decomp-verified; the TOI tail is dead code until missiles arm PerfectClip | If missiles (F.3) arm PerfectClip, the two ACE quirks may diverge from retail — clip-through or wrong deflection on cylinder targets; re-decompile 0x0053acb0 in Ghidra before shipping missiles | `CCylSphere::collide_with_point` 0x0053acb0 (pc:324173, x87 mush from 0x0053adb6); ACE CylSphere.cs `CollideWithPoint` |
|
||||
|
||||
## 4. Temporary stopgap (TS) — 39 rows (TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,236 @@
|
|||
# CCylSphere collision family — retail pseudocode (port prep)
|
||||
|
||||
**Date:** 2026-07-05 · **Trigger:** the Holtburg town-network portal platform
|
||||
(stab `0xC0A9B465`, Setup `0x020019E3`, CylSphere r=2.597 m h=0.256 m) blocks
|
||||
the player with an endless rim slide instead of the retail step-up-onto-top.
|
||||
Surfaced the moment #149 (`4cf6eeb`) started registering BSP-less stab
|
||||
CylSpheres — the collision SHAPE is right; the RESPONSE family was never
|
||||
ported. Feeds #137 (dungeon door feet flow through the same dispatcher).
|
||||
|
||||
**Sources:** named-retail pseudo-C (addresses below) = ground truth;
|
||||
`references/ACE/Source/ACE.Server/Physics/CylSphere.cs` = cross-reference
|
||||
(settles BN x87 garbles; one ACE bug found, noted in §8).
|
||||
|
||||
## Retail function inventory
|
||||
|
||||
| Function | Address | pseudo-C line |
|
||||
|---|---|---|
|
||||
| `CCylSphere::intersects_sphere(CTransition*)` — dispatcher | `0x0053b440` | :324558 |
|
||||
| `CCylSphere::intersects_sphere(Position*, float scale, CTransition*)` — wrapper | `0x0053b8f0` | :324744 |
|
||||
| `CCylSphere::collides_with_sphere` | `0x0053a880` | :323943 |
|
||||
| `CCylSphere::normal_of_collision` | `0x0053ab50` | :324102 |
|
||||
| `CCylSphere::collide_with_point` | `0x0053acb0` | :324173 |
|
||||
| `CCylSphere::slide_sphere` | `0x0053b2a0` | :324502 |
|
||||
| `CCylSphere::step_sphere_up` | `0x0053b310` | :324516 |
|
||||
| `CCylSphere::land_on_cylinder` | `0x0053b3d0` | :324542 |
|
||||
| `CCylSphere::step_sphere_down` | `0x0053a9b0` | :324032 |
|
||||
| `COLLISIONINFO::set_contact_plane(plane, is_water)` | `0x00509d80` | :271925 |
|
||||
|
||||
## 1. Wrapper (0x0053b8f0) — globalize the cylinder
|
||||
|
||||
```
|
||||
intersects_sphere(cyl, Position* objPos, float scale, CTransition* t):
|
||||
SPHEREPATH::cache_localspace_sphere(&t->sphere_path, objPos, 1f)
|
||||
world_cyl = { low_pt: objPos.localtoglobal(cyl.low_pt * scale),
|
||||
radius: cyl.radius * scale,
|
||||
height: cyl.height * scale }
|
||||
return world_cyl.intersects_sphere(t) // axis stays world-Z
|
||||
```
|
||||
|
||||
**acdream mapping:** `ShadowEntry` already stores the globalized base point
|
||||
(`Position` = entity pos + rotated scaled local offset, registration sites in
|
||||
`GameWindow.cs`) and pre-scaled Radius/CylHeight — the wrapper's work is done
|
||||
at registration. `cache_localspace_sphere` matters only for
|
||||
`localspace_pos` (used by step_sphere_up's normal rotation, §6).
|
||||
|
||||
## 2. collides_with_sphere (0x0053a880) — pure overlap test
|
||||
|
||||
```
|
||||
collides_with_sphere(cyl, CSphere* sphere, Vector3* disp, float radsum):
|
||||
// disp = sphere.center − cyl.low_pt (caller computes)
|
||||
if (disp.x² + disp.y² <= radsum²) // XY overlap
|
||||
halfH = cyl.height * 0.5
|
||||
if (|halfH − disp.z| <= sphere.radius − F_EPSILON + halfH) // Z band
|
||||
return 1
|
||||
return 0
|
||||
```
|
||||
|
||||
`radsum` at every call site = `cyl.radius − F_EPSILON + sphere.radius`
|
||||
(ε shaved ONCE, in the dispatcher preamble). The ε is what makes "resting
|
||||
exactly on the top" a non-overlap, so landings settle instead of re-colliding.
|
||||
|
||||
## 3. Dispatcher (0x0053b440)
|
||||
|
||||
```
|
||||
intersects_sphere(cyl, CTransition* t): // cyl in world frame
|
||||
sp = t.sphere_path; oi = t.object_info
|
||||
s0 = sp.global_sphere[0]; disp0 = s0.center − low_pt
|
||||
if sp.num_sphere > 1: s1 = sp.global_sphere[1]; disp1 = s1.center − low_pt
|
||||
radsum = cyl.radius − F_EPSILON + s0.radius
|
||||
|
||||
// ── branch 1: placement / ethereal — detection only ──
|
||||
if (sp.insert_type == PLACEMENT_INSERT || sp.obstruction_ethereal):
|
||||
if collides(s0, disp0) → COLLIDED
|
||||
if num_sphere>1 && collides(s1, disp1) → COLLIDED
|
||||
return OK
|
||||
|
||||
// ── branch 2: step-down probe — land on the top ──
|
||||
if (sp.step_down): return step_sphere_down(t, s0, disp0, radsum)
|
||||
|
||||
// ── branch 3: walkable probe — cylinder occupancy blocks ──
|
||||
if (sp.check_walkable):
|
||||
if collides(s0, disp0) → COLLIDED
|
||||
if num_sphere>1 && collides(s1, disp1) → COLLIDED
|
||||
return OK
|
||||
|
||||
// ── branch 4: normal sweep (collide flag clear) ──
|
||||
if (!sp.collide):
|
||||
if (oi.state & (CONTACT|ON_WALKABLE)): // grounded
|
||||
if collides(s0, disp0) → step_sphere_up(t, s0, disp0, radsum)
|
||||
if num_sphere>1 && collides(s1, disp1)
|
||||
→ slide_sphere(t, s1, disp1, radsum, sphereNum=1) // §8: retail passes disp1
|
||||
elif (oi.state & PATH_CLIPPED):
|
||||
if collides(s0, disp0) → collide_with_point(t, s0, disp0, radsum, 0)
|
||||
else: // airborne
|
||||
if collides(s0, disp0) → land_on_cylinder(t, s0, disp0, radsum)
|
||||
if num_sphere>1 && collides(s1, disp1)
|
||||
→ collide_with_point(t, s1, disp1, radsum, 1)
|
||||
return OK
|
||||
|
||||
// ── branch 5: collide-flag re-test — exact-TOI cap landing ──
|
||||
if collides(s0,disp0) || (num_sphere>1 && collides(s1,disp1)):
|
||||
movement = sp.global_curr_center[0] − s0.center − block_offset(cur→check)
|
||||
if |movement.z| < F_EPSILON → COLLIDED
|
||||
timecheck = (height + s0.radius − disp0.z) / movement.z
|
||||
offset = movement * timecheck
|
||||
if radsum² < |xy(offset + disp0)|² → OK // rewound off the cap
|
||||
t2 = (1 − timecheck) * sp.walk_interp
|
||||
if t2 >= sp.walk_interp || t2 < −0.1 → COLLIDED
|
||||
pt = s0.center + offset; pt.z −= s0.radius
|
||||
ci.set_contact_plane(Plane(n=(0,0,1), d=−pt.z), is_water=1) // literal 1, §7
|
||||
ci.contact_plane_cell_id = sp.check_pos.objcell_id
|
||||
sp.walk_interp = t2
|
||||
sp.add_offset_to_check_pos(offset)
|
||||
return ADJUSTED
|
||||
return OK
|
||||
```
|
||||
|
||||
State bits (verified against our `ObjectInfoState`): CONTACT=0x1,
|
||||
ON_WALKABLE=0x2, PATH_CLIPPED=0x8, PERFECT_CLIP=0x40.
|
||||
|
||||
## 4. step_sphere_down (0x0053a9b0) — land on the top during a step-down probe
|
||||
|
||||
```
|
||||
step_sphere_down(t, s0, disp0, radsum):
|
||||
if !collides(s0,disp0) && !(num_sphere>1 && collides(s1,disp1)) → OK
|
||||
stepScale = sp.step_down_amt * sp.walk_interp
|
||||
if |stepScale| < F_EPSILON → COLLIDED
|
||||
deltaz = height + s0.radius − disp0.z // lift so bottom rests on top
|
||||
interp = (1 − deltaz / stepScale) * sp.walk_interp // divisor = stepScale (BN garbled; ACE)
|
||||
if interp >= sp.walk_interp || interp < −0.1 → COLLIDED
|
||||
contactPt = (s0.center.x, s0.center.y, s0.center.z + deltaz − s0.radius)
|
||||
ci.set_contact_plane(Plane(n=(0,0,1), d=−contactPt.z), is_water=1) // §7
|
||||
ci.contact_plane_cell_id = sp.check_pos.objcell_id
|
||||
sp.walk_interp = interp
|
||||
sp.add_offset_to_check_pos((0,0,deltaz))
|
||||
return ADJUSTED
|
||||
```
|
||||
|
||||
This is THE missing piece that made step-up-onto-a-wide-cylinder impossible:
|
||||
`CTransition::step_up`'s internal step-down probe needs branch 2 to produce a
|
||||
walkable contact plane ON the cylinder top.
|
||||
|
||||
## 5. normal_of_collision (0x0053ab50)
|
||||
|
||||
```
|
||||
normal_of_collision(cyl, sp, sphere, dispCheck, radsum, sphereNum, out n) → bool definite:
|
||||
dispCurr = sp.global_curr_center[sphereNum] − low_pt
|
||||
if (radsum² < dispCurr.x² + dispCurr.y²): // curr was XY-OUTSIDE → side hit
|
||||
n = (dispCurr.x, dispCurr.y, 0) // radial, horizontal
|
||||
// definite unless the contact could actually be a diagonal cap hit:
|
||||
zBandOverlapAtCurr = |halfH − dispCurr.z| <= sphere.radius − F_EPSILON + halfH
|
||||
noZMovement = |dispCurr.z − dispCheck.z| <= F_EPSILON
|
||||
return zBandOverlapAtCurr || noZMovement
|
||||
// curr was XY-INSIDE the footprint → cap hit
|
||||
n = (0, 0, (dispCheck.z − dispCurr.z <= 0) ? +1 : −1) // descending → top (+1)
|
||||
return true
|
||||
```
|
||||
|
||||
Cap polarity settled by ACE + geometry (BN's x87 branch rendering is
|
||||
untrustworthy here — [[feedback_bn_decomp_field_names]] class 2).
|
||||
|
||||
## 6. step_sphere_up (0x0053b310) / land_on_cylinder (0x0053b3d0) / slide_sphere (0x0053b2a0)
|
||||
|
||||
```
|
||||
step_sphere_up(t, s0, disp0, radsum):
|
||||
if (oi.step_up_height < s0.radius + height − disp0.z) // too tall
|
||||
→ slide_sphere(t, s0, disp0, radsum, 0)
|
||||
definite = normal_of_collision(..., 0, out n)
|
||||
if normalize_check_small(n) → COLLIDED
|
||||
nWorld = localspace_pos.localtoglobalvec(n) // rotate by the OBJECT's frame
|
||||
if CTransition::step_up(t, nWorld) → OK
|
||||
else → sp.step_up_slide(t)
|
||||
|
||||
land_on_cylinder(t, s0, disp0, radsum): // airborne foot hit
|
||||
normal_of_collision(..., 0, out n)
|
||||
if normalize_check_small(n) → COLLIDED
|
||||
sp.set_collide(n) // backup + Collide flag
|
||||
sp.walkable_allowance = LANDING_Z (0.0871557)
|
||||
return ADJUSTED
|
||||
|
||||
slide_sphere(t, sphere, disp, radsum, sphereNum):
|
||||
normal_of_collision(..., sphereNum, out n)
|
||||
if normalize_check_small(n) → COLLIDED
|
||||
return CSphere::slide_sphere(sphere, sp, ci, n, sp.global_curr_center[sphereNum])
|
||||
```
|
||||
|
||||
The airborne landing closes through the retry loop: land_on_cylinder
|
||||
(ADJUSTED, sets `sp.collide`) → next attempt → branch 5 exact-TOI rests the
|
||||
sphere on the top + CP → next attempt → ε-shaved overlap now misses → OK →
|
||||
TransitionalInsert Phase 3 `sp.Collide` placement re-test validates on the
|
||||
CP → landing completes.
|
||||
|
||||
## 7. collide_with_point (0x0053acb0) — PathClipped / head-sphere hits
|
||||
|
||||
Port per ACE `CylSphere.CollideWithPoint` verbatim (self-contained TOI math):
|
||||
non-PerfectClip movers → `set_collision_normal` + COLLIDED. PerfectClip →
|
||||
exact time-of-impact reposition (`add_offset_to_check_pos`) + ADJUSTED, with
|
||||
the not-definite branch deriving cap-vs-side from the movement.
|
||||
|
||||
## 8. Divergences + settled ambiguities (register-relevant)
|
||||
|
||||
1. **`is_water=1` on cylinder-top contact planes is RETAIL** (literal 1 at
|
||||
0x0053aae2 and the branch-5 site; `set_contact_plane` 0x00509d80 stores
|
||||
arg3 → `contact_plane_is_water`). Port verbatim; do not "fix".
|
||||
2. **ACE bug (do NOT copy):** ACE's grounded head-sphere leg passes the FOOT
|
||||
disp to `SlideSphere`; retail 0x0053b843 passes the HEAD disp (`x_2`).
|
||||
Retail wins. (Class: [[feedback_bn_decomp_field_names]] #3 — ACE decode
|
||||
wrong in a branch ACE rarely exercises.)
|
||||
3. **Block offset in branch 5:** retail subtracts the cur→check landblock
|
||||
offset; acdream's physics frame is continuous world-space → offset = 0.
|
||||
Standing frame adaptation (same as SlideSphere's gDelta note).
|
||||
4. **Ethereal targets:** branch 1 returns COLLIDED on overlap even for
|
||||
ethereal; passability comes from the caller's Layer-2 override
|
||||
(pc:276961-276989, non-static + !step_down → forced OK) plus the #150
|
||||
step-down skip. The previous port consumed ObstructionEthereal with an
|
||||
early OK before any test — response-equivalent for non-static targets,
|
||||
but branch 1 is the faithful shape and also gives placement inserts the
|
||||
retail blocked-by-cylinder semantics. Ported faithfully now.
|
||||
5. **`normalize_check_small`** = normalize; returns true (fail) when |v| < ε
|
||||
before normalizing — maps to `LengthSquared() < EpsilonSq` guard.
|
||||
6. **step_sphere_up normal rotation:** retail rotates the collision normal by
|
||||
the target OBJECT's frame (`localspace_pos` = the object's Position cached
|
||||
by the wrapper) before `CTransition::step_up`. For yaw-only AC objects
|
||||
this only affects yawed radial normals; ported faithfully via
|
||||
`Vector3.Transform(n, obj.Rotation)`.
|
||||
|
||||
## 9. acdream port surface
|
||||
|
||||
`Transition.CylinderCollision` (TransitionTypes.cs) becomes the branch-4/5
|
||||
dispatcher body; new private siblings `CylCollidesWithSphere`,
|
||||
`CylNormalOfCollision`, `CylStepSphereUp`, `CylStepSphereDown`,
|
||||
`CylSlideSphere`, `CylLandOnCylinder`, `CylCollideWithPoint`. Callers
|
||||
unchanged (`FindObjCollisionsInCell` Cylinder branch; the BspOnlyDispatch
|
||||
gate and the #150 ethereal step-down skip sit ABOVE this dispatch and are
|
||||
unaffected). `DoStepUp` (= CTransition::step_up, A6.P6) and
|
||||
`SpherePath.StepUpSlide` are reused as-is.
|
||||
Loading…
Add table
Add a link
Reference in a new issue