fix(physics): Task 3 follow-up — obstruction_ethereal consume for Cylinder+Sphere shapes
Retail oracle greps confirmed: - CSphere::intersects_sphere @ 0x00537ae4 (pc:321692): the ethereal branch is `void __thiscall` — all paths return void (no COLLIDED). The function performs a proximity check only; no blocking result is produced. - CCylSphere::intersects_sphere @ 0x0053b4a0 (pc:324573): same void-return pattern — ethereal branch calls collides_with_sphere (check only, no slide), all returns are void = passable. Change: added `if (sp.ObstructionEthereal) return TransitionState.OK` at the top of SphereCollision and CylinderCollision in TransitionTypes.cs, mirroring the void-return semantics of both retail functions. The existing per-object clear at pc:276989 (line 2837) still fires after the early OK return. Before this fix: an ethereal-alone NPC/ghost with a Cylinder or Sphere shadow shape would BLOCK the player (regression introduced when Task 3 made ETHEREAL- alone fall through ShouldSkip instead of instant-skipping). After: all three shape types — BSP (via BSPQuery Path 1), Sphere, and Cylinder — correctly pass through when obstruction_ethereal is set. Tests: added 4 tests to ObstructionEtherealTests.cs verifying: - Ethereal Cylinder → passable (sweep passes through, no CollisionNormalValid) - Ethereal Sphere → passable (same) - Non-ethereal Cylinder → still blocks (regression guard) - Non-ethereal Sphere → still blocks (regression guard) Full Core suite: 1584 pass, 0 fail, 2 skip (pre-existing dat skips). Pseudocode doc updated with confirmed cyl/sphere ethereal contracts and the complete set/clear/consume flow summary. Retail refs: - CSphere::intersects_sphere @ 0x00537ae4 / acclient_2013_pseudo_c.txt:321692 - CCylSphere::intersects_sphere @ 0x0053b4a0 / acclient_2013_pseudo_c.txt:324573 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3361a8d776
commit
dc1e927080
3 changed files with 298 additions and 20 deletions
|
|
@ -150,15 +150,43 @@ else:
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
**acdream scope note:** `CylinderCollision` in `TransitionTypes.cs` does NOT implement
|
**acdream implementation (Task 3 follow-up, 2026-06-24):** `CylinderCollision` in
|
||||||
this gate. That is a separate task. Cylinder-type shadow entries for ETHEREAL objects
|
`TransitionTypes.cs` now implements this gate as `if (sp.ObstructionEthereal) return OK`
|
||||||
would not be blocked by the BSP path (BSP is separate), so the ETHEREAL Cyl gate
|
at the top of the method — mirroring exactly how `SphereCollision` (consume site 1) is
|
||||||
primarily matters for objects that are purely Cylinder (no BSP) — which is less common
|
handled. The retail function is `void __thiscall`, so all returns in the ethereal branch
|
||||||
than BSP objects. Task 3 scope covers BSP.
|
are void (= OK). An ethereal Cylinder (e.g. an NPC ghost) is now fully passable.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Summary: set / clear / consume flow for Task 3
|
## 3b. Consume site 1 — `CSphere::intersects_sphere` (pc:321692) — full contract
|
||||||
|
|
||||||
|
The full decomp at pc:321692 / 0x537ae4 confirms:
|
||||||
|
|
||||||
|
```
|
||||||
|
// pc:321692:
|
||||||
|
if (obstruction_ethereal != 0 || insert_type == PLACEMENT_INSERT):
|
||||||
|
// distSq check — if sphere NOT overlapping: return (void = passable)
|
||||||
|
if (distSq < radiusSumSq): // overlapping:
|
||||||
|
// calls collides_with_sphere on sphere[1] if num_sphere > 1
|
||||||
|
// returns void — no COLLIDED
|
||||||
|
return // void = passable in all cases
|
||||||
|
else:
|
||||||
|
// step_down / check_walkable / slide blocking paths
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key:** the `CSphere::intersects_sphere` function is `void __thiscall` — there is NO
|
||||||
|
`return 2` (COLLIDED) from the ethereal branch. The inner `collides_with_sphere` call
|
||||||
|
(for the penetrating case) also produces no blocking result (it may contribute to a
|
||||||
|
side-channel contact; it does NOT stop the player). The player is passable in ALL
|
||||||
|
sub-cases of the ethereal branch.
|
||||||
|
|
||||||
|
**acdream implementation (Task 3 follow-up, 2026-06-24):** `SphereCollision` implements
|
||||||
|
`if (sp.ObstructionEthereal) return OK` at the top. The inner overlap/slide path is
|
||||||
|
bypassed — matches the void-return semantics of the retail ethereal branch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary: set / clear / consume flow (complete after Task 3 follow-up)
|
||||||
|
|
||||||
```
|
```
|
||||||
FindObjCollisionsLoop():
|
FindObjCollisionsLoop():
|
||||||
|
|
@ -170,10 +198,10 @@ FindObjCollisionsLoop():
|
||||||
// Set flag for ETHEREAL-alone
|
// Set flag for ETHEREAL-alone
|
||||||
sp.ObstructionEthereal = (obj.State & 0x4) != 0
|
sp.ObstructionEthereal = (obj.State & 0x4) != 0
|
||||||
|
|
||||||
// Shape dispatch (BSP path):
|
// Shape dispatch — ALL three shapes now consume the flag:
|
||||||
// BSPQuery.FindCollisions → Path 1 checks ObstructionEthereal
|
// BSP: BSPQuery.FindCollisions Path 1 → sphere_intersects_solid (passable)
|
||||||
// When ObstructionEthereal=true → sphere_intersects_solid (passable)
|
// Sphere: SphereCollision → if (ObstructionEthereal) return OK
|
||||||
// When ObstructionEthereal=false → normal blocking paths
|
// Cylinder: CylinderCollision → if (ObstructionEthereal) return OK
|
||||||
|
|
||||||
// Clear after per-object test (retail pc:276989)
|
// Clear after per-object test (retail pc:276989)
|
||||||
sp.ObstructionEthereal = false
|
sp.ObstructionEthereal = false
|
||||||
|
|
@ -183,10 +211,12 @@ FindEnvCollisions():
|
||||||
// BSP dispatch against cell walls ...
|
// BSP dispatch against cell walls ...
|
||||||
```
|
```
|
||||||
|
|
||||||
**Contracts:**
|
**Contracts (complete):**
|
||||||
- ETHEREAL-only target (`state = 0x0001000C`): `ShouldSkip` returns false → flag set to
|
- ETHEREAL-alone BSP target (e.g. open door): `ShouldSkip` = false → flag set → BSP
|
||||||
true → BSP Path 1 fires → `sphere_intersects_solid` test → player walks through open
|
Path 1 fires → `sphere_intersects_solid` → player walks through open door. Passable.
|
||||||
door's BSP (no solid leaf at the opening) → OK → passable.
|
- ETHEREAL-alone Sphere target (e.g. ghost NPC with sphere shape): flag set →
|
||||||
- Non-ethereal wall: flag = false → Path 1 DOES NOT fire → normal blocking paths → player
|
`SphereCollision` returns OK immediately. Passable.
|
||||||
stopped.
|
- ETHEREAL-alone Cylinder target (e.g. ghost NPC with cyl shape): flag set →
|
||||||
- ENV cell walls: flag cleared before BSP dispatch → ENV BSP always uses the blocking path.
|
`CylinderCollision` returns OK immediately. Passable.
|
||||||
|
- Non-ethereal wall/object: flag = false → normal blocking paths. Blocked.
|
||||||
|
- ENV cell walls: flag cleared before BSP dispatch → ENV BSP always blocking. Blocked.
|
||||||
|
|
|
||||||
|
|
@ -2986,6 +2986,15 @@ public sealed class Transition
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private TransitionState SphereCollision(ShadowEntry obj, SpherePath sp)
|
private TransitionState SphereCollision(ShadowEntry obj, SpherePath sp)
|
||||||
{
|
{
|
||||||
|
// Consume site 1 — CSphere::intersects_sphere @ 0x00537ae4 (pc:321692).
|
||||||
|
// When obstruction_ethereal is set (target is ETHEREAL-alone, state & 0x4),
|
||||||
|
// the retail function is void and all paths in the ethereal branch return
|
||||||
|
// without producing a COLLIDED/Slid result — the player is fully passable.
|
||||||
|
// We mirror this by returning OK immediately, skipping all blocking paths.
|
||||||
|
// Retail ref: acclient_2013_pseudo_c.txt:321692.
|
||||||
|
if (sp.ObstructionEthereal)
|
||||||
|
return TransitionState.OK;
|
||||||
|
|
||||||
var ci = CollisionInfo;
|
var ci = CollisionInfo;
|
||||||
Vector3 sphereCurrPos = sp.GlobalCurrCenter[0].Origin;
|
Vector3 sphereCurrPos = sp.GlobalCurrCenter[0].Origin;
|
||||||
Vector3 sphereCheckPos = sp.GlobalSphere[0].Origin;
|
Vector3 sphereCheckPos = sp.GlobalSphere[0].Origin;
|
||||||
|
|
@ -3090,6 +3099,15 @@ public sealed class Transition
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private TransitionState CylinderCollision(ShadowEntry obj, SpherePath sp, PhysicsEngine engine)
|
private TransitionState CylinderCollision(ShadowEntry obj, SpherePath sp, PhysicsEngine engine)
|
||||||
{
|
{
|
||||||
|
// Consume site 2 — CCylSphere::intersects_sphere @ 0x0053b4a0 (pc:324573).
|
||||||
|
// When obstruction_ethereal is set (target is ETHEREAL-alone, state & 0x4),
|
||||||
|
// the retail function is void and all paths in the ethereal branch return
|
||||||
|
// without producing a COLLIDED/Slid result — the player is fully passable.
|
||||||
|
// We mirror this by returning OK immediately, skipping all blocking paths.
|
||||||
|
// Retail ref: acclient_2013_pseudo_c.txt:324573.
|
||||||
|
if (sp.ObstructionEthereal)
|
||||||
|
return TransitionState.OK;
|
||||||
|
|
||||||
var ci = CollisionInfo;
|
var ci = CollisionInfo;
|
||||||
var oi = ObjectInfo;
|
var oi = ObjectInfo;
|
||||||
Vector3 sphereCurrPos = sp.GlobalCurrCenter[0].Origin;
|
Vector3 sphereCurrPos = sp.GlobalCurrCenter[0].Origin;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Numerics;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -21,7 +22,7 @@ namespace AcDream.Core.Tests.Physics;
|
||||||
/// </para>
|
/// </para>
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Consume site: <c>BSPTREE::find_collisions @ 0x0053a496</c>
|
/// Consume site 1: <c>BSPTREE::find_collisions @ 0x0053a496</c>
|
||||||
/// (<c>acclient_2013_pseudo_c.txt:323742</c>): Path 1 fires when
|
/// (<c>acclient_2013_pseudo_c.txt:323742</c>): Path 1 fires when
|
||||||
/// <c>insert_type == PLACEMENT_INSERT || obstruction_ethereal != 0</c>,
|
/// <c>insert_type == PLACEMENT_INSERT || obstruction_ethereal != 0</c>,
|
||||||
/// routing to <c>sphere_intersects_solid</c> (passable-ethereal) instead
|
/// routing to <c>sphere_intersects_solid</c> (passable-ethereal) instead
|
||||||
|
|
@ -29,8 +30,34 @@ namespace AcDream.Core.Tests.Physics;
|
||||||
/// </para>
|
/// </para>
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Retail divergence register: row AD-7 retired in this commit — the
|
/// Consume site 2: <c>CSphere::intersects_sphere @ 0x00537ae4</c>
|
||||||
/// ETHEREAL-alone shim is replaced by the faithful port.
|
/// (<c>acclient_2013_pseudo_c.txt:321692</c>): The ethereal branch performs
|
||||||
|
/// a proximity check only (no push-back, no COLLIDED return). All code
|
||||||
|
/// paths in the branch are void returns — the player is fully passable.
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Consume site 3: <c>CCylSphere::intersects_sphere @ 0x0053b4a0</c>
|
||||||
|
/// (<c>acclient_2013_pseudo_c.txt:324573</c>): Same pattern — the ethereal
|
||||||
|
/// branch calls <c>collides_with_sphere</c> for an overlap check only, all
|
||||||
|
/// returns are void, no COLLIDED result — the player is fully passable.
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Task 3 follow-up (2026-06-24): the Sphere and Cylinder consume sites
|
||||||
|
/// are now implemented in <c>SphereCollision</c> and <c>CylinderCollision</c>
|
||||||
|
/// in <c>TransitionTypes.cs</c>. The tests below verify:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>Ethereal-alone Cylinder target → passable (no CollisionNormalValid).</item>
|
||||||
|
/// <item>Ethereal-alone Sphere target → passable (no CollisionNormalValid).</item>
|
||||||
|
/// <item>Non-ethereal Cylinder target → still blocks (regression guard).</item>
|
||||||
|
/// <item>Non-ethereal Sphere target → still blocks (regression guard).</item>
|
||||||
|
/// </list>
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Retail divergence register: row AD-7 retired in the original Task 3 commit —
|
||||||
|
/// the ETHEREAL-alone shim is replaced by the faithful port.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ObstructionEtherealTests
|
public class ObstructionEtherealTests
|
||||||
|
|
@ -87,6 +114,114 @@ public class ObstructionEtherealTests
|
||||||
Assert.True(sp.ObstructionEthereal);
|
Assert.True(sp.ObstructionEthereal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Cylinder shape: ethereal-alone → passable ─────────────────────────
|
||||||
|
//
|
||||||
|
// Retail oracle: CCylSphere::intersects_sphere @ 0x0053b4a0 (pc:324573).
|
||||||
|
// When obstruction_ethereal != 0, all code paths return void (no COLLIDED).
|
||||||
|
// acdream mirrors this via `if (sp.ObstructionEthereal) return OK` at the
|
||||||
|
// top of CylinderCollision, before any overlap/slide logic.
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CylinderEtherealAlone_IsPassable()
|
||||||
|
{
|
||||||
|
// A Cylinder-type shadow with state=ETHEREAL_PS (0x4, no 0x10).
|
||||||
|
// Player sphere sweeps directly into it. Expect NO collision response
|
||||||
|
// (CollisionNormalValid must stay false throughout all 20 ticks).
|
||||||
|
// Retail ref: CCylSphere::intersects_sphere pc:324573 — ethereal branch
|
||||||
|
// does collides_with_sphere checks only (void return, never COLLIDED).
|
||||||
|
var engine = BuildEngineWithSingleShadow(
|
||||||
|
collisionType: ShadowCollisionType.Cylinder,
|
||||||
|
objectState: ETHEREAL_PS,
|
||||||
|
objectPos: new Vector3(12f, 12f, 0f),
|
||||||
|
radius: 0.3f,
|
||||||
|
cylHeight: 1.5f);
|
||||||
|
|
||||||
|
// Approach dead-center from -Y, sweeping +Y through the cylinder.
|
||||||
|
var start = new Vector3(12f, 11f, 0.48f);
|
||||||
|
var perTick = new Vector3(0f, 0.10f, 0f);
|
||||||
|
|
||||||
|
var (blocked, finalPos, _) = SweepUntilBlocked(engine, start, perTick, maxTicks: 20);
|
||||||
|
|
||||||
|
Assert.False(blocked,
|
||||||
|
$"Ethereal-alone Cylinder must be passable (no collision). " +
|
||||||
|
$"Sphere stopped at ({finalPos.X:F3},{finalPos.Y:F3},{finalPos.Z:F3}).");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CylinderNonEthereal_StillBlocks()
|
||||||
|
{
|
||||||
|
// Control: same Cylinder, state=0 (no ETHEREAL). Must still block.
|
||||||
|
// This guards the regression that the ethereal gate does NOT affect
|
||||||
|
// normal (non-ethereal) Cylinder objects.
|
||||||
|
var engine = BuildEngineWithSingleShadow(
|
||||||
|
collisionType: ShadowCollisionType.Cylinder,
|
||||||
|
objectState: 0u, // no ethereal bit
|
||||||
|
objectPos: new Vector3(12f, 12f, 0f),
|
||||||
|
radius: 0.3f,
|
||||||
|
cylHeight: 1.5f);
|
||||||
|
|
||||||
|
var start = new Vector3(12f, 11f, 0.48f);
|
||||||
|
var perTick = new Vector3(0f, 0.10f, 0f);
|
||||||
|
|
||||||
|
var (blocked, finalPos, _) = SweepUntilBlocked(engine, start, perTick, maxTicks: 20);
|
||||||
|
|
||||||
|
Assert.True(blocked,
|
||||||
|
$"Non-ethereal Cylinder must still block. " +
|
||||||
|
$"Sphere reached ({finalPos.X:F3},{finalPos.Y:F3},{finalPos.Z:F3}) after 20 ticks.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Sphere shape: ethereal-alone → passable ───────────────────────────
|
||||||
|
//
|
||||||
|
// Retail oracle: CSphere::intersects_sphere @ 0x00537ae4 (pc:321692).
|
||||||
|
// When obstruction_ethereal != 0, all code paths return void (no COLLIDED).
|
||||||
|
// acdream mirrors this via `if (sp.ObstructionEthereal) return OK` at the
|
||||||
|
// top of SphereCollision.
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SphereEtherealAlone_IsPassable()
|
||||||
|
{
|
||||||
|
// A Sphere-type shadow with state=ETHEREAL_PS (0x4, no 0x10).
|
||||||
|
// Player sphere sweeps directly into it. Expect NO collision response.
|
||||||
|
// Retail ref: CSphere::intersects_sphere pc:321692 — ethereal branch
|
||||||
|
// does a proximity check only (void return, never COLLIDED / Slid).
|
||||||
|
var engine = BuildEngineWithSingleShadow(
|
||||||
|
collisionType: ShadowCollisionType.Sphere,
|
||||||
|
objectState: ETHEREAL_PS,
|
||||||
|
objectPos: new Vector3(12f, 12f, 0.48f),
|
||||||
|
radius: 0.4f,
|
||||||
|
cylHeight: 0f);
|
||||||
|
|
||||||
|
var start = new Vector3(12f, 11f, 0.48f);
|
||||||
|
var perTick = new Vector3(0f, 0.10f, 0f);
|
||||||
|
|
||||||
|
var (blocked, finalPos, _) = SweepUntilBlocked(engine, start, perTick, maxTicks: 20);
|
||||||
|
|
||||||
|
Assert.False(blocked,
|
||||||
|
$"Ethereal-alone Sphere must be passable (no collision). " +
|
||||||
|
$"Sphere stopped at ({finalPos.X:F3},{finalPos.Y:F3},{finalPos.Z:F3}).");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SphereNonEthereal_StillBlocks()
|
||||||
|
{
|
||||||
|
// Control: same Sphere, state=0 (no ETHEREAL). Must still block.
|
||||||
|
var engine = BuildEngineWithSingleShadow(
|
||||||
|
collisionType: ShadowCollisionType.Sphere,
|
||||||
|
objectState: 0u, // no ethereal bit
|
||||||
|
objectPos: new Vector3(12f, 12f, 0.48f),
|
||||||
|
radius: 0.4f,
|
||||||
|
cylHeight: 0f);
|
||||||
|
|
||||||
|
var start = new Vector3(12f, 11f, 0.48f);
|
||||||
|
var perTick = new Vector3(0f, 0.10f, 0f);
|
||||||
|
|
||||||
|
var (blocked, finalPos, _) = SweepUntilBlocked(engine, start, perTick, maxTicks: 20);
|
||||||
|
|
||||||
|
Assert.True(blocked,
|
||||||
|
$"Non-ethereal Sphere must still block. " +
|
||||||
|
$"Sphere reached ({finalPos.X:F3},{finalPos.Y:F3},{finalPos.Z:F3}) after 20 ticks.");
|
||||||
|
}
|
||||||
|
|
||||||
// ── BSPQuery Path-1 gate: ObstructionEthereal routes through
|
// ── BSPQuery Path-1 gate: ObstructionEthereal routes through
|
||||||
// sphere_intersects_solid (not the blocking slide path) ──────────────
|
// sphere_intersects_solid (not the blocking slide path) ──────────────
|
||||||
//
|
//
|
||||||
|
|
@ -96,4 +231,99 @@ public class ObstructionEtherealTests
|
||||||
// via the FindEnvCollisions D5 clear and the SpherePath property
|
// via the FindEnvCollisions D5 clear and the SpherePath property
|
||||||
// round-trip. The DoorCollisionApparatusTests and CellarUpTrajectory-
|
// round-trip. The DoorCollisionApparatusTests and CellarUpTrajectory-
|
||||||
// ReplayTests are the regression guards for the consume-site change.
|
// ReplayTests are the regression guards for the consume-site change.
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private const uint TestLandblockId = 0xA9B40000u;
|
||||||
|
private const uint TestCellId = TestLandblockId | 0x0001u;
|
||||||
|
private const uint TestEntityId = 0xF4201u;
|
||||||
|
private const uint TestGfxObjId = 0xF4202u;
|
||||||
|
|
||||||
|
private const float SphereRadius = 0.48f;
|
||||||
|
private const float SphereHeight = 1.20f;
|
||||||
|
private const float StepUpHeight = 0.60f;
|
||||||
|
private const float StepDownHeight = 0.04f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Build a minimal PhysicsEngine with a synthetic flat landblock
|
||||||
|
/// and a single registered shadow entry at <paramref name="objectPos"/>
|
||||||
|
/// with the given collision type and state bits.
|
||||||
|
/// No dat required — Cylinder and Sphere shapes need no GfxObj/BSP.
|
||||||
|
/// </summary>
|
||||||
|
private static PhysicsEngine BuildEngineWithSingleShadow(
|
||||||
|
ShadowCollisionType collisionType,
|
||||||
|
uint objectState,
|
||||||
|
Vector3 objectPos,
|
||||||
|
float radius,
|
||||||
|
float cylHeight)
|
||||||
|
{
|
||||||
|
var cache = new PhysicsDataCache();
|
||||||
|
var engine = new PhysicsEngine { DataCache = cache };
|
||||||
|
|
||||||
|
// Stub landblock: flat terrain far below so it never interferes.
|
||||||
|
var heights = new byte[81];
|
||||||
|
var heightTable = new float[256];
|
||||||
|
for (int i = 0; i < 256; i++) heightTable[i] = -1000f;
|
||||||
|
engine.AddLandblock(
|
||||||
|
landblockId: TestLandblockId,
|
||||||
|
terrain: new TerrainSurface(heights, heightTable),
|
||||||
|
cells: System.Array.Empty<CellSurface>(),
|
||||||
|
portals: System.Array.Empty<PortalPlane>(),
|
||||||
|
worldOffsetX: 0f,
|
||||||
|
worldOffsetY: 0f);
|
||||||
|
|
||||||
|
// Register the target shadow. Cylinder/Sphere shapes don't need a
|
||||||
|
// GfxObj BSP in the cache — FindObjCollisions only queries the cache
|
||||||
|
// for BSP collision types.
|
||||||
|
engine.ShadowObjects.Register(
|
||||||
|
entityId: TestEntityId,
|
||||||
|
gfxObjId: TestGfxObjId,
|
||||||
|
worldPos: objectPos,
|
||||||
|
rotation: System.Numerics.Quaternion.Identity,
|
||||||
|
radius: radius,
|
||||||
|
worldOffsetX: 0f,
|
||||||
|
worldOffsetY: 0f,
|
||||||
|
landblockId: TestLandblockId,
|
||||||
|
collisionType: collisionType,
|
||||||
|
cylHeight: cylHeight,
|
||||||
|
scale: 1.0f,
|
||||||
|
state: objectState,
|
||||||
|
flags: EntityCollisionFlags.None);
|
||||||
|
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sweep a player sphere by <paramref name="perTick"/> each step until
|
||||||
|
/// <c>CollisionNormalValid</c> fires or <paramref name="maxTicks"/> elapse.
|
||||||
|
/// </summary>
|
||||||
|
private static (bool blocked, Vector3 finalPos, Vector3 normal)
|
||||||
|
SweepUntilBlocked(PhysicsEngine engine, Vector3 start, Vector3 perTick, int maxTicks)
|
||||||
|
{
|
||||||
|
Vector3 pos = start;
|
||||||
|
uint cellId = TestCellId;
|
||||||
|
bool onGround = false;
|
||||||
|
|
||||||
|
for (int tick = 0; tick < maxTicks; tick++)
|
||||||
|
{
|
||||||
|
Vector3 target = pos + perTick;
|
||||||
|
var result = engine.ResolveWithTransition(
|
||||||
|
pos, target, cellId,
|
||||||
|
SphereRadius, SphereHeight,
|
||||||
|
StepUpHeight, StepDownHeight,
|
||||||
|
onGround,
|
||||||
|
body: null,
|
||||||
|
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
|
||||||
|
movingEntityId: 0);
|
||||||
|
|
||||||
|
if (result.CollisionNormalValid)
|
||||||
|
return (true, result.Position, result.CollisionNormal);
|
||||||
|
|
||||||
|
pos = result.Position;
|
||||||
|
cellId = result.CellId;
|
||||||
|
onGround = result.IsOnGround;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (false, pos, Vector3.Zero);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue