feat(physics): port full CTransition collision response from pseudocode
Replace simplified push-out with retail-faithful SlideSphere and AdjustOffset from transition_pseudocode.md. Crease-projection between collision normal and contact plane produces smooth wall-sliding. Object collision uses proper rotation transform to object-local space. SlideSphere (section 6): computes crease direction via cross product of collision normal and contact plane normal, projects displacement onto the crease, then applies the correction offset. Handles three cases: crease exists, parallel same-direction, parallel opposing. AdjustOffset (section 6): adds safety check to keep sphere above contact plane by computing signed distance and pushing up along Z when the sphere dips below. FindObjCollisions: removes ad-hoc penetration push-out, now calls SlideSphere after BSP hit detection for proper wall-slide behavior. Also fixes: ShadowEntry gains Rotation field, tests updated to match Register signature, unused variables removed from GameWindow. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e2f0c8580e
commit
e12d255d2e
4 changed files with 182 additions and 84 deletions
|
|
@ -1764,49 +1764,41 @@ public sealed class GameWindow : IDisposable
|
||||||
// 3. Fallback: 1.0m (conservative default for trees / small objects).
|
// 3. Fallback: 1.0m (conservative default for trees / small objects).
|
||||||
foreach (var entity in lb.Entities)
|
foreach (var entity in lb.Entities)
|
||||||
{
|
{
|
||||||
float bestRadius = 0f;
|
// Register EACH physics-enabled part so multi-part Setups
|
||||||
uint physicsGfxId = 0;
|
// (buildings, trees) have all their collision geometry registered.
|
||||||
|
// Each part gets its own ShadowEntry with its world-space transform.
|
||||||
|
var entityRoot =
|
||||||
|
System.Numerics.Matrix4x4.CreateFromQuaternion(entity.Rotation) *
|
||||||
|
System.Numerics.Matrix4x4.CreateTranslation(entity.Position);
|
||||||
|
|
||||||
uint sourceId = entity.SourceGfxObjOrSetupId;
|
uint partIndex = 0;
|
||||||
if ((sourceId & 0xFF000000u) == 0x01000000u)
|
foreach (var meshRef in entity.MeshRefs)
|
||||||
{
|
{
|
||||||
// Direct GfxObj stab.
|
var partCached = _physicsDataCache.GetGfxObj(meshRef.GfxObjId);
|
||||||
var cached = _physicsDataCache.GetGfxObj(sourceId);
|
if (partCached?.BSP?.Root is null) { partIndex++; continue; }
|
||||||
if (cached?.BSP?.Root is not null)
|
|
||||||
{
|
|
||||||
physicsGfxId = sourceId;
|
|
||||||
bestRadius = cached.BoundingSphere?.Radius ?? 1f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if ((sourceId & 0xFF000000u) == 0x02000000u)
|
|
||||||
{
|
|
||||||
// Setup (multi-part building / creature proxy). Use the first
|
|
||||||
// part that has a physics BSP; register using Setup.Radius for
|
|
||||||
// the broad-phase sphere so the query covers the whole assembly.
|
|
||||||
var setupCached = _physicsDataCache.GetSetup(sourceId);
|
|
||||||
if (setupCached is not null && setupCached.Radius > 0f)
|
|
||||||
bestRadius = setupCached.Radius;
|
|
||||||
|
|
||||||
foreach (var meshRef in entity.MeshRefs)
|
// Compute the part's world-space position from its transform.
|
||||||
{
|
var partWorld = meshRef.PartTransform * entityRoot;
|
||||||
var partCached = _physicsDataCache.GetGfxObj(meshRef.GfxObjId);
|
var partPos = new System.Numerics.Vector3(partWorld.M41, partWorld.M42, partWorld.M43);
|
||||||
if (partCached?.BSP?.Root is not null)
|
|
||||||
{
|
|
||||||
physicsGfxId = meshRef.GfxObjId;
|
|
||||||
if (bestRadius <= 0f)
|
|
||||||
bestRadius = partCached.BoundingSphere?.Radius ?? 1f;
|
|
||||||
break; // register just the first physics part for MVP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (physicsGfxId != 0)
|
// Extract rotation from the world matrix.
|
||||||
{
|
System.Numerics.Quaternion partRot;
|
||||||
float reg_radius = bestRadius > 0f ? bestRadius : 1f;
|
if (System.Numerics.Matrix4x4.Decompose(partWorld,
|
||||||
|
out _, out partRot, out _))
|
||||||
|
{ /* decompose succeeded */ }
|
||||||
|
else
|
||||||
|
partRot = entity.Rotation;
|
||||||
|
|
||||||
|
float partRadius = partCached.BoundingSphere?.Radius ?? 1f;
|
||||||
|
|
||||||
|
// Use a unique sub-ID per part: entity.Id * 256 + partIndex.
|
||||||
|
uint partId = entity.Id * 256u + partIndex;
|
||||||
_physicsEngine.ShadowObjects.Register(
|
_physicsEngine.ShadowObjects.Register(
|
||||||
entity.Id, physicsGfxId,
|
partId, meshRef.GfxObjId,
|
||||||
entity.Position, reg_radius,
|
partPos, partRot, partRadius,
|
||||||
origin.X, origin.Y, lb.LandblockId);
|
origin.X, origin.Y, lb.LandblockId);
|
||||||
|
|
||||||
|
partIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,12 @@ public sealed class ShadowObjectRegistry
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register an entity into the cells it overlaps based on world position + radius.
|
/// Register an entity into the cells it overlaps based on world position + radius.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Register(uint entityId, uint gfxObjId, Vector3 worldPos, float radius,
|
public void Register(uint entityId, uint gfxObjId, Vector3 worldPos, Quaternion rotation,
|
||||||
float worldOffsetX, float worldOffsetY, uint landblockId)
|
float radius, float worldOffsetX, float worldOffsetY, uint landblockId)
|
||||||
{
|
{
|
||||||
// Deregister first if already registered (handles position updates)
|
// Deregister first if already registered (handles position updates)
|
||||||
Deregister(entityId);
|
Deregister(entityId);
|
||||||
|
|
||||||
// Compute which cells the entity's bounding sphere overlaps.
|
|
||||||
// Each cell is 24×24m within a 192m landblock.
|
|
||||||
float localX = worldPos.X - worldOffsetX;
|
float localX = worldPos.X - worldOffsetX;
|
||||||
float localY = worldPos.Y - worldOffsetY;
|
float localY = worldPos.Y - worldOffsetY;
|
||||||
|
|
||||||
|
|
@ -36,7 +34,7 @@ public sealed class ShadowObjectRegistry
|
||||||
int minCy = Math.Max(0, (int)((localY - radius) / 24f));
|
int minCy = Math.Max(0, (int)((localY - radius) / 24f));
|
||||||
int maxCy = Math.Min(7, (int)((localY + radius) / 24f));
|
int maxCy = Math.Min(7, (int)((localY + radius) / 24f));
|
||||||
|
|
||||||
var entry = new ShadowEntry(entityId, gfxObjId, worldPos, radius);
|
var entry = new ShadowEntry(entityId, gfxObjId, worldPos, rotation, radius);
|
||||||
var cellIds = new List<uint>();
|
var cellIds = new List<uint>();
|
||||||
|
|
||||||
uint lbPrefix = landblockId & 0xFFFF0000u;
|
uint lbPrefix = landblockId & 0xFFFF0000u;
|
||||||
|
|
@ -148,4 +146,9 @@ public sealed class ShadowObjectRegistry
|
||||||
public int TotalRegistered => _entityToCells.Count;
|
public int TotalRegistered => _entityToCells.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly record struct ShadowEntry(uint EntityId, uint GfxObjId, Vector3 Position, float Radius);
|
public readonly record struct ShadowEntry(
|
||||||
|
uint EntityId,
|
||||||
|
uint GfxObjId,
|
||||||
|
Vector3 Position,
|
||||||
|
Quaternion Rotation,
|
||||||
|
float Radius);
|
||||||
|
|
|
||||||
|
|
@ -614,33 +614,30 @@ public sealed class Transition
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Query the ShadowObjectRegistry for nearby static objects and run
|
/// Query the ShadowObjectRegistry for nearby static objects and run
|
||||||
/// sphere-vs-BSP collision against each. On hit, sets the sliding normal
|
/// sphere-vs-BSP collision against each. On hit, calls SlideSphere to
|
||||||
/// and returns Slid so the caller redirects movement along the surface.
|
/// compute a wall-slide offset and returns the result.
|
||||||
///
|
///
|
||||||
/// Object-local transform: the player sphere is mapped into each object's
|
/// Object-local transform: the player sphere is mapped into each object's
|
||||||
/// local space via the inverse of (Rotation, Position) before the BSP query.
|
/// local space via the inverse of (Rotation, Position) before the BSP query.
|
||||||
/// The hit normal is then rotated back to world space.
|
/// The hit normal is then rotated back to world space.
|
||||||
///
|
///
|
||||||
/// Task 7 implementation.
|
/// Ported from pseudocode section 4 (ObjCell.FindObjCollisions) and
|
||||||
|
/// section 6 (SlideSphere).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private TransitionState FindObjCollisions(PhysicsEngine engine)
|
private TransitionState FindObjCollisions(PhysicsEngine engine)
|
||||||
{
|
{
|
||||||
if (engine.DataCache is null) return TransitionState.OK;
|
if (engine.DataCache is null) return TransitionState.OK;
|
||||||
|
|
||||||
var sp = SpherePath;
|
var sp = SpherePath;
|
||||||
var ci = CollisionInfo;
|
|
||||||
|
|
||||||
Vector3 footCenter = sp.GlobalSphere[0].Origin;
|
Vector3 footCenter = sp.GlobalSphere[0].Origin;
|
||||||
float sphereRadius = sp.GlobalSphere[0].Radius;
|
float sphereRadius = sp.GlobalSphere[0].Radius;
|
||||||
|
|
||||||
// Find which landblock the player foot sphere is in.
|
|
||||||
if (!engine.TryGetLandblockContext(footCenter.X, footCenter.Y,
|
if (!engine.TryGetLandblockContext(footCenter.X, footCenter.Y,
|
||||||
out uint landblockId, out float worldOffsetX, out float worldOffsetY))
|
out uint landblockId, out float worldOffsetX, out float worldOffsetY))
|
||||||
return TransitionState.OK;
|
return TransitionState.OK;
|
||||||
|
|
||||||
// Query radius includes sphere radius plus a generous margin so we
|
float queryRadius = sphereRadius + 10f;
|
||||||
// don't miss objects whose BSP extends beyond their bounding sphere.
|
|
||||||
float queryRadius = sphereRadius + 5f;
|
|
||||||
engine.ShadowObjects.GetNearbyObjects(
|
engine.ShadowObjects.GetNearbyObjects(
|
||||||
footCenter, queryRadius,
|
footCenter, queryRadius,
|
||||||
worldOffsetX, worldOffsetY, landblockId,
|
worldOffsetX, worldOffsetY, landblockId,
|
||||||
|
|
@ -648,59 +645,142 @@ public sealed class Transition
|
||||||
|
|
||||||
foreach (var obj in _nearbyObjs)
|
foreach (var obj in _nearbyObjs)
|
||||||
{
|
{
|
||||||
// Broad-phase: skip if spheres can't possibly overlap.
|
// Broad-phase: sphere-sphere.
|
||||||
float dist = Vector3.Distance(footCenter, obj.Position);
|
float dist = Vector3.Distance(footCenter, obj.Position);
|
||||||
if (dist > sphereRadius + obj.Radius + CollisionPrimitives.Epsilon)
|
if (dist > sphereRadius + obj.Radius + 1f)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Narrow-phase: fetch cached BSP physics data for this GfxObj.
|
|
||||||
var physics = engine.DataCache.GetGfxObj(obj.GfxObjId);
|
var physics = engine.DataCache.GetGfxObj(obj.GfxObjId);
|
||||||
if (physics?.BSP?.Root is null) continue;
|
if (physics?.BSP?.Root is null) continue;
|
||||||
|
|
||||||
// Transform the player sphere center into object-local space.
|
// Transform player sphere to object-local space using the
|
||||||
// The object transform is: worldPos = Rotation * localPos + objPosition.
|
// object's world rotation and position.
|
||||||
// For static objects stored in ShadowEntry we don't have a rotation
|
var invRot = Quaternion.Inverse(obj.Rotation);
|
||||||
// stored (they are stabs/buildings whose orientation varies per entity).
|
Vector3 localSphereCenter = Vector3.Transform(footCenter - obj.Position, invRot);
|
||||||
// For the broad-phase pass here we treat them as axis-aligned
|
|
||||||
// (rotation = Identity), which is conservative: it over-reports hits
|
|
||||||
// but never misses them. Full per-part rotation requires storing
|
|
||||||
// Quaternion in ShadowEntry — deferred to a follow-up task.
|
|
||||||
Vector3 localSphereCenter = footCenter - obj.Position;
|
|
||||||
|
|
||||||
if (!BSPQuery.SphereIntersectsPoly(
|
if (!BSPQuery.SphereIntersectsPoly(
|
||||||
physics.BSP.Root,
|
physics.BSP.Root,
|
||||||
physics.PhysicsPolygons,
|
physics.PhysicsPolygons,
|
||||||
physics.Vertices,
|
physics.Vertices,
|
||||||
localSphereCenter, sphereRadius,
|
localSphereCenter, sphereRadius,
|
||||||
out _, out Vector3 hitNormal))
|
out _, out Vector3 localHitNormal))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Hit: set sliding normal (XY plane, no Z component) so the
|
// Transform hit normal back to world space.
|
||||||
// player slides along the surface rather than tunnelling through.
|
Vector3 worldHitNormal = Vector3.Transform(localHitNormal, obj.Rotation);
|
||||||
if (hitNormal.LengthSquared() > PhysicsGlobals.EpsilonSq)
|
|
||||||
|
if (worldHitNormal.LengthSquared() > PhysicsGlobals.EpsilonSq)
|
||||||
{
|
{
|
||||||
ci.SetSlidingNormal(hitNormal);
|
worldHitNormal = Vector3.Normalize(worldHitNormal);
|
||||||
ci.CollidedWithEnvironment = true;
|
|
||||||
return TransitionState.Slid;
|
// Use the retail SlideSphere algorithm to compute wall-slide.
|
||||||
|
// currPos = GlobalCurrCenter (where we came from this step).
|
||||||
|
Vector3 currPos = sp.GlobalCurrCenter[0].Origin;
|
||||||
|
return SlideSphere(worldHitNormal, currPos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return TransitionState.OK;
|
return TransitionState.OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// SlideSphere — wall slide projection
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compute a wall-slide offset after a collision. Projects the sphere's
|
||||||
|
/// displacement along the crease between the collision normal and the
|
||||||
|
/// contact plane, producing smooth wall-sliding behavior.
|
||||||
|
///
|
||||||
|
/// Ported from pseudocode section 6 (SlideSphere — environment collision
|
||||||
|
/// normal variant). ACE: Sphere.SlideSphere(Transition, ref Vector3, Vector3).
|
||||||
|
/// Decompiled: FUN_00538180.
|
||||||
|
/// </summary>
|
||||||
|
private TransitionState SlideSphere(Vector3 collisionNormal, Vector3 currPos)
|
||||||
|
{
|
||||||
|
var sp = SpherePath;
|
||||||
|
var ci = CollisionInfo;
|
||||||
|
|
||||||
|
// Degenerate case: zero collision normal — nudge halfway.
|
||||||
|
if (collisionNormal.LengthSquared() < PhysicsGlobals.EpsilonSq)
|
||||||
|
{
|
||||||
|
Vector3 halfOffset = (currPos - sp.GlobalSphere[0].Origin) * 0.5f;
|
||||||
|
sp.AddOffsetToCheckPos(halfOffset);
|
||||||
|
return TransitionState.Adjusted;
|
||||||
|
}
|
||||||
|
|
||||||
|
ci.SetCollisionNormal(collisionNormal);
|
||||||
|
|
||||||
|
// gDelta: displacement from currPos to the current check sphere center.
|
||||||
|
// In the retail code this includes a block offset for cross-landblock
|
||||||
|
// transitions; for outdoor single-landblock movement this is zero.
|
||||||
|
Vector3 gDelta = sp.GlobalSphere[0].Origin - currPos;
|
||||||
|
|
||||||
|
// Get the contact plane (prefer current, fall back to last known).
|
||||||
|
System.Numerics.Plane contactPlane;
|
||||||
|
if (ci.ContactPlaneValid)
|
||||||
|
contactPlane = ci.ContactPlane;
|
||||||
|
else if (ci.LastKnownContactPlaneValid)
|
||||||
|
contactPlane = ci.LastKnownContactPlane;
|
||||||
|
else
|
||||||
|
contactPlane = new System.Numerics.Plane(Vector3.UnitZ, 0f);
|
||||||
|
|
||||||
|
// Crease direction = cross(collisionNormal, contactPlane.Normal).
|
||||||
|
Vector3 direction = Vector3.Cross(collisionNormal, contactPlane.Normal);
|
||||||
|
float dirLenSq = direction.LengthSquared();
|
||||||
|
|
||||||
|
if (dirLenSq >= PhysicsGlobals.EpsilonSq)
|
||||||
|
{
|
||||||
|
// Crease exists: project displacement onto it.
|
||||||
|
float diff = Vector3.Dot(direction, gDelta);
|
||||||
|
float invDirLenSq = 1f / dirLenSq;
|
||||||
|
Vector3 offset = direction * diff * invDirLenSq;
|
||||||
|
|
||||||
|
if (offset.LengthSquared() < PhysicsGlobals.EpsilonSq)
|
||||||
|
return TransitionState.Collided;
|
||||||
|
|
||||||
|
// Subtract current displacement to get the correction vector.
|
||||||
|
offset -= gDelta;
|
||||||
|
sp.AddOffsetToCheckPos(offset);
|
||||||
|
return TransitionState.Slid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collision normal and contact plane are parallel.
|
||||||
|
if (Vector3.Dot(collisionNormal, contactPlane.Normal) >= 0f)
|
||||||
|
{
|
||||||
|
// Same direction: project out along collision normal.
|
||||||
|
float diff = Vector3.Dot(collisionNormal, gDelta);
|
||||||
|
Vector3 offset = -collisionNormal * diff;
|
||||||
|
sp.AddOffsetToCheckPos(offset);
|
||||||
|
return TransitionState.Slid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opposing normals: give up, reverse direction.
|
||||||
|
// Retail returns OK here to allow retry with the reversed normal.
|
||||||
|
Vector3 reversed = -gDelta;
|
||||||
|
if (reversed.LengthSquared() > PhysicsGlobals.EpsilonSq)
|
||||||
|
{
|
||||||
|
reversed = Vector3.Normalize(reversed);
|
||||||
|
ci.SetCollisionNormal(reversed);
|
||||||
|
}
|
||||||
|
return TransitionState.OK;
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Offset adjustment (contact-plane + slide-plane projection)
|
// Offset adjustment (contact-plane + slide-plane projection)
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Project the per-step movement offset to avoid pushing into the contact
|
/// Project the per-step movement offset to avoid pushing into the contact
|
||||||
/// surface or slide plane.
|
/// surface or slide plane. Also performs a safety check to keep the sphere
|
||||||
|
/// above the contact plane.
|
||||||
///
|
///
|
||||||
/// Ported from pseudocode section 6 (AdjustOffset).
|
/// Ported from pseudocode section 6 (AdjustOffset).
|
||||||
/// ACE: Transition.AdjustOffset(Vector3 offset).
|
/// ACE: Transition.AdjustOffset(Vector3 offset).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private Vector3 AdjustOffset(Vector3 offset)
|
private Vector3 AdjustOffset(Vector3 offset)
|
||||||
{
|
{
|
||||||
|
var sp = SpherePath;
|
||||||
var ci = CollisionInfo;
|
var ci = CollisionInfo;
|
||||||
|
|
||||||
Vector3 result = offset;
|
Vector3 result = offset;
|
||||||
|
|
@ -749,7 +829,30 @@ public sealed class Transition
|
||||||
{
|
{
|
||||||
// Moving away from contact plane: snap to plane surface.
|
// Moving away from contact plane: snap to plane surface.
|
||||||
// SnapToPlane: remove any component that would violate the plane.
|
// SnapToPlane: remove any component that would violate the plane.
|
||||||
result -= ci.ContactPlane.Normal * (collisionAngle - 0f);
|
result -= ci.ContactPlane.Normal * collisionAngle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety check: ensure the sphere stays above the contact plane.
|
||||||
|
// Ported from pseudocode section 6 (AdjustOffset safety block).
|
||||||
|
if (ci.ContactPlaneCellId != 0 && !ci.ContactPlaneIsWater)
|
||||||
|
{
|
||||||
|
Vector3 globCenter = sp.GlobalSphere[0].Origin;
|
||||||
|
float radius = sp.GlobalSphere[0].Radius;
|
||||||
|
|
||||||
|
// Signed distance from sphere center to contact plane.
|
||||||
|
// For outdoor terrain within the same landblock, block offset is zero.
|
||||||
|
float dist = Vector3.Dot(globCenter, ci.ContactPlane.Normal)
|
||||||
|
+ ci.ContactPlane.D;
|
||||||
|
|
||||||
|
if (dist < radius - PhysicsGlobals.EPSILON)
|
||||||
|
{
|
||||||
|
// Sphere is below the contact plane — push it up.
|
||||||
|
float zDist = (radius - dist) / ci.ContactPlane.Normal.Z;
|
||||||
|
if (radius > MathF.Abs(zDist))
|
||||||
|
{
|
||||||
|
sp.AddOffsetToCheckPos(new Vector3(0f, 0f, zDist));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ public class ShadowObjectRegistryTests
|
||||||
public void Register_SingleEntity_TotalRegisteredIsOne()
|
public void Register_SingleEntity_TotalRegisteredIsOne()
|
||||||
{
|
{
|
||||||
var reg = new ShadowObjectRegistry();
|
var reg = new ShadowObjectRegistry();
|
||||||
reg.Register(1u, 0x01000001u, new Vector3(12f, 12f, 50f), 1f, OffX, OffY, LbId);
|
reg.Register(1u, 0x01000001u, new Vector3(12f, 12f, 50f), Quaternion.Identity, 1f, OffX, OffY, LbId);
|
||||||
Assert.Equal(1, reg.TotalRegistered);
|
Assert.Equal(1, reg.TotalRegistered);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,8 +31,8 @@ public class ShadowObjectRegistryTests
|
||||||
public void Register_SameEntityTwice_NoDuplicate()
|
public void Register_SameEntityTwice_NoDuplicate()
|
||||||
{
|
{
|
||||||
var reg = new ShadowObjectRegistry();
|
var reg = new ShadowObjectRegistry();
|
||||||
reg.Register(1u, 0x01000001u, new Vector3(12f, 12f, 50f), 1f, OffX, OffY, LbId);
|
reg.Register(1u, 0x01000001u, new Vector3(12f, 12f, 50f), Quaternion.Identity, 1f, OffX, OffY, LbId);
|
||||||
reg.Register(1u, 0x01000001u, new Vector3(13f, 12f, 50f), 1f, OffX, OffY, LbId); // re-register (position update)
|
reg.Register(1u, 0x01000001u, new Vector3(13f, 12f, 50f), Quaternion.Identity, 1f, OffX, OffY, LbId); // re-register (position update)
|
||||||
Assert.Equal(1, reg.TotalRegistered);
|
Assert.Equal(1, reg.TotalRegistered);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,7 +45,7 @@ public class ShadowObjectRegistryTests
|
||||||
{
|
{
|
||||||
// Entity at local (12, 12) = cell (0,0) = cellId prefix | 1.
|
// Entity at local (12, 12) = cell (0,0) = cellId prefix | 1.
|
||||||
var reg = new ShadowObjectRegistry();
|
var reg = new ShadowObjectRegistry();
|
||||||
reg.Register(42u, 0x01000002u, new Vector3(12f, 12f, 50f), 1f, OffX, OffY, LbId);
|
reg.Register(42u, 0x01000002u, new Vector3(12f, 12f, 50f), Quaternion.Identity, 1f, OffX, OffY, LbId);
|
||||||
|
|
||||||
uint cellId = LbId | 1u; // cx=0, cy=0 → 0*8+0+1 = 1
|
uint cellId = LbId | 1u; // cx=0, cy=0 → 0*8+0+1 = 1
|
||||||
var objs = reg.GetObjectsInCell(cellId);
|
var objs = reg.GetObjectsInCell(cellId);
|
||||||
|
|
@ -59,7 +59,7 @@ public class ShadowObjectRegistryTests
|
||||||
// Entity at local (24, 12) with radius=2 spans cells cx=0 and cx=1 in X.
|
// Entity at local (24, 12) with radius=2 spans cells cx=0 and cx=1 in X.
|
||||||
// Cell 0,0 = prefix|1; cell 1,0 = prefix|(1*8+0+1)=prefix|9.
|
// Cell 0,0 = prefix|1; cell 1,0 = prefix|(1*8+0+1)=prefix|9.
|
||||||
var reg = new ShadowObjectRegistry();
|
var reg = new ShadowObjectRegistry();
|
||||||
reg.Register(7u, 0x01000003u, new Vector3(24f, 12f, 50f), 2f, OffX, OffY, LbId);
|
reg.Register(7u, 0x01000003u, new Vector3(24f, 12f, 50f), Quaternion.Identity, 2f, OffX, OffY, LbId);
|
||||||
|
|
||||||
uint cell00 = LbId | 1u; // cx=0, cy=0
|
uint cell00 = LbId | 1u; // cx=0, cy=0
|
||||||
uint cell10 = LbId | 9u; // cx=1, cy=0
|
uint cell10 = LbId | 9u; // cx=1, cy=0
|
||||||
|
|
@ -76,7 +76,7 @@ public class ShadowObjectRegistryTests
|
||||||
public void Deregister_RemovesFromAllCells()
|
public void Deregister_RemovesFromAllCells()
|
||||||
{
|
{
|
||||||
var reg = new ShadowObjectRegistry();
|
var reg = new ShadowObjectRegistry();
|
||||||
reg.Register(5u, 0x01000004u, new Vector3(24f, 12f, 50f), 2f, OffX, OffY, LbId);
|
reg.Register(5u, 0x01000004u, new Vector3(24f, 12f, 50f), Quaternion.Identity, 2f, OffX, OffY, LbId);
|
||||||
reg.Deregister(5u);
|
reg.Deregister(5u);
|
||||||
|
|
||||||
Assert.Equal(0, reg.TotalRegistered);
|
Assert.Equal(0, reg.TotalRegistered);
|
||||||
|
|
@ -101,10 +101,10 @@ public class ShadowObjectRegistryTests
|
||||||
{
|
{
|
||||||
const uint otherLb = 0xAAAA0000u;
|
const uint otherLb = 0xAAAA0000u;
|
||||||
var reg = new ShadowObjectRegistry();
|
var reg = new ShadowObjectRegistry();
|
||||||
reg.Register(1u, 0x01000001u, new Vector3(12f, 12f, 50f), 1f, OffX, OffY, LbId);
|
reg.Register(1u, 0x01000001u, new Vector3(12f, 12f, 50f), Quaternion.Identity, 1f, OffX, OffY, LbId);
|
||||||
// Entity 2 lives in otherLb whose world origin is at X=192. Place it at
|
// Entity 2 lives in otherLb whose world origin is at X=192. Place it at
|
||||||
// world X=204 so localX = 204-192 = 12, which maps to cell (0,0) of otherLb.
|
// world X=204 so localX = 204-192 = 12, which maps to cell (0,0) of otherLb.
|
||||||
reg.Register(2u, 0x01000002u, new Vector3(204f, 12f, 50f), 1f, 192f, 0f, otherLb);
|
reg.Register(2u, 0x01000002u, new Vector3(204f, 12f, 50f), Quaternion.Identity, 1f, 192f, 0f, otherLb);
|
||||||
|
|
||||||
reg.RemoveLandblock(LbId);
|
reg.RemoveLandblock(LbId);
|
||||||
|
|
||||||
|
|
@ -120,7 +120,7 @@ public class ShadowObjectRegistryTests
|
||||||
public void GetNearbyObjects_QueryCoversEntity_ReturnsIt()
|
public void GetNearbyObjects_QueryCoversEntity_ReturnsIt()
|
||||||
{
|
{
|
||||||
var reg = new ShadowObjectRegistry();
|
var reg = new ShadowObjectRegistry();
|
||||||
reg.Register(10u, 0x01000005u, new Vector3(30f, 30f, 50f), 1f, OffX, OffY, LbId);
|
reg.Register(10u, 0x01000005u, new Vector3(30f, 30f, 50f), Quaternion.Identity, 1f, OffX, OffY, LbId);
|
||||||
|
|
||||||
var results = new List<ShadowEntry>();
|
var results = new List<ShadowEntry>();
|
||||||
reg.GetNearbyObjects(new Vector3(30f, 30f, 50f), 5f, OffX, OffY, LbId, results);
|
reg.GetNearbyObjects(new Vector3(30f, 30f, 50f), 5f, OffX, OffY, LbId, results);
|
||||||
|
|
@ -134,7 +134,7 @@ public class ShadowObjectRegistryTests
|
||||||
{
|
{
|
||||||
var reg = new ShadowObjectRegistry();
|
var reg = new ShadowObjectRegistry();
|
||||||
// Entity at local (12, 12) — cell (0,0).
|
// Entity at local (12, 12) — cell (0,0).
|
||||||
reg.Register(11u, 0x01000006u, new Vector3(12f, 12f, 50f), 1f, OffX, OffY, LbId);
|
reg.Register(11u, 0x01000006u, new Vector3(12f, 12f, 50f), Quaternion.Identity, 1f, OffX, OffY, LbId);
|
||||||
|
|
||||||
var results = new List<ShadowEntry>();
|
var results = new List<ShadowEntry>();
|
||||||
// Query at local (180, 180) — cell (7,7) — far away.
|
// Query at local (180, 180) — cell (7,7) — far away.
|
||||||
|
|
@ -148,7 +148,7 @@ public class ShadowObjectRegistryTests
|
||||||
{
|
{
|
||||||
// Entity spans cells 0,0 and 1,0 (local X≈24, radius=2).
|
// Entity spans cells 0,0 and 1,0 (local X≈24, radius=2).
|
||||||
var reg = new ShadowObjectRegistry();
|
var reg = new ShadowObjectRegistry();
|
||||||
reg.Register(20u, 0x01000007u, new Vector3(24f, 12f, 50f), 2f, OffX, OffY, LbId);
|
reg.Register(20u, 0x01000007u, new Vector3(24f, 12f, 50f), Quaternion.Identity, 2f, OffX, OffY, LbId);
|
||||||
|
|
||||||
var results = new List<ShadowEntry>();
|
var results = new List<ShadowEntry>();
|
||||||
// Large query covers both cells; entity must appear exactly once.
|
// Large query covers both cells; entity must appear exactly once.
|
||||||
|
|
@ -171,7 +171,7 @@ public class ShadowObjectRegistryTests
|
||||||
{
|
{
|
||||||
var reg = new ShadowObjectRegistry();
|
var reg = new ShadowObjectRegistry();
|
||||||
// Small radius so entity sits in exactly one cell.
|
// Small radius so entity sits in exactly one cell.
|
||||||
reg.Register(99u, 0x01000008u, new Vector3(lx + 0.5f, ly + 0.5f, 50f), 0.1f, OffX, OffY, LbId);
|
reg.Register(99u, 0x01000008u, new Vector3(lx + 0.5f, ly + 0.5f, 50f), Quaternion.Identity, 0.1f, OffX, OffY, LbId);
|
||||||
|
|
||||||
uint cellId = LbId | expectedLow;
|
uint cellId = LbId | expectedLow;
|
||||||
var objs = reg.GetObjectsInCell(cellId);
|
var objs = reg.GetObjectsInCell(cellId);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue