Files
Project-M/Assets/_Project/Tests/EditMode/BuildPlaceSystemTests.cs
T
2026-06-04 11:35:57 -07:00

152 lines
6.2 KiB
C#

using NUnit.Framework;
using ProjectM.Server;
using ProjectM.Simulation;
using Unity.Core;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
using Unity.Transforms;
namespace ProjectM.Tests
{
/// <summary>
/// Plain-Entities EditMode tests for the server-only <see cref="BuildPlaceSystem"/> — the RPC structure-placement
/// handler. A bare world is seeded with StructureCatalog (+ a Turret entry referencing a Prefab-tagged prefab),
/// BaseAnchor, ResourceLedger (+ Ore), NetworkTime, and synthetic BuildPlaceRequest + ReceiveRpcCommandRequest
/// entities. The headline case is co-op atomicity: two same-tick requests for one cell must place EXACTLY one
/// structure and withdraw the cost ONCE (the in-place commit). Also pins cost/plot validation and request cleanup.
/// </summary>
public class BuildPlaceSystemTests
{
static (World world, SimulationSystemGroup group) MakeWorld(string name, int oreCount)
{
var world = new World(name);
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
group.AddSystemToUpdateList(world.GetOrCreateSystem<BuildPlaceSystem>());
group.SortSystems();
world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f));
var em = world.EntityManager;
var nt = em.CreateEntity(typeof(NetworkTime));
em.SetComponentData(nt, new NetworkTime { ServerTick = new NetworkTick(300) });
var anchor = em.CreateEntity(typeof(BaseAnchor));
em.SetComponentData(anchor, new BaseAnchor
{
AnchorPos = new float3(5, 0, 5),
GridOrigin = new float3(0, 0, 0),
CellSize = 2f,
GridDims = new int2(5, 5),
});
// Turret prefab: LocalTransform (looked up for placement) + PlacedStructure (SetComponent target on the
// clone) + a real Prefab tag so it is excluded from the live-structure occupancy scan.
var prefab = em.CreateEntity(typeof(LocalTransform), typeof(PlacedStructure));
em.AddComponent<Prefab>(prefab);
var catalogE = em.CreateEntity(typeof(StructureCatalog));
var catalog = em.AddBuffer<StructureCatalogEntry>(catalogE);
catalog.Add(new StructureCatalogEntry
{
Type = StructureType.Turret, Prefab = prefab, CostResourceId = ResourceId.Ore, CostAmount = 10,
});
var ledgerE = em.CreateEntity(typeof(ResourceLedger));
var ledger = em.AddBuffer<StorageEntry>(ledgerE);
ledger.Add(new StorageEntry { ItemId = ResourceId.Ore, Count = oreCount });
return (world, group);
}
static void MakeBuildRequest(EntityManager em, byte type, int cellX, int cellZ)
{
var e = em.CreateEntity();
em.AddComponentData(e, new BuildPlaceRequest { StructureType = type, CellX = cellX, CellZ = cellZ });
em.AddComponentData(e, default(ReceiveRpcCommandRequest));
}
static int StructureCount(EntityManager em)
{
using var q = em.CreateEntityQuery(typeof(PlacedStructure));
return q.CalculateEntityCount();
}
static int OreCount(EntityManager em)
{
using var q = em.CreateEntityQuery(typeof(ResourceLedger));
var ledger = em.GetBuffer<StorageEntry>(q.GetSingletonEntity());
for (int i = 0; i < ledger.Length; i++)
if (ledger[i].ItemId == ResourceId.Ore) return ledger[i].Count;
return 0;
}
[Test]
public void Valid_Request_Places_Structure_Withdraws_Cost_And_Destroys_Request()
{
var (world, group) = MakeWorld("BuildValid", oreCount: 50);
using (world)
{
var em = world.EntityManager;
MakeBuildRequest(em, StructureType.Turret, cellX: 1, cellZ: 1);
group.Update();
Assert.AreEqual(1, StructureCount(em), "A valid request places exactly one structure.");
Assert.AreEqual(40, OreCount(em), "The build cost (10) is withdrawn from the ledger.");
using var reqQ = em.CreateEntityQuery(typeof(BuildPlaceRequest));
Assert.AreEqual(0, reqQ.CalculateEntityCount(), "The handled request is destroyed.");
}
}
[Test]
public void Two_Same_Cell_Requests_Place_Only_One_And_Withdraw_Once()
{
var (world, group) = MakeWorld("BuildAtomic", oreCount: 50);
using (world)
{
var em = world.EntityManager;
MakeBuildRequest(em, StructureType.Turret, cellX: 1, cellZ: 1);
MakeBuildRequest(em, StructureType.Turret, cellX: 1, cellZ: 1);
group.Update();
Assert.AreEqual(1, StructureCount(em),
"Two same-tick requests for one cell place exactly one structure (co-op atomicity).");
Assert.AreEqual(40, OreCount(em), "The cost is withdrawn exactly once, not twice.");
}
}
[Test]
public void Insufficient_Resources_Places_Nothing()
{
var (world, group) = MakeWorld("BuildPoor", oreCount: 5);
using (world)
{
var em = world.EntityManager;
MakeBuildRequest(em, StructureType.Turret, cellX: 1, cellZ: 1);
group.Update();
Assert.AreEqual(0, StructureCount(em), "A request that can't afford the cost places nothing.");
Assert.AreEqual(5, OreCount(em), "The ledger is untouched on an unaffordable request.");
}
}
[Test]
public void Out_Of_Plot_Cell_Places_Nothing()
{
var (world, group) = MakeWorld("BuildOOB", oreCount: 50);
using (world)
{
var em = world.EntityManager;
MakeBuildRequest(em, StructureType.Turret, cellX: 99, cellZ: 99);
group.Update();
Assert.AreEqual(0, StructureCount(em), "An out-of-plot cell places nothing.");
Assert.AreEqual(50, OreCount(em), "No cost is withdrawn for an illegal placement.");
}
}
}
}