test(physics): S6 — the camera provably reaches both PerfectClip TOI tails; contained, not dormant
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
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
AP-83/AP-91 claimed no current mover sets PerfectClip. The containment proof found the opposite and the contract's honest-fallback fired: the camera probe (the sole production setter) reaches BOTH ACE-derived tails live — the viewer exemption is creature-only, the shadow-list walk is unconditional, and static scenery with authored primitives is a real non-creature population. Every reach is now recorded (camera-live silently; any non-viewer mover loudly, one-shot), so a future flag change cannot exercise unreviewed ACE-derived math silently. Four tests drive the camera's exact call shape both ways; the sabotage was intelligently adapted — there was no existing cut to disable, so it flips the one axis the proof depends on (IsCreature) and asserts reachability inverts. Both register rows rewritten CONTAINED-not-dormant with severity narrowed to camera-feel (the probe never commits a PhysicsBody). Landing note: diagnostics-only diff (two guard calls + counters + corrected stale comments), verified directly by the session lead rather than a review cycle — the review budget went where behaviour changed tonight. Campaign S CLOSES with this landing: S1A/S1B/S2/S4/S5/S6 done, S3 cancelled, three user-passed gates, one honestly-open item — AD-66's reland, twice self-refused by its own stability gate, blocked on the #341 codegen-shape measurement instability whose ABA evidence and first discriminating experiment are filed. Clean-room suite: 11,257 passed / 6 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
00c03a33a5
commit
c5443b3df9
5 changed files with 425 additions and 10 deletions
|
|
@ -263,8 +263,8 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
|
||||||
| 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-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-85 | **Point-light pool = single 128-cap player-nearest list, optionally FILTERED by LAST FRAME's rendered visible-cell set, vs retail's dual pools (7 dynamic + 40 static, degrade-scaled) collected from a DBObj-load/flush-bounded resident registry** (A7.L1, 2026-07-09 — third revision, Town Network starvation fix #79/#93/#176/#177): retail's `CEnvCell::visible_cell_table` (`add_visible_cell` 0x0052de40) is populated ON DEMAND as cells are approached/seen (`DBObj::Get`-loads) and pruned by `flush_cells` — so a real dungeon's per-frame candidate set stays small (naturally proximity-bounded) even though the collection walk itself (`add_dynamic_lights` 0x0052d410) is "the whole resident table, not a re-flood." acdream's `_all` list instead registers at LANDBLOCK-granularity load/unload (a whole single-landblock dungeon streams as ONE unit), so for the Town Network (463 registered fixtures, one landblock) `_all` is effectively "everything ever loaded in this dungeon," not a proximity-bounded set — wide enough that the player-nearest-128 cap alone let a straight-line-closer-but-wall-disconnected corridor's fixtures out-rank the player's own room, starving it. Fix: `BuildPointLightSnapshot(playerWorldPos, visibleCells)` takes an optional candidacy FILTER — a light joins the pool iff `CellId==0` (cell-less, always in) or `visibleCells.Contains(CellId)` — narrowing candidates to the frame's actual visible cells BEFORE the existing dynamics-first player-nearest cap runs; `GameWindow` feeds LAST FRAME's already-rendered `RetailPViewFrameResult.DrawableCells` back to `WorldRenderFrameBuilder` (one frame / ~16 ms latency, chosen specifically to avoid re-threading a mid-`DrawInside` callback — the exact mechanism, `c500912b`, that caused the #176 seam-floor flicker regression when it re-flooded an independent CAMERA-seeded set mid-frame). The distance-sort anchor stays the PLAYER (unchanged from the prior revision) — only candidacy narrows. Remaining deviation: this is a RENDER-visibility approximation of retail's true on-demand-load/flush RESIDENCY bound, with one frame of latency, not a port of the DBObj-load/flush mechanism itself; and the pool is still ONE 128-cap list vs retail's separate 7-dynamic/40-static degrade-scaled pools | `src/AcDream.Core/Lighting/LightManager.cs` (`BuildPointLightSnapshot`, `MaxGlobalLights`); `src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs` (`RuntimeWorldFrameEnvironmentPreparation.ObserveDrawableCells`, `ClearDrawableCells`, `Prepare`); pins `PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant`, `PointSnapshot_OverCap_DynamicsNeverEvictedByNearerStatics`, `PointSnapshot_ResidentCollection_CellTagDoesNotFilter`, `BuildPointLightSnapshot_VisibleCellScoping_RoomLightsSurviveOverEuclideanCloserInvisibleCell`, `BuildPointLightSnapshot_VisibleCellScoping_CellLessLightAlwaysIncluded` | The render already computes a visible-cell set every frame for drawing (single source of truth, no duplicate flood) — reusing it as a candidacy filter approximates retail's proximity-bounded residency without porting DBObj on-demand load/flush; one-frame latency is imperceptible at normal camera speeds and structurally differs from the reverted mechanism (no independent re-flood mid-frame) | On a portal crossing, the FIRST indoor frame after re-entry (or after any outdoor-only frame) is unscoped (fail-open) — one frame may show slightly wider pool composition than steady-state; a room with >7 resident dynamics still shows them all (retail trims to 7 player-nearest) — slightly purpler wedge than retail; adopt the dual pools + degrade caps + true DBObj-bounded residency in later A7-arc work | `insert_light` 0x0054d1b0 (player-sorted, capped); `add_visible_cell` 0x0052de40 (on-demand-load resident registry + flush); `add_dynamic_lights` 0x0052d410 (whole-table walk); caller 0x00452d30; `calc_point_light` 0x0059c8b0 (static 1/d³ curve — A7 fix #2) |
|
| AP-85 | **Point-light pool = single 128-cap player-nearest list, optionally FILTERED by LAST FRAME's rendered visible-cell set, vs retail's dual pools (7 dynamic + 40 static, degrade-scaled) collected from a DBObj-load/flush-bounded resident registry** (A7.L1, 2026-07-09 — third revision, Town Network starvation fix #79/#93/#176/#177): retail's `CEnvCell::visible_cell_table` (`add_visible_cell` 0x0052de40) is populated ON DEMAND as cells are approached/seen (`DBObj::Get`-loads) and pruned by `flush_cells` — so a real dungeon's per-frame candidate set stays small (naturally proximity-bounded) even though the collection walk itself (`add_dynamic_lights` 0x0052d410) is "the whole resident table, not a re-flood." acdream's `_all` list instead registers at LANDBLOCK-granularity load/unload (a whole single-landblock dungeon streams as ONE unit), so for the Town Network (463 registered fixtures, one landblock) `_all` is effectively "everything ever loaded in this dungeon," not a proximity-bounded set — wide enough that the player-nearest-128 cap alone let a straight-line-closer-but-wall-disconnected corridor's fixtures out-rank the player's own room, starving it. Fix: `BuildPointLightSnapshot(playerWorldPos, visibleCells)` takes an optional candidacy FILTER — a light joins the pool iff `CellId==0` (cell-less, always in) or `visibleCells.Contains(CellId)` — narrowing candidates to the frame's actual visible cells BEFORE the existing dynamics-first player-nearest cap runs; `GameWindow` feeds LAST FRAME's already-rendered `RetailPViewFrameResult.DrawableCells` back to `WorldRenderFrameBuilder` (one frame / ~16 ms latency, chosen specifically to avoid re-threading a mid-`DrawInside` callback — the exact mechanism, `c500912b`, that caused the #176 seam-floor flicker regression when it re-flooded an independent CAMERA-seeded set mid-frame). The distance-sort anchor stays the PLAYER (unchanged from the prior revision) — only candidacy narrows. Remaining deviation: this is a RENDER-visibility approximation of retail's true on-demand-load/flush RESIDENCY bound, with one frame of latency, not a port of the DBObj-load/flush mechanism itself; and the pool is still ONE 128-cap list vs retail's separate 7-dynamic/40-static degrade-scaled pools | `src/AcDream.Core/Lighting/LightManager.cs` (`BuildPointLightSnapshot`, `MaxGlobalLights`); `src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs` (`RuntimeWorldFrameEnvironmentPreparation.ObserveDrawableCells`, `ClearDrawableCells`, `Prepare`); pins `PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant`, `PointSnapshot_OverCap_DynamicsNeverEvictedByNearerStatics`, `PointSnapshot_ResidentCollection_CellTagDoesNotFilter`, `BuildPointLightSnapshot_VisibleCellScoping_RoomLightsSurviveOverEuclideanCloserInvisibleCell`, `BuildPointLightSnapshot_VisibleCellScoping_CellLessLightAlwaysIncluded` | The render already computes a visible-cell set every frame for drawing (single source of truth, no duplicate flood) — reusing it as a candidacy filter approximates retail's proximity-bounded residency without porting DBObj on-demand load/flush; one-frame latency is imperceptible at normal camera speeds and structurally differs from the reverted mechanism (no independent re-flood mid-frame) | On a portal crossing, the FIRST indoor frame after re-entry (or after any outdoor-only frame) is unscoped (fail-open) — one frame may show slightly wider pool composition than steady-state; a room with >7 resident dynamics still shows them all (retail trims to 7 player-nearest) — slightly purpler wedge than retail; adopt the dual pools + degrade caps + true DBObj-bounded residency in later A7-arc work | `insert_light` 0x0054d1b0 (player-sorted, capped); `add_visible_cell` 0x0052de40 (on-demand-load resident registry + flush); `add_dynamic_lights` 0x0052d410 (whole-table walk); caller 0x00452d30; `calc_point_light` 0x0059c8b0 (static 1/d³ curve — A7 fix #2) |
|
||||||
| AP-84 | **BSP shadow-shape part poses = motion-table default-state frame snapshot at registration, not retail's live CPhysicsPart pose** (#175): server entities with a wire MotionTableId register their BSP part shapes at the default style's first-cycle LowFrame pose through `LiveEntityDefaultPoseResolver`; retail collision reads each part's CURRENT pose every test. Equivalent for the door lifecycle (closed = default pose; open = ETHEREAL bypasses collision entirely, #150) and for idle statics | `src/AcDream.App/Physics/LiveEntityDefaultPoseResolver.cs`; `src/AcDream.App/Physics/LiveEntityCollisionBuilder.cs`; `src/AcDream.Core/Physics/ShadowShapeBuilder.cs` (`partPoseOverride`) | Registration is one-shot in acdream (retail re-poses parts per frame); the default-state pose is the correct idle pose and the only non-ethereal pose doors ever collide in | An entity whose server-driven motion state materially MOVES a BSP-bearing part while NON-ethereal would collide at the stale default pose (no known case — doors are the dominant BSP-part weenies); revisit if animated non-ethereal BSP movers appear | `CPhysicsPart` live pose (see #150 notes); motion-table default state = CPartArray init; ShadowShapeBuilder placement-frame fallback for table-less entities |
|
| AP-84 | **BSP shadow-shape part poses = motion-table default-state frame snapshot at registration, not retail's live CPhysicsPart pose** (#175): server entities with a wire MotionTableId register their BSP part shapes at the default style's first-cycle LowFrame pose through `LiveEntityDefaultPoseResolver`; retail collision reads each part's CURRENT pose every test. Equivalent for the door lifecycle (closed = default pose; open = ETHEREAL bypasses collision entirely, #150) and for idle statics | `src/AcDream.App/Physics/LiveEntityDefaultPoseResolver.cs`; `src/AcDream.App/Physics/LiveEntityCollisionBuilder.cs`; `src/AcDream.Core/Physics/ShadowShapeBuilder.cs` (`partPoseOverride`) | Registration is one-shot in acdream (retail re-poses parts per frame); the default-state pose is the correct idle pose and the only non-ethereal pose doors ever collide in | An entity whose server-driven motion state materially MOVES a BSP-bearing part while NON-ethereal would collide at the stale default pose (no known case — doors are the dominant BSP-part weenies); revisit if animated non-ethereal BSP movers appear | `CPhysicsPart` live pose (see #150 notes); motion-table default state = CPartArray init; ShadowShapeBuilder placement-frame fallback for table-less entities |
|
||||||
| 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). No current mover sets PerfectClip: players never do, and shipped ordinary missiles add PathClipped only. 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 remains dormant unless a future mover explicitly enables PerfectClip | If a future mover explicitly enables PerfectClip, the two ACE quirks may diverge from retail — clip-through or wrong deflection on cylinder targets; re-decompile 0x0053acb0 in Ghidra before shipping that mover | `CCylSphere::collide_with_point` 0x0053acb0 (pc:324173, x87 mush from 0x0053adb6); ACE CylSphere.cs `CollideWithPoint` |
|
| AP-83 | **CONTAINED, not dormant (Campaign S S6, 2026-08-07) — the row's 'no current mover sets PerfectClip' premise was FALSE.** The camera (`PhysicsCameraCollisionProbe.SweepEye`, the sole production PerfectClip setter) reaches this tail LIVE: neither `CollisionExemption.ShouldSkip` (creature-only viewer exemption) nor `FindObjCollisionsInCell` (unconditional shadow-list walk) cuts the chain for a non-creature Cyl-shaped shadow entry — a real population (static scenery with an authored primitive and no physics BSP). The tail head now records every reach (`PhysicsDiagnostics.RecordCylPerfectClipTailReach`): viewer movers count camera-live silently; any NON-viewer mover reaching it logs loudly one-shot, so a future flag change cannot exercise this ACE-derived math unreviewed. Four containment tests drive the camera's exact call shape both ways, sabotage-verified on the creature-exemption axis the proof depends on. **Severity narrowed to camera-feel only**: the probe never commits a PhysicsBody, so a wrong TOI can only mispull the spring-arm camera. The math itself remains ACE-derived and the row stays ACTIVE for that reason alone. Original text: **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). No current mover sets PerfectClip: players never do, and shipped ordinary missiles add PathClipped only. 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 remains dormant unless a future mover explicitly enables PerfectClip | **Risk restated at S6:** the camera ALREADY reaches this tail — an ACE/retail TOI delta here is a live, currently-unverified camera-feel risk (a prop the camera pulls in slightly off), not a dormant one. If a future mover explicitly enables PerfectClip, the two ACE quirks may diverge from retail — clip-through or wrong deflection on cylinder targets; re-decompile 0x0053acb0 in Ghidra before shipping that mover | `CCylSphere::collide_with_point` 0x0053acb0 (pc:324173, x87 mush from 0x0053adb6); ACE CylSphere.cs `CollideWithPoint` |
|
||||||
| AP-91 | **CSphere `collide_with_point` PerfectClip TOI decoded via ACE, not the binary**: the CSphere family port reads the unreadable x87 tail from ACE `Sphere.CollideWithPoint`/`FindTimeOfCollision`; no current mover sets PerfectClip, and shipped ordinary missiles add PathClipped only | `src/AcDream.Core/Physics/TransitionTypes.cs` (`SphereCollideWithPoint`; `FindSphereTimeOfCollision`) | Load-bearing non-PerfectClip behavior is named-decomp verified; the adapted branch remains dormant unless a future mover explicitly enables PerfectClip | If a future mover explicitly enables PerfectClip, an ACE/retail TOI delta could cause clip-through or wrong sphere-target deflection | `CSphere::collide_with_point @ 0x00537230`; ACE `Sphere.CollideWithPoint` |
|
| AP-91 | **CONTAINED, not dormant (Campaign S S6, 2026-08-07) — the row's 'no current mover sets PerfectClip' premise was FALSE.** The camera (`PhysicsCameraCollisionProbe.SweepEye`, the sole production PerfectClip setter) reaches this tail LIVE: neither `CollisionExemption.ShouldSkip` (creature-only viewer exemption) nor `FindObjCollisionsInCell` (unconditional shadow-list walk) cuts the chain for a non-creature Sphere-shaped shadow entry — a real population (static scenery with an authored primitive and no physics BSP). The tail head now records every reach (`PhysicsDiagnostics.RecordSpherePerfectClipTailReach`): viewer movers count camera-live silently; any NON-viewer mover reaching it logs loudly one-shot, so a future flag change cannot exercise this ACE-derived math unreviewed. Four containment tests drive the camera's exact call shape both ways, sabotage-verified on the creature-exemption axis the proof depends on. **Severity narrowed to camera-feel only**: the probe never commits a PhysicsBody, so a wrong TOI can only mispull the spring-arm camera. The math itself remains ACE-derived and the row stays ACTIVE for that reason alone. Original text: **CSphere `collide_with_point` PerfectClip TOI decoded via ACE, not the binary**: the CSphere family port reads the unreadable x87 tail from ACE `Sphere.CollideWithPoint`/`FindTimeOfCollision`; no current mover sets PerfectClip, and shipped ordinary missiles add PathClipped only | `src/AcDream.Core/Physics/TransitionTypes.cs` (`SphereCollideWithPoint`; `FindSphereTimeOfCollision`) | Load-bearing non-PerfectClip behavior is named-decomp verified; the adapted branch remains dormant unless a future mover explicitly enables PerfectClip | **Risk restated at S6:** the camera ALREADY reaches this tail — an ACE/retail TOI delta here is a live, currently-unverified camera-feel risk (a prop the camera pulls in slightly off), not a dormant one. If a future mover explicitly enables PerfectClip, an ACE/retail TOI delta could cause clip-through or wrong sphere-target deflection | `CSphere::collide_with_point @ 0x00537230`; ACE `Sphere.CollideWithPoint` |
|
||||||
| AP-86 | **Remote SHADOW-follows-resolved via a pose/cell-gated per-tick re-flood** (remote-creature de-overlap #184): every remote's collision shadow is rewritten at the resolved body position by the DR tick or authoritative UP tail, so collision remains where the creature renders and de-overlap persists. The effect matches retail, but acdream runs the full multipart cell flood whenever the body moved more than 1 cm, changed complete orientation, or crossed a cell instead of translating the existing shadow in place and relinking only when its crossed-cell set changes. Cross-cell motion now commits body/root/full-cell before the canonical rebucket callback; local and authoritative remote publishers prove exact-record spatial residency after that callback; pending projection suspends the retained shadow and cannot re-add it, including initial-pending and callback GUID-reuse cases. | `src/AcDream.App/Physics/RemotePhysicsUpdater.cs`; `src/AcDream.App/Physics/LiveEntityShadowPublisher.cs`; `src/AcDream.App/Rendering/GameWindow.cs` (local projection); `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` (authoritative UP tails); `src/AcDream.App/World/LiveEntityPresentationController.cs` (ordinary projection residency); `src/AcDream.Core/Physics/ShadowObjectRegistry.cs` (`UpdatePosition`) | The pose/cell gate is exact at de-overlap equilibrium, preserves offset/multipart shapes during in-place turns, and the resulting registered cell set matches retail; loaded/pending residency is symmetric and incarnation-scoped | A dense moving or turning crowd can still perform a full registration flood per creature per tick and create CPU/Gen0 pressure; a still crowd is gated out. Retire with an in-place move plus cell-relink-on-change implementation | `CPhysicsObj::SetPositionInternal(CTransition const*)` 0x00515330 → `change_cell`, then `remove_shadows_from_cells`/`add_shadows_to_cells` after the resolved frame/contact commit |
|
| AP-86 | **Remote SHADOW-follows-resolved via a pose/cell-gated per-tick re-flood** (remote-creature de-overlap #184): every remote's collision shadow is rewritten at the resolved body position by the DR tick or authoritative UP tail, so collision remains where the creature renders and de-overlap persists. The effect matches retail, but acdream runs the full multipart cell flood whenever the body moved more than 1 cm, changed complete orientation, or crossed a cell instead of translating the existing shadow in place and relinking only when its crossed-cell set changes. Cross-cell motion now commits body/root/full-cell before the canonical rebucket callback; local and authoritative remote publishers prove exact-record spatial residency after that callback; pending projection suspends the retained shadow and cannot re-add it, including initial-pending and callback GUID-reuse cases. | `src/AcDream.App/Physics/RemotePhysicsUpdater.cs`; `src/AcDream.App/Physics/LiveEntityShadowPublisher.cs`; `src/AcDream.App/Rendering/GameWindow.cs` (local projection); `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` (authoritative UP tails); `src/AcDream.App/World/LiveEntityPresentationController.cs` (ordinary projection residency); `src/AcDream.Core/Physics/ShadowObjectRegistry.cs` (`UpdatePosition`) | The pose/cell gate is exact at de-overlap equilibrium, preserves offset/multipart shapes during in-place turns, and the resulting registered cell set matches retail; loaded/pending residency is symmetric and incarnation-scoped | A dense moving or turning crowd can still perform a full registration flood per creature per tick and create CPU/Gen0 pressure; a still crowd is gated out. Retire with an in-place move plus cell-relink-on-change implementation | `CPhysicsObj::SetPositionInternal(CTransition const*)` 0x00515330 → `change_cell`, then `remove_shadows_from_cells`/`add_shadows_to_cells` after the resolved frame/contact commit |
|
||||||
| AP-87 | **Remote MoveOrTeleport placement adds a 4 m body-to-target snap + a no-Sequencer snap** beyond retail's <96 m-unconditional interpolate (remote-creature de-overlap #184, 2026-07-07; unified across player-remote and NPC-remote by C4 route 4a, 2026-08-03 — retail's disassembly makes no `this==player` distinction here either, so the two formerly-duplicated per-kind copies are now the SAME decision): retail `CPhysicsObj::MoveOrTeleport` (0x00516330) hard-places only on the teleport-timestamp / cell==0 branch or the ≥96 m far-snap, and InterpolateTo-queues every near correction; acdream ADDS two snap conditions — `|Body.Position − worldPos| > 4 m` (a large correction / an unplaced first-UP body) and `!willBeDrTicked` (no Sequencer to consume the queue). Without them an unplaced body (origin / spawn seed) would enqueue, the InterpolationManager's 100 m far-blip would fire, and the per-tick sweep would run over a huge distance in a cell not containing the body → garbage resolved pos → the reverted attempt's INVISIBLE monster. A third condition `firstUp` (`LastServerPosTime <= 0`) is RETAINED, not dropped, in the unified seam: it is a belt hint only — the 4 m guard is the load-bearing backstop — and it is structurally false for player remotes because the player-remote caller stamps `LastServerPosTime` in its diagnostic roll-forward block before it routes, so unifying the two copies on all three conditions leaves the player branch's own behaviour bit-identical | `src/AcDream.Runtime/Physics/RuntimeRemoteSteadyStatePosition.cs` (`ApplyInterpolate`, `BodySnapThreshold`); called from `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` for both the player-remote and NPC-remote near-Interpolate branches. C4 route 4b-2 (2026-08-04) deleted the App's two duplicated `MaxPhysicsDistance = 96f` / `BodySnapThreshold = 4f` constant pairs and both `_playerController?.Position ?? Vector3.Zero` fabrications: the far branch is now a canonical Runtime placement, and the cell-less/rejected/unclassified leftovers call this same seam (AP-137). The 4 m constant exists in exactly one place | acdream's catch-up+sweep needs the body already near the target (a valid nearby cell) for the per-frame sweep to be small; the 4 m snap keeps it there, and retail's own large-correction path (the 100 m far-blip) is upstream of it. The de-overlap sweep also uses the fixed human sphere (R 0.48 / H 1.835) for the mover regardless of creature size, so large packed creatures de-overlap at human radii — inherits **TS-46** | A grounded remote that legitimately lags >4 m from its server pos snaps (a small pop) where retail would slide; a no-Sequencer server-moved entity hard-snaps every UP (no DR smoothing). Both are rare. **2026-08-04 observed live, then FIXED AT THE SOURCE the same day**: the route 4a two-client test caught exactly this risk — a player remote jumping onto a house roof planted there and sat until it had drifted >4 m from the server's slid-down position, at which point this backstop fired (`producer=ap87-4m` in the capture) and blipped it instead of sliding. The CAUSE was the remote tick forging `Contact | OnWalkable` and deciding its landing edge from the contact-derived `ResolveResult.IsOnGround`; that is fixed (Bug B, `docs/ISSUES.md` #32) and the thresholds and conditions of this row are deliberately UNCHANGED. The snap remains the #184 invisible-but-solid backstop; it should simply fire far less often now that the body genuinely tracks the server, and less often again since AP-140's same-day retirement pointed both routing gates at CONTACT: a steep-face slide now interpolates toward the server pose every packet instead of being classified free-flight and hard-snapped, so `bodyToTarget` converges rather than being left to drift past 4 m. This row's own thresholds and conditions are unchanged by either fix | `CPhysicsObj::MoveOrTeleport` 0x00516330 (near-interpolate <96 m; teleport/cell-0 snap; far-snap ≥96 m); `InterpolationManager` 100 m `AutonomyBlipDistance` (the retail large-correction path) |
|
| AP-87 | **Remote MoveOrTeleport placement adds a 4 m body-to-target snap + a no-Sequencer snap** beyond retail's <96 m-unconditional interpolate (remote-creature de-overlap #184, 2026-07-07; unified across player-remote and NPC-remote by C4 route 4a, 2026-08-03 — retail's disassembly makes no `this==player` distinction here either, so the two formerly-duplicated per-kind copies are now the SAME decision): retail `CPhysicsObj::MoveOrTeleport` (0x00516330) hard-places only on the teleport-timestamp / cell==0 branch or the ≥96 m far-snap, and InterpolateTo-queues every near correction; acdream ADDS two snap conditions — `|Body.Position − worldPos| > 4 m` (a large correction / an unplaced first-UP body) and `!willBeDrTicked` (no Sequencer to consume the queue). Without them an unplaced body (origin / spawn seed) would enqueue, the InterpolationManager's 100 m far-blip would fire, and the per-tick sweep would run over a huge distance in a cell not containing the body → garbage resolved pos → the reverted attempt's INVISIBLE monster. A third condition `firstUp` (`LastServerPosTime <= 0`) is RETAINED, not dropped, in the unified seam: it is a belt hint only — the 4 m guard is the load-bearing backstop — and it is structurally false for player remotes because the player-remote caller stamps `LastServerPosTime` in its diagnostic roll-forward block before it routes, so unifying the two copies on all three conditions leaves the player branch's own behaviour bit-identical | `src/AcDream.Runtime/Physics/RuntimeRemoteSteadyStatePosition.cs` (`ApplyInterpolate`, `BodySnapThreshold`); called from `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` for both the player-remote and NPC-remote near-Interpolate branches. C4 route 4b-2 (2026-08-04) deleted the App's two duplicated `MaxPhysicsDistance = 96f` / `BodySnapThreshold = 4f` constant pairs and both `_playerController?.Position ?? Vector3.Zero` fabrications: the far branch is now a canonical Runtime placement, and the cell-less/rejected/unclassified leftovers call this same seam (AP-137). The 4 m constant exists in exactly one place | acdream's catch-up+sweep needs the body already near the target (a valid nearby cell) for the per-frame sweep to be small; the 4 m snap keeps it there, and retail's own large-correction path (the 100 m far-blip) is upstream of it. The de-overlap sweep also uses the fixed human sphere (R 0.48 / H 1.835) for the mover regardless of creature size, so large packed creatures de-overlap at human radii — inherits **TS-46** | A grounded remote that legitimately lags >4 m from its server pos snaps (a small pop) where retail would slide; a no-Sequencer server-moved entity hard-snaps every UP (no DR smoothing). Both are rare. **2026-08-04 observed live, then FIXED AT THE SOURCE the same day**: the route 4a two-client test caught exactly this risk — a player remote jumping onto a house roof planted there and sat until it had drifted >4 m from the server's slid-down position, at which point this backstop fired (`producer=ap87-4m` in the capture) and blipped it instead of sliding. The CAUSE was the remote tick forging `Contact | OnWalkable` and deciding its landing edge from the contact-derived `ResolveResult.IsOnGround`; that is fixed (Bug B, `docs/ISSUES.md` #32) and the thresholds and conditions of this row are deliberately UNCHANGED. The snap remains the #184 invisible-but-solid backstop; it should simply fire far less often now that the body genuinely tracks the server, and less often again since AP-140's same-day retirement pointed both routing gates at CONTACT: a steep-face slide now interpolates toward the server pose every packet instead of being classified free-flight and hard-snapped, so `bodyToTarget` converges rather than being left to drift past 4 m. This row's own thresholds and conditions are unchanged by either fix | `CPhysicsObj::MoveOrTeleport` 0x00516330 (near-interpolate <96 m; teleport/cell-0 snap; far-snap ≥96 m); `InterpolationManager` 100 m `AutonomyBlipDistance` (the retail large-correction path) |
|
||||||
| AP-89 | **TransparentPartHook fade multiplies the SAMPLED TEXTURE alpha, not a separate material alpha channel** (#188, 2026-07-08 — the fading-wall secret-passage doors, e.g. "Pedestal Weak Spot"): retail's `CPhysicsPart::SetTranslucency` (0x0050e670) → `CMaterial::SetTranslucencySimple` (0x005396f0) REPLACES the D3D9 material's 4 alpha channels wholesale (`Ambient.a = Diffuse.a = Specular.a = Emissive.a = 1 − translucency`) — a per-material alpha that composes with, but is conceptually separate from, the surface's own sampled texture alpha. acdream's `mesh_modern.frag` has no material-alpha concept at all; the port multiplies the runtime fade's opacity multiplier directly against the already-sampled `color.a` (`FragColor = vec4(rgb, color.a * vOpacityMultiplier)`) | `src/AcDream.App/Rendering/Shaders/mesh_modern.frag` (final `FragColor` line); `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (`ClassifyBatches` `opacityMultiplier` param, `InstanceGroup.Opacities`); `src/AcDream.Core/Rendering/TranslucencyFadeManager.cs` | Observably identical to retail for any surface whose base texture alpha is 1.0 everywhere — the Pedestal Weak Spot's stone-wall texture, and the overwhelming majority of AC surfaces, since `color.a * 1.0 == color.a` and the fade multiplier alone then drives the ramp exactly as `1 − translucency` would | A hypothetical object that is BOTH already alpha-keyed/blended from its own texture (stained glass, a flame surface) AND plays a TransparentPartHook fade simultaneously would compound the two alphas (texture-alpha × fade-multiplier) instead of the fade cleanly replacing/overriding the surface's own alpha as retail's material-replace does — such an object would fade darker / more-transparent than retail, not just at retail's rate | `CPhysicsPart::SetTranslucency` 0x0050e670; `CMaterial::SetTranslucencySimple` 0x005396f0 (`alpha = 1 − translucency`, applied to all 4 D3D9 material alpha channels) |
|
| AP-89 | **TransparentPartHook fade multiplies the SAMPLED TEXTURE alpha, not a separate material alpha channel** (#188, 2026-07-08 — the fading-wall secret-passage doors, e.g. "Pedestal Weak Spot"): retail's `CPhysicsPart::SetTranslucency` (0x0050e670) → `CMaterial::SetTranslucencySimple` (0x005396f0) REPLACES the D3D9 material's 4 alpha channels wholesale (`Ambient.a = Diffuse.a = Specular.a = Emissive.a = 1 − translucency`) — a per-material alpha that composes with, but is conceptually separate from, the surface's own sampled texture alpha. acdream's `mesh_modern.frag` has no material-alpha concept at all; the port multiplies the runtime fade's opacity multiplier directly against the already-sampled `color.a` (`FragColor = vec4(rgb, color.a * vOpacityMultiplier)`) | `src/AcDream.App/Rendering/Shaders/mesh_modern.frag` (final `FragColor` line); `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (`ClassifyBatches` `opacityMultiplier` param, `InstanceGroup.Opacities`); `src/AcDream.Core/Rendering/TranslucencyFadeManager.cs` | Observably identical to retail for any surface whose base texture alpha is 1.0 everywhere — the Pedestal Weak Spot's stone-wall texture, and the overwhelming majority of AC surfaces, since `color.a * 1.0 == color.a` and the fade multiplier alone then drives the ramp exactly as `1 − translucency` would | A hypothetical object that is BOTH already alpha-keyed/blended from its own texture (stained glass, a flame surface) AND plays a TransparentPartHook fade simultaneously would compound the two alphas (texture-alpha × fade-multiplier) instead of the fade cleanly replacing/overriding the surface's own alpha as retail's material-replace does — such an object would fade darker / more-transparent than retail, not just at retail's rate | `CPhysicsPart::SetTranslucency` 0x0050e670; `CMaterial::SetTranslucencySimple` 0x005396f0 (`alpha = 1 − translucency`, applied to all 4 D3D9 material alpha channels) |
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,16 @@ misreading — see its section) · S1B LANDED (b3e43d22, #335 closed) and S2 LAN
|
||||||
narrowed), Session-B dungeon gate USER-PASSED 2026-08-07 evening ("Feels
|
narrowed), Session-B dungeon gate USER-PASSED 2026-08-07 evening ("Feels
|
||||||
good!") · S4 half-landed (AD-65 shipped and USER-PASSED at the 2026-08-07 morning gate; AD-66 withheld
|
good!") · S4 half-landed (AD-65 shipped and USER-PASSED at the 2026-08-07 morning gate; AD-66 withheld
|
||||||
behind #341's measurement anomaly; AD-69 filed) · S5 closed (fix predated the
|
behind #341's measurement anomaly; AD-69 filed) · S5 closed (fix predated the
|
||||||
campaign; zombie register row) · S6 unstarted · #330 hoist landed, wiring
|
campaign; zombie register row) · S6 LANDED (containment: the camera provably reaches BOTH ACE-derived TOI
|
||||||
|
tails live — the rows' dormancy premise was false; guarded with counters +
|
||||||
|
one-shot unverified-mover log, four tests, sabotage on the real exemption
|
||||||
|
axis; AP-83/AP-91 rewritten CONTAINED-not-dormant, severity camera-feel
|
||||||
|
only) · **CAMPAIGN CLOSED 2026-08-07 night** with ONE honestly-open item:
|
||||||
|
AD-66's reland is blocked by the #341 codegen-shape measurement instability
|
||||||
|
(twice self-refused by its own stability gate; the ABA evidence and the
|
||||||
|
first discriminating experiment are in #341). The user's final slope look
|
||||||
|
travels with that reland. Next: #344, #343, #341's boundary hunt, then
|
||||||
|
vendors (M4) · #330 hoist landed, wiring
|
||||||
withheld with a seven-point scope map · #32/#338 pre-work both closed.
|
withheld with a seven-point scope map · #32/#338 pre-work both closed.
|
||||||
**Scope:** the twelve remaining collision-domain items — five shape/membership
|
**Scope:** the twelve remaining collision-domain items — five shape/membership
|
||||||
divergences, three resolution-math divergences, two undecodable-math rows, and
|
divergences, three resolution-math divergences, two undecodable-math rows, and
|
||||||
|
|
|
||||||
|
|
@ -2170,6 +2170,9 @@ public static class PhysicsDiagnostics
|
||||||
// Dump-trigger sets
|
// Dump-trigger sets
|
||||||
ProbeDumpCellIds = new System.Collections.Generic.HashSet<uint>();
|
ProbeDumpCellIds = new System.Collections.Generic.HashSet<uint>();
|
||||||
ProbeDumpGfxObjIds = new System.Collections.Generic.HashSet<uint>();
|
ProbeDumpGfxObjIds = new System.Collections.Generic.HashSet<uint>();
|
||||||
|
|
||||||
|
// S6 PerfectClip TOI-tail containment guard (AP-83/AP-91).
|
||||||
|
ResetPerfectClipTailGuardForTest();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlySet<uint> ParseHexIdList(string? raw)
|
private static IReadOnlySet<uint> ParseHexIdList(string? raw)
|
||||||
|
|
@ -2656,6 +2659,122 @@ public static class PhysicsDiagnostics
|
||||||
$"[step-h] site={site} stepUp={stepUp:F3} stepDown={stepDown:F3} {detail}");
|
$"[step-h] site={site} stepUp={stepUp:F3} stepDown={stepDown:F3} {detail}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// S6 (Campaign S, 2026-08-07) — AP-83/AP-91 PerfectClip TOI-tail
|
||||||
|
// containment guard.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S6 reachability proof
|
||||||
|
/// (<c>docs/research/2026-08-07-s6-perfectclip-containment-contract.md</c>):
|
||||||
|
/// the ONLY production site that sets
|
||||||
|
/// <see cref="ObjectInfoState.PerfectClip"/> on a mover is
|
||||||
|
/// <c>PhysicsCameraCollisionProbe.SweepEye</c>
|
||||||
|
/// (<c>src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs:69</c>),
|
||||||
|
/// which ALSO sets <see cref="ObjectInfoState.IsViewer"/>. The camera's
|
||||||
|
/// resolve DOES reach both the Cyl and Sphere PerfectClip time-of-impact
|
||||||
|
/// tails (<c>TransitionTypes.CylCollideWithPoint</c> / AP-83,
|
||||||
|
/// <c>SphereCollideWithPoint</c> / AP-91) — neither candidate cut from the
|
||||||
|
/// S6 scoping doc holds: <c>CollisionExemption.ShouldSkip</c> only
|
||||||
|
/// exempts a viewer mover against a CREATURE target
|
||||||
|
/// (<c>CollisionExemption.cs:91-94</c>), and
|
||||||
|
/// <c>TransitionTypes.FindObjCollisionsInCell</c> walks every cell's
|
||||||
|
/// shadow list unconditionally, with no viewer/mover-flag gate before the
|
||||||
|
/// per-target loop (called from <c>FindPrimaryCellCollisions</c>,
|
||||||
|
/// <c>TransitionTypes.cs:2477</c>). A non-creature Cyl/Sphere-shaped
|
||||||
|
/// shadow entry is a real production population — static landblock
|
||||||
|
/// scenery registered by <c>LandblockPhysicsPublisher.PublishStaticEntity</c>
|
||||||
|
/// with <c>EntityCollisionFlags.None</c> (tracked live via
|
||||||
|
/// <c>publication.CylinderOwnerCount</c>) — so the camera's foot sphere
|
||||||
|
/// reaches the tail whenever it overlaps one: the camera is always
|
||||||
|
/// PathClipped and never grounded (its resolve passes <c>body: null</c>,
|
||||||
|
/// <c>isOnGround: false</c>), which is exactly Branch 4's PathClipped
|
||||||
|
/// route in both <c>CylinderCollision</c> and <c>SphereCollision</c>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Call this at the head of the PerfectClip branch inside each TOI tail,
|
||||||
|
/// after confirming <c>ObjectInfo.State</c> carries
|
||||||
|
/// <see cref="ObjectInfoState.PerfectClip"/>. A mover that ALSO carries
|
||||||
|
/// <see cref="ObjectInfoState.IsViewer"/> is the verified-reachable
|
||||||
|
/// population from the proof above — this is expected production
|
||||||
|
/// behavior, not a bug, so it is recorded with a plain counter (never an
|
||||||
|
/// assertion/throw). A mover WITHOUT <c>IsViewer</c> reaching this code
|
||||||
|
/// has no verified reachability chain — the only prior candidate (a
|
||||||
|
/// PathClipped missile) was ported per ACE but never armed PerfectClip in
|
||||||
|
/// M1.5 (<c>PhysicsEngine.cs:1984</c>: "PerfectClip is deliberately not
|
||||||
|
/// inferred"). That population is logged LOUDLY (one-shot per tail) so a
|
||||||
|
/// future flag change cannot silently start executing ACE-derived math
|
||||||
|
/// nobody re-verified.
|
||||||
|
/// </remarks>
|
||||||
|
public static void RecordSpherePerfectClipTailReach(bool moverIsViewer) =>
|
||||||
|
RecordPerfectClipTailReachCore(
|
||||||
|
"Sphere", moverIsViewer,
|
||||||
|
ref _sphereToiCameraLiveCount, ref _sphereToiUnverifiedCount,
|
||||||
|
ref _sphereToiUnverifiedAnnounced);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="RecordSpherePerfectClipTailReach"/>
|
||||||
|
public static void RecordCylPerfectClipTailReach(bool moverIsViewer) =>
|
||||||
|
RecordPerfectClipTailReachCore(
|
||||||
|
"Cyl", moverIsViewer,
|
||||||
|
ref _cylToiCameraLiveCount, ref _cylToiUnverifiedCount,
|
||||||
|
ref _cylToiUnverifiedAnnounced);
|
||||||
|
|
||||||
|
private static void RecordPerfectClipTailReachCore(
|
||||||
|
string tail, bool moverIsViewer,
|
||||||
|
ref int cameraLiveCount, ref int unverifiedCount, ref int unverifiedAnnounced)
|
||||||
|
{
|
||||||
|
if (moverIsViewer)
|
||||||
|
{
|
||||||
|
System.Threading.Interlocked.Increment(ref cameraLiveCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Threading.Interlocked.Increment(ref unverifiedCount);
|
||||||
|
if (System.Threading.Interlocked.Exchange(ref unverifiedAnnounced, 1) != 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[perfectclip-tail] UNVERIFIED mover reached the {tail} PerfectClip "
|
||||||
|
+ "time-of-impact tail (AP-83/AP-91 — ACE-derived math with no retail "
|
||||||
|
+ "decompile; verified-reachable population is the camera / IsViewer "
|
||||||
|
+ "only). A non-viewer mover just executed this path; its reachability "
|
||||||
|
+ "was never re-verified — see "
|
||||||
|
+ "docs/research/2026-08-07-s6-perfectclip-containment-contract.md "
|
||||||
|
+ "before trusting the result.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int _sphereToiCameraLiveCount;
|
||||||
|
private static int _sphereToiUnverifiedCount;
|
||||||
|
private static int _sphereToiUnverifiedAnnounced;
|
||||||
|
private static int _cylToiCameraLiveCount;
|
||||||
|
private static int _cylToiUnverifiedCount;
|
||||||
|
private static int _cylToiUnverifiedAnnounced;
|
||||||
|
|
||||||
|
/// <summary>Diagnostic counter — see <see cref="RecordSpherePerfectClipTailReach"/>.</summary>
|
||||||
|
public static int SphereToiCameraLiveCount => _sphereToiCameraLiveCount;
|
||||||
|
/// <summary>Diagnostic counter — see <see cref="RecordSpherePerfectClipTailReach"/>.</summary>
|
||||||
|
public static int SphereToiUnverifiedCount => _sphereToiUnverifiedCount;
|
||||||
|
/// <summary>Diagnostic counter — see <see cref="RecordCylPerfectClipTailReach"/>.</summary>
|
||||||
|
public static int CylToiCameraLiveCount => _cylToiCameraLiveCount;
|
||||||
|
/// <summary>Diagnostic counter — see <see cref="RecordCylPerfectClipTailReach"/>.</summary>
|
||||||
|
public static int CylToiUnverifiedCount => _cylToiUnverifiedCount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test-only reset for the S6 PerfectClip-tail guard counters. Does NOT
|
||||||
|
/// reset the one-shot "announced" latches independently of the counts —
|
||||||
|
/// a full reset (counts AND latches) so a sabotage test that intends to
|
||||||
|
/// re-trigger the loud unverified log can observe it fire again.
|
||||||
|
/// </summary>
|
||||||
|
public static void ResetPerfectClipTailGuardForTest()
|
||||||
|
{
|
||||||
|
_sphereToiCameraLiveCount = 0;
|
||||||
|
_sphereToiUnverifiedCount = 0;
|
||||||
|
_sphereToiUnverifiedAnnounced = 0;
|
||||||
|
_cylToiCameraLiveCount = 0;
|
||||||
|
_cylToiUnverifiedCount = 0;
|
||||||
|
_cylToiUnverifiedAnnounced = 0;
|
||||||
|
}
|
||||||
|
|
||||||
private static int ParsePositiveInt(string? value) =>
|
private static int ParsePositiveInt(string? value) =>
|
||||||
int.TryParse(
|
int.TryParse(
|
||||||
value,
|
value,
|
||||||
|
|
|
||||||
|
|
@ -4803,8 +4803,15 @@ public sealed class Transition
|
||||||
/// PathClipped movers + airborne head-sphere hits. Non-PerfectClip records the
|
/// PathClipped movers + airborne head-sphere hits. Non-PerfectClip records the
|
||||||
/// center-to-center collision normal and hard-stops (the M1.5 load-bearing
|
/// center-to-center collision normal and hard-stops (the M1.5 load-bearing
|
||||||
/// path — players never set PerfectClip). PerfectClip gets the exact
|
/// path — players never set PerfectClip). PerfectClip gets the exact
|
||||||
/// time-of-impact reposition (missiles only — AP-91, dead in M1.5, ported per
|
/// time-of-impact reposition (ported per ACE Sphere.cs:175-210).
|
||||||
/// ACE Sphere.cs:175-210; re-verify vs Ghidra before missiles ship).
|
///
|
||||||
|
/// <para>
|
||||||
|
/// S6 (AP-91, 2026-08-07): this branch is NOT dead — the camera
|
||||||
|
/// (<c>PhysicsCameraCollisionProbe</c>) sets PerfectClip AND IsViewer on
|
||||||
|
/// every sweep and is not exempted from non-creature Cyl/Sphere shadow
|
||||||
|
/// entries, so it reaches here live in production. See the reachability
|
||||||
|
/// proof on <see cref="PhysicsDiagnostics.RecordSpherePerfectClipTailReach"/>.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private TransitionState SphereCollideWithPoint(ShadowEntry obj, SpherePath sp,
|
private TransitionState SphereCollideWithPoint(ShadowEntry obj, SpherePath sp,
|
||||||
Sphere checkSphere, float radsum, int sphereNum)
|
Sphere checkSphere, float radsum, int sphereNum)
|
||||||
|
|
@ -4819,7 +4826,11 @@ public sealed class Transition
|
||||||
return TransitionState.Collided;
|
return TransitionState.Collided;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PerfectClip exact time-of-impact (AP-91 — dead in M1.5). Block offset = 0.
|
// S6 containment guard (AP-91) — see PhysicsDiagnostics.RecordSpherePerfectClipTailReach.
|
||||||
|
PhysicsDiagnostics.RecordSpherePerfectClipTailReach(
|
||||||
|
(ObjectInfo.State & ObjectInfoState.IsViewer) != 0);
|
||||||
|
|
||||||
|
// PerfectClip exact time-of-impact reposition. Block offset = 0.
|
||||||
Vector3 checkOffset = checkSphere.Origin - gCenter;
|
Vector3 checkOffset = checkSphere.Origin - gCenter;
|
||||||
double toi = FindSphereTimeOfCollision(checkOffset, globalOffset, radsum + PhysicsGlobals.EPSILON);
|
double toi = FindSphereTimeOfCollision(checkOffset, globalOffset, radsum + PhysicsGlobals.EPSILON);
|
||||||
if (toi < PhysicsGlobals.EPSILON || toi > 1.0)
|
if (toi < PhysicsGlobals.EPSILON || toi > 1.0)
|
||||||
|
|
@ -5232,10 +5243,17 @@ public sealed class Transition
|
||||||
/// PathClipped movers + airborne head-sphere hits. Non-PerfectClip movers
|
/// PathClipped movers + airborne head-sphere hits. Non-PerfectClip movers
|
||||||
/// record the collision normal and hard-stop; PerfectClip movers get the
|
/// record the collision normal and hard-stop; PerfectClip movers get the
|
||||||
/// exact time-of-impact reposition. TOI sub-branches ported per ACE
|
/// exact time-of-impact reposition. TOI sub-branches ported per ACE
|
||||||
/// CylSphere.CollideWithPoint (BN mush too heavy in 0x0053adb6+); no
|
/// CylSphere.CollideWithPoint (BN mush too heavy in 0x0053adb6+).
|
||||||
/// PerfectClip mover exists in M1.5 (players never set it), so only the
|
///
|
||||||
/// Collided path is load-bearing today — revisit against Ghidra if
|
/// <para>
|
||||||
/// missiles ever arm PerfectClip (pseudocode doc §7).
|
/// S6 (AP-83, 2026-08-07): this branch is NOT dead — the camera
|
||||||
|
/// (<c>PhysicsCameraCollisionProbe</c>) sets PerfectClip AND IsViewer on
|
||||||
|
/// every sweep and is not exempted from non-creature Cyl-shaped shadow
|
||||||
|
/// entries (e.g. static landblock scenery with a CylSphere and no
|
||||||
|
/// physics BSP), so it reaches here live in production whenever its
|
||||||
|
/// PathClipped foot sphere overlaps one. See the reachability proof on
|
||||||
|
/// <see cref="PhysicsDiagnostics.RecordCylPerfectClipTailReach"/>.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private TransitionState CylCollideWithPoint(ShadowEntry obj, SpherePath sp,
|
private TransitionState CylCollideWithPoint(ShadowEntry obj, SpherePath sp,
|
||||||
float cylHeight, Sphere checkSphere, Vector3 disp, float radsum, int sphereNum)
|
float cylHeight, Sphere checkSphere, Vector3 disp, float radsum, int sphereNum)
|
||||||
|
|
@ -5251,6 +5269,10 @@ public sealed class Transition
|
||||||
return TransitionState.Collided;
|
return TransitionState.Collided;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// S6 containment guard (AP-83) — see PhysicsDiagnostics.RecordCylPerfectClipTailReach.
|
||||||
|
PhysicsDiagnostics.RecordCylPerfectClipTailReach(
|
||||||
|
(ObjectInfo.State & ObjectInfoState.IsViewer) != 0);
|
||||||
|
|
||||||
// Retail reads global_curr_center[0] even for the head hit
|
// Retail reads global_curr_center[0] even for the head hit
|
||||||
// (0x0053ad26; ACE agrees) — verbatim.
|
// (0x0053ad26; ACE agrees) — verbatim.
|
||||||
Vector3 globCenter = sp.GlobalCurrCenter[0].Origin;
|
Vector3 globCenter = sp.GlobalCurrCenter[0].Origin;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,265 @@
|
||||||
|
using System;
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Tests.Physics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S6 (Campaign S, 2026-08-07) — reachability proof + containment guard for
|
||||||
|
/// the AP-83/AP-91 PerfectClip time-of-impact tails
|
||||||
|
/// (<c>TransitionTypes.CylCollideWithPoint</c> / <c>SphereCollideWithPoint</c>).
|
||||||
|
/// Contract: docs/research/2026-08-07-s6-perfectclip-containment-contract.md.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// FINDING (not a failure — the contract explicitly anticipates this outcome):
|
||||||
|
/// BOTH scoping-doc candidate arguments for "the camera can never reach these
|
||||||
|
/// tails" fail. <c>CollisionExemption.ShouldSkip</c> only exempts a viewer
|
||||||
|
/// mover against a CREATURE target (CollisionExemption.cs:91-94) — a
|
||||||
|
/// non-creature Cyl/Sphere shadow entry passes straight through. And
|
||||||
|
/// <c>FindObjCollisionsInCell</c> walks every cell's shadow list
|
||||||
|
/// unconditionally, with no viewer skip-all gate before the per-target loop
|
||||||
|
/// (TransitionTypes.cs:3806, reached from <c>FindPrimaryCellCollisions</c> with
|
||||||
|
/// no mover-flag check, TransitionTypes.cs:2477). So the camera
|
||||||
|
/// (<c>PhysicsCameraCollisionProbe.SweepEye</c> — the ONLY production
|
||||||
|
/// PerfectClip setter, grep-confirmed against the whole src tree) DOES reach
|
||||||
|
/// both TOI tails whenever its PathClipped, never-grounded foot sphere
|
||||||
|
/// (moverFlags carry PathClipped; body=null + isOnGround=false means neither
|
||||||
|
/// Contact nor OnWalkable is ever seeded) overlaps a non-creature Cyl/Sphere
|
||||||
|
/// shadow entry. That population is real production content: static
|
||||||
|
/// landblock scenery with a CylSphere/Sphere and no physics BSP registers
|
||||||
|
/// with <c>EntityCollisionFlags.None</c>
|
||||||
|
/// (<c>LandblockPhysicsPublisher.PublishStaticEntity</c>, tracked live via
|
||||||
|
/// <c>publication.CylinderOwnerCount</c>).
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// These tests drive the REAL production resolve
|
||||||
|
/// (<c>PhysicsEngine.ResolveWithTransition</c>) with the camera's exact call
|
||||||
|
/// shape (IsViewer|PathClipped|FreeRotate|PerfectClip, single 0.3 m sphere,
|
||||||
|
/// body=null, isOnGround=false — mirrors <c>PhysicsCameraCollisionProbe
|
||||||
|
/// .SweepEye</c> and the harness already used by
|
||||||
|
/// <c>CameraCornerSealReplayTests</c> / <c>Issue180CorridorSweepHysteresis
|
||||||
|
/// ReplayTests</c>) against a synthetic Cyl/Sphere shadow entry, and assert
|
||||||
|
/// the guard (<c>PhysicsDiagnostics.RecordCylPerfectClipTailReach</c> /
|
||||||
|
/// <c>RecordSpherePerfectClipTailReach</c>) records the reach as camera-live,
|
||||||
|
/// never as unverified.
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// <b>Sabotage</b> (deliverable 3): the contract's literal wording is "disable
|
||||||
|
/// the exemption that cuts the chain and watch the reachability flip" — but
|
||||||
|
/// the proof found NOTHING currently cuts the chain for a non-creature
|
||||||
|
/// target, so there is no existing cut to disable. The faithful adaptation
|
||||||
|
/// flips the SAME one axis <c>CollisionExemption.ShouldSkip</c> actually
|
||||||
|
/// reads (<c>EntityCollisionFlags.IsCreature</c>) from off to on and asserts
|
||||||
|
/// reachability inverts: the guard goes silent and the camera walks straight
|
||||||
|
/// through, exactly the retail "camera ray ignores creatures" rule
|
||||||
|
/// (acclient_2013_pseudo_c.txt:276787-276790). This proves the guard is wired
|
||||||
|
/// to the actual condition the reachability proof depends on, not merely
|
||||||
|
/// "always fires regardless of input".
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public class S6PerfectClipTailContainmentTests
|
||||||
|
{
|
||||||
|
private readonly ITestOutputHelper _out;
|
||||||
|
public S6PerfectClipTailContainmentTests(ITestOutputHelper output) => _out = output;
|
||||||
|
|
||||||
|
private const uint TestLandblockId = 0xA9B40000u;
|
||||||
|
private const uint TestCellId = TestLandblockId | 0x0001u; // landcell (0,0), matches CylSphereFamilyTests/SphereCollisionFamilyTests
|
||||||
|
|
||||||
|
private const float ViewerSphereRadius = 0.3f; // retail viewer_sphere (acclient :93314)
|
||||||
|
|
||||||
|
private static readonly ObjectInfoState CameraMoverFlags =
|
||||||
|
ObjectInfoState.IsViewer | ObjectInfoState.PathClipped
|
||||||
|
| ObjectInfoState.FreeRotate | ObjectInfoState.PerfectClip;
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────
|
||||||
|
// Sphere tail (AP-91)
|
||||||
|
// ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CameraSweep_HitsNonCreatureSphereProp_ReachesTail_RecordsCameraLive()
|
||||||
|
{
|
||||||
|
PhysicsDiagnostics.ResetPerfectClipTailGuardForTest();
|
||||||
|
var engine = BuildEngine();
|
||||||
|
RegisterSphere(engine, 0x00005001u, new Vector3(12f, 14f, 1.0f), radius: 1.0f,
|
||||||
|
flags: EntityCollisionFlags.None); // static prop, not a creature
|
||||||
|
|
||||||
|
Vector3 pivot = new(12f, 10f, 1.0f);
|
||||||
|
Vector3 eye = new(12f, 20f, 1.0f); // straight through the prop's center
|
||||||
|
|
||||||
|
var r = SweepViewer(engine, pivot, eye, TestCellId);
|
||||||
|
|
||||||
|
_out.WriteLine(FormattableString.Invariant($"ok={r.Ok} pos=({r.Position.X:F3},{r.Position.Y:F3},{r.Position.Z:F3}) collNorm={r.CollisionNormalValid} normal=({r.CollisionNormal.X:F3},{r.CollisionNormal.Y:F3},{r.CollisionNormal.Z:F3}) cameraLive={PhysicsDiagnostics.SphereToiCameraLiveCount} unverified={PhysicsDiagnostics.SphereToiUnverifiedCount}"));
|
||||||
|
|
||||||
|
Assert.True(PhysicsDiagnostics.SphereToiCameraLiveCount > 0,
|
||||||
|
"The camera's PathClipped, never-grounded sweep must reach the Sphere PerfectClip TOI tail (AP-91) for a non-creature target.");
|
||||||
|
Assert.Equal(0, PhysicsDiagnostics.SphereToiUnverifiedCount);
|
||||||
|
|
||||||
|
// Golden: the sphere prop must have stopped the camera's forward sweep
|
||||||
|
// (PathClipped hard-stops at first contact) well short of the far side.
|
||||||
|
Assert.True(r.Position.Y < 14f,
|
||||||
|
$"camera must be stopped by the prop, not pass through it; got Y={r.Position.Y:F3}");
|
||||||
|
Assert.True(r.Position.Y > 11f,
|
||||||
|
$"camera must actually reach the prop's surface, not stop early; got Y={r.Position.Y:F3}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CameraSweep_HitsCreatureFlaggedSphere_ExemptionCutsChain_GuardStaysSilent()
|
||||||
|
{
|
||||||
|
PhysicsDiagnostics.ResetPerfectClipTailGuardForTest();
|
||||||
|
var engine = BuildEngine();
|
||||||
|
// SABOTAGE: flip the one axis CollisionExemption.ShouldSkip reads for a
|
||||||
|
// viewer mover — everything else about the geometry is identical to the
|
||||||
|
// reached case above.
|
||||||
|
RegisterSphere(engine, 0x00005002u, new Vector3(12f, 14f, 1.0f), radius: 1.0f,
|
||||||
|
flags: EntityCollisionFlags.IsCreature);
|
||||||
|
|
||||||
|
Vector3 pivot = new(12f, 10f, 1.0f);
|
||||||
|
Vector3 eye = new(12f, 20f, 1.0f);
|
||||||
|
|
||||||
|
var r = SweepViewer(engine, pivot, eye, TestCellId);
|
||||||
|
|
||||||
|
_out.WriteLine(FormattableString.Invariant($"ok={r.Ok} pos=({r.Position.X:F3},{r.Position.Y:F3},{r.Position.Z:F3}) cameraLive={PhysicsDiagnostics.SphereToiCameraLiveCount}"));
|
||||||
|
|
||||||
|
Assert.Equal(0, PhysicsDiagnostics.SphereToiCameraLiveCount);
|
||||||
|
Assert.Equal(0, PhysicsDiagnostics.SphereToiUnverifiedCount);
|
||||||
|
// Reachability flips: CollisionExemption now exempts the whole target
|
||||||
|
// for the viewer mover before shape dispatch, so the camera sails
|
||||||
|
// straight through to the far side (retail: camera ray ignores
|
||||||
|
// creatures, acclient_2013_pseudo_c.txt:276787-276790).
|
||||||
|
Assert.True(r.Position.Y > 19f,
|
||||||
|
$"a creature-flagged prop must be fully exempt for a viewer mover; got Y={r.Position.Y:F3}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────
|
||||||
|
// Cyl tail (AP-83)
|
||||||
|
// ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CameraSweep_HitsNonCreatureCylinderProp_ReachesTail_RecordsCameraLive()
|
||||||
|
{
|
||||||
|
PhysicsDiagnostics.ResetPerfectClipTailGuardForTest();
|
||||||
|
var engine = BuildEngine();
|
||||||
|
RegisterCylinder(engine, 0x00006001u, new Vector3(12f, 14f, 0f), radius: 1.0f, height: 2.0f,
|
||||||
|
flags: EntityCollisionFlags.None);
|
||||||
|
|
||||||
|
Vector3 pivot = new(12f, 10f, 1.0f);
|
||||||
|
Vector3 eye = new(12f, 20f, 1.0f);
|
||||||
|
|
||||||
|
var r = SweepViewer(engine, pivot, eye, TestCellId);
|
||||||
|
|
||||||
|
_out.WriteLine(FormattableString.Invariant($"ok={r.Ok} pos=({r.Position.X:F3},{r.Position.Y:F3},{r.Position.Z:F3}) collNorm={r.CollisionNormalValid} normal=({r.CollisionNormal.X:F3},{r.CollisionNormal.Y:F3},{r.CollisionNormal.Z:F3}) cameraLive={PhysicsDiagnostics.CylToiCameraLiveCount} unverified={PhysicsDiagnostics.CylToiUnverifiedCount}"));
|
||||||
|
|
||||||
|
Assert.True(PhysicsDiagnostics.CylToiCameraLiveCount > 0,
|
||||||
|
"The camera's PathClipped, never-grounded sweep must reach the Cyl PerfectClip TOI tail (AP-83) for a non-creature target.");
|
||||||
|
Assert.Equal(0, PhysicsDiagnostics.CylToiUnverifiedCount);
|
||||||
|
|
||||||
|
Assert.True(r.Position.Y < 14f,
|
||||||
|
$"camera must be stopped by the prop, not pass through it; got Y={r.Position.Y:F3}");
|
||||||
|
Assert.True(r.Position.Y > 11f,
|
||||||
|
$"camera must actually reach the prop's surface, not stop early; got Y={r.Position.Y:F3}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CameraSweep_HitsCreatureFlaggedCylinder_ExemptionCutsChain_GuardStaysSilent()
|
||||||
|
{
|
||||||
|
PhysicsDiagnostics.ResetPerfectClipTailGuardForTest();
|
||||||
|
var engine = BuildEngine();
|
||||||
|
RegisterCylinder(engine, 0x00006002u, new Vector3(12f, 14f, 0f), radius: 1.0f, height: 2.0f,
|
||||||
|
flags: EntityCollisionFlags.IsCreature);
|
||||||
|
|
||||||
|
Vector3 pivot = new(12f, 10f, 1.0f);
|
||||||
|
Vector3 eye = new(12f, 20f, 1.0f);
|
||||||
|
|
||||||
|
var r = SweepViewer(engine, pivot, eye, TestCellId);
|
||||||
|
|
||||||
|
_out.WriteLine(FormattableString.Invariant($"ok={r.Ok} pos=({r.Position.X:F3},{r.Position.Y:F3},{r.Position.Z:F3}) cameraLive={PhysicsDiagnostics.CylToiCameraLiveCount}"));
|
||||||
|
|
||||||
|
Assert.Equal(0, PhysicsDiagnostics.CylToiCameraLiveCount);
|
||||||
|
Assert.Equal(0, PhysicsDiagnostics.CylToiUnverifiedCount);
|
||||||
|
Assert.True(r.Position.Y > 19f,
|
||||||
|
$"a creature-flagged prop must be fully exempt for a viewer mover; got Y={r.Position.Y:F3}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────
|
||||||
|
// Harness (mirrors CylSphereFamilyTests / SphereCollisionFamilyTests)
|
||||||
|
// ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static PhysicsEngine BuildEngine()
|
||||||
|
{
|
||||||
|
var cache = new PhysicsDataCache();
|
||||||
|
var engine = new PhysicsEngine { DataCache = cache };
|
||||||
|
|
||||||
|
// Flat terrain at Z=0 across the whole landblock — irrelevant to these
|
||||||
|
// sweeps (they travel at Z≈1.0, well above ground) but required so the
|
||||||
|
// outdoor cell resolves.
|
||||||
|
var heights = new byte[81];
|
||||||
|
var heightTable = new float[256]; // all zero → terrain Z = 0
|
||||||
|
engine.AddLandblock(
|
||||||
|
landblockId: TestLandblockId,
|
||||||
|
terrain: new TerrainSurface(heights, heightTable),
|
||||||
|
cells: Array.Empty<CellSurface>(),
|
||||||
|
portals: Array.Empty<PortalPlane>(),
|
||||||
|
worldOffsetX: 0f,
|
||||||
|
worldOffsetY: 0f);
|
||||||
|
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mirror of PhysicsCameraCollisionProbe.SweepEye's transition call — same
|
||||||
|
/// shape already used by CameraCornerSealReplayTests,
|
||||||
|
/// Issue180CorridorSweepHysteresisReplayTests, and CylSphereFamilyTests'
|
||||||
|
/// landblock (0x0100 low word stays below the AdjustPosition threshold, so
|
||||||
|
/// the outdoor cellId is used as-is).
|
||||||
|
/// </summary>
|
||||||
|
private static ResolveResult SweepViewer(PhysicsEngine engine, Vector3 pivot, Vector3 desiredEye, uint cellId)
|
||||||
|
{
|
||||||
|
Vector3 begin = pivot - new Vector3(0f, 0f, ViewerSphereRadius);
|
||||||
|
Vector3 end = desiredEye - new Vector3(0f, 0f, ViewerSphereRadius);
|
||||||
|
|
||||||
|
return engine.ResolveWithTransition(
|
||||||
|
currentPos: begin,
|
||||||
|
targetPos: end,
|
||||||
|
cellId: cellId,
|
||||||
|
sphereRadius: ViewerSphereRadius,
|
||||||
|
sphereHeight: 0f,
|
||||||
|
stepUpHeight: 0f,
|
||||||
|
stepDownHeight: 0f,
|
||||||
|
isOnGround: false,
|
||||||
|
body: null,
|
||||||
|
moverFlags: CameraMoverFlags,
|
||||||
|
movingEntityId: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RegisterSphere(PhysicsEngine engine, uint entityId, Vector3 worldPos,
|
||||||
|
float radius, EntityCollisionFlags flags)
|
||||||
|
{
|
||||||
|
engine.ShadowObjects.Register(
|
||||||
|
entityId, gfxObjId: 0u,
|
||||||
|
worldPos, Quaternion.Identity, radius,
|
||||||
|
worldOffsetX: 0f, worldOffsetY: 0f, landblockId: TestLandblockId,
|
||||||
|
collisionType: ShadowCollisionType.Sphere,
|
||||||
|
cylHeight: 0f, scale: 1f,
|
||||||
|
state: 0u,
|
||||||
|
flags: flags,
|
||||||
|
isStatic: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RegisterCylinder(PhysicsEngine engine, uint entityId, Vector3 worldPos,
|
||||||
|
float radius, float height, EntityCollisionFlags flags)
|
||||||
|
{
|
||||||
|
engine.ShadowObjects.Register(
|
||||||
|
entityId, gfxObjId: 0u,
|
||||||
|
worldPos, Quaternion.Identity, radius,
|
||||||
|
worldOffsetX: 0f, worldOffsetY: 0f, landblockId: TestLandblockId,
|
||||||
|
collisionType: ShadowCollisionType.Cylinder,
|
||||||
|
cylHeight: height, scale: 1f,
|
||||||
|
state: 0u,
|
||||||
|
flags: flags,
|
||||||
|
isStatic: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue