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 { /// /// Plain-Entities EditMode tests for the server-only — 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. /// public class BuildPlaceSystemTests { static (World world, SimulationSystemGroup group) MakeWorld(string name, int oreCount) { var world = new World(name); var group = world.GetOrCreateSystemManaged(); group.AddSystemToUpdateList(world.GetOrCreateSystem()); 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); var catalogE = em.CreateEntity(typeof(StructureCatalog)); var catalog = em.AddBuffer(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(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(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."); } } } }