using System.Collections.Generic;
using System.Numerics;
using AcDream.Core.Physics;
using Xunit;
namespace AcDream.Core.Tests.Physics;
///
/// Unit tests for — Task 7 cell-based
/// spatial index for object collision.
///
public class ShadowObjectRegistryTests
{
private const uint LbId = 0xA9B40000u; // landblock prefix used throughout
private const float OffX = 0f;
private const float OffY = 0f;
// -----------------------------------------------------------------------
// Register / TotalRegistered
// -----------------------------------------------------------------------
[Fact]
public void Register_SingleEntity_TotalRegisteredIsOne()
{
var reg = new ShadowObjectRegistry();
reg.Register(1u, 0x01000001u, new Vector3(12f, 12f, 50f), Quaternion.Identity, 1f, OffX, OffY, LbId);
Assert.Equal(1, reg.TotalRegistered);
}
[Fact]
public void Register_SameEntityTwice_NoDuplicate()
{
var reg = new ShadowObjectRegistry();
reg.Register(1u, 0x01000001u, new Vector3(12f, 12f, 50f), Quaternion.Identity, 1f, OffX, OffY, LbId);
reg.Register(1u, 0x01000001u, new Vector3(13f, 12f, 50f), Quaternion.Identity, 1f, OffX, OffY, LbId); // re-register (position update)
Assert.Equal(1, reg.TotalRegistered);
}
// -----------------------------------------------------------------------
// GetObjectsInCell
// -----------------------------------------------------------------------
[Fact]
public void GetObjectsInCell_EntityInCenter_ReturnedInExpectedCell()
{
// Entity at local (12, 12) = cell (0,0) = cellId prefix | 1.
var reg = new ShadowObjectRegistry();
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
var objs = reg.GetObjectsInCell(cellId);
Assert.Single(objs);
Assert.Equal(42u, objs[0].EntityId);
}
[Fact]
public void GetObjectsInCell_EntitySpanning2Cells_RegisteredInBoth()
{
// 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.
var reg = new ShadowObjectRegistry();
reg.Register(7u, 0x01000003u, new Vector3(24f, 12f, 50f), Quaternion.Identity, 2f, OffX, OffY, LbId);
uint cell00 = LbId | 1u; // cx=0, cy=0
uint cell10 = LbId | 9u; // cx=1, cy=0
Assert.Contains(reg.GetObjectsInCell(cell00), e => e.EntityId == 7u);
Assert.Contains(reg.GetObjectsInCell(cell10), e => e.EntityId == 7u);
}
// -----------------------------------------------------------------------
// Deregister
// -----------------------------------------------------------------------
[Fact]
public void Deregister_RemovesFromAllCells()
{
var reg = new ShadowObjectRegistry();
reg.Register(5u, 0x01000004u, new Vector3(24f, 12f, 50f), Quaternion.Identity, 2f, OffX, OffY, LbId);
reg.Deregister(5u);
Assert.Equal(0, reg.TotalRegistered);
Assert.Empty(reg.GetObjectsInCell(LbId | 1u));
Assert.Empty(reg.GetObjectsInCell(LbId | 9u));
}
[Fact]
public void Deregister_NonexistentEntity_NoThrow()
{
var reg = new ShadowObjectRegistry();
// Should not throw.
reg.Deregister(999u);
}
// -----------------------------------------------------------------------
// RemoveLandblock
// -----------------------------------------------------------------------
[Fact]
public void RemoveLandblock_ClearsAllEntitiesForThatBlock()
{
const uint otherLb = 0xAAAA0000u;
var reg = new ShadowObjectRegistry();
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
// 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), Quaternion.Identity, 1f, 192f, 0f, otherLb);
reg.RemoveLandblock(LbId);
Assert.Equal(1, reg.TotalRegistered); // entity 2 (otherLb) survives
Assert.Empty(reg.GetObjectsInCell(LbId | 1u));
}
// -----------------------------------------------------------------------
// GetNearbyObjects
// -----------------------------------------------------------------------
[Fact]
public void GetNearbyObjects_QueryCoversEntity_ReturnsIt()
{
var reg = new ShadowObjectRegistry();
reg.Register(10u, 0x01000005u, new Vector3(30f, 30f, 50f), Quaternion.Identity, 1f, OffX, OffY, LbId);
var results = new List();
reg.GetNearbyObjects(new Vector3(30f, 30f, 50f), 5f, OffX, OffY, LbId, results);
Assert.Single(results);
Assert.Equal(10u, results[0].EntityId);
}
[Fact]
public void GetNearbyObjects_QueryFarFromEntity_ReturnsEmpty()
{
var reg = new ShadowObjectRegistry();
// Entity at local (12, 12) — cell (0,0).
reg.Register(11u, 0x01000006u, new Vector3(12f, 12f, 50f), Quaternion.Identity, 1f, OffX, OffY, LbId);
var results = new List();
// Query at local (180, 180) — cell (7,7) — far away.
reg.GetNearbyObjects(new Vector3(180f, 180f, 50f), 5f, OffX, OffY, LbId, results);
Assert.Empty(results);
}
[Fact]
public void GetNearbyObjects_EntityInMultipleCells_ReturnedOnce()
{
// Entity spans cells 0,0 and 1,0 (local X≈24, radius=2).
var reg = new ShadowObjectRegistry();
reg.Register(20u, 0x01000007u, new Vector3(24f, 12f, 50f), Quaternion.Identity, 2f, OffX, OffY, LbId);
var results = new List();
// Large query covers both cells; entity must appear exactly once.
reg.GetNearbyObjects(new Vector3(24f, 12f, 50f), 10f, OffX, OffY, LbId, results);
Assert.Single(results);
Assert.Equal(20u, results[0].EntityId);
}
// -----------------------------------------------------------------------
// Cell ID formula
// -----------------------------------------------------------------------
[Theory]
[InlineData(0f, 0f, 1u)] // cell (0,0) → index 0*8+0+1 = 1
[InlineData(48f, 0f, 17u)] // cell (2,0) → 2*8+0+1 = 17
[InlineData(0f, 48f, 3u)] // cell (0,2) → 0*8+2+1 = 3
[InlineData(168f, 168f, 64u)] // cell (7,7) → 7*8+7+1 = 64
public void GetObjectsInCell_CellIdFormula_Correct(float lx, float ly, uint expectedLow)
{
var reg = new ShadowObjectRegistry();
// Small radius so entity sits in exactly one cell.
reg.Register(99u, 0x01000008u, new Vector3(lx + 0.5f, ly + 0.5f, 50f), Quaternion.Identity, 0.1f, OffX, OffY, LbId);
uint cellId = LbId | expectedLow;
var objs = reg.GetObjectsInCell(cellId);
Assert.Contains(objs, e => e.EntityId == 99u);
}
}