using NUnit.Framework; using ProjectM.Server; using ProjectM.Simulation; using Unity.Collections; using Unity.Core; using Unity.Entities; using Unity.NetCode; namespace ProjectM.Tests { /// /// Pins Step 13 — : the Staging-gated Aether→tier purchase with DR-014 in-loop /// ledger atomicity (two same-tick barely-enough purchases → exactly ONE succeeds), the TotalOf pre-check /// (Withdraw clamps, it never rejects), the ABSOLUTE-value modifier upsert on every live class member (R-F1/2), /// tier bump-or-append on the director record, the SaveRequest flag, and the reject paths (wrong class mask, /// MaxTier cap, non-Staging lifecycle) — with requests always consumed. /// public class MetaSpendSystemTests { static (World world, SimulationSystemGroup group) MakeWorld() { var world = new World("MetaSpendTest"); var group = world.GetOrCreateSystemManaged(); group.AddSystemToUpdateList(world.GetOrCreateSystem()); group.SortSystems(); world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f)); return (world, group); } static Entity MakeDirector(EntityManager em, byte lifecycle, int aether) { var dir = em.CreateEntity(typeof(RunInfo), typeof(ResourceLedger), typeof(SaveRequest), typeof(MetaUpgradeCatalog)); em.SetComponentData(dir, new RunInfo { Lifecycle = lifecycle }); em.SetComponentData(dir, new MetaUpgradeCatalog { Value = MetaCatalogData.BuildDefault() }); var ledger = em.AddBuffer(dir); if (aether > 0) ledger.Add(new StorageEntry { ItemId = ResourceId.Aether, Count = aether }); em.AddBuffer(dir); return dir; } static Entity MakePlayer(EntityManager em, int networkId, byte classId) { var conn = em.CreateEntity(typeof(NetworkId)); em.SetComponentData(conn, new NetworkId { Value = networkId }); var player = em.CreateEntity(typeof(PlayerTag), typeof(GhostOwner), typeof(PlayerClass)); em.SetComponentData(player, new GhostOwner { NetworkId = networkId }); em.SetComponentData(player, new PlayerClass { ClassId = classId }); em.AddBuffer(player); em.AddComponentData(player, new ConnRef { Conn = conn }); return player; } /// Test-only pointer so a request can be issued from the player's own connection. struct ConnRef : IComponentData { public Entity Conn; } static void SendRequest(EntityManager em, Entity player, byte upgradeId) { var conn = em.GetComponentData(player).Conn; var req = em.CreateEntity(typeof(MetaSpendRequest), typeof(ReceiveRpcCommandRequest)); em.SetComponentData(req, new MetaSpendRequest { UpgradeId = upgradeId }); em.SetComponentData(req, new ReceiveRpcCommandRequest { SourceConnection = conn }); } static int PendingRequests(EntityManager em) { var q = em.CreateEntityQuery(typeof(MetaSpendRequest)); int n = q.CalculateEntityCount(); q.Dispose(); return n; } static float MetaModValue(EntityManager em, Entity player, byte upgradeId, out int rowCount) { var mods = em.GetBuffer(player, true); float value = 0f; rowCount = 0; for (int i = 0; i < mods.Length; i++) if (mods[i].SourceId == Tuning.MetaSourceIdBase + upgradeId) { value = mods[i].Value; rowCount++; } return value; } [Test] public void Purchase_BumpsTier_Withdraws_UpsertsAllClassMembers_FlagsSave() { var (world, group) = MakeWorld(); using (world) { var em = world.EntityManager; var dir = MakeDirector(em, RunLifecycle.Staging, 25); var warriorA = MakePlayer(em, 1, ClassTraits.WarriorClass); var warriorB = MakePlayer(em, 2, ClassTraits.WarriorClass); // classmate: shared per-class pool var ranger = MakePlayer(em, 3, ClassTraits.RangerClass); // other class: untouched SendRequest(em, warriorA, 1); // Reinforced Frame: BaseCost 10, +15/tier group.Update(); var record = em.GetBuffer(dir, true); Assert.AreEqual(1, MetaMath.TierOf(record, ClassTraits.WarriorClass, 1), "tier bumped to 1"); Assert.AreEqual(15, StorageMath.TotalOf(em.GetBuffer(dir, true), ResourceId.Aether), "cost 10 withdrawn from 25"); Assert.AreEqual(15f, MetaModValue(em, warriorA, 1, out int rowsA), 1e-3f, "buyer modifier = 15 * tier1"); Assert.AreEqual(1, rowsA); Assert.AreEqual(15f, MetaModValue(em, warriorB, 1, out _), 1e-3f, "live classmate upserted too (R-F2)"); Assert.AreEqual(0f, MetaModValue(em, ranger, 1, out int rowsR), 1e-3f, "other class untouched"); Assert.AreEqual(0, rowsR); Assert.AreEqual(1, em.GetComponentData(dir).Pending, "purchase flags the autosave"); Assert.AreEqual(0, PendingRequests(em), "request consumed"); } } [Test] public void TwoSameTick_BarelyEnough_ExactlyOneSucceeds() { var (world, group) = MakeWorld(); using (world) { var em = world.EntityManager; var dir = MakeDirector(em, RunLifecycle.Staging, 10); // ids 1 and 4 BOTH cost 10 at tier 0 var warrior = MakePlayer(em, 1, ClassTraits.WarriorClass); SendRequest(em, warrior, 1); SendRequest(em, warrior, 4); group.Update(); var record = em.GetBuffer(dir, true); int bought = MetaMath.TierOf(record, ClassTraits.WarriorClass, 1) + MetaMath.TierOf(record, ClassTraits.WarriorClass, 4); Assert.AreEqual(1, bought, "in-loop atomicity: barely-enough Aether buys exactly ONE (DR-014)"); Assert.AreEqual(0, StorageMath.TotalOf(em.GetBuffer(dir, true), ResourceId.Aether), "the single cost fully drained the ledger — and never went negative (TotalOf pre-check)"); Assert.AreEqual(0, PendingRequests(em), "both requests consumed"); } } [Test] public void SecondPurchase_AbsoluteUpsert_SingleRow_RampedCost() { var (world, group) = MakeWorld(); using (world) { var em = world.EntityManager; var dir = MakeDirector(em, RunLifecycle.Staging, 30); // tier1 = 10, tier2 = 10 + 1*5 = 15 var warrior = MakePlayer(em, 1, ClassTraits.WarriorClass); SendRequest(em, warrior, 1); group.Update(); SendRequest(em, warrior, 1); group.Update(); var record = em.GetBuffer(dir, true); Assert.AreEqual(2, MetaMath.TierOf(record, ClassTraits.WarriorClass, 1), "tier 2 owned"); Assert.AreEqual(1, record.Length, "record row BUMPED in place, not duplicated"); Assert.AreEqual(30f, MetaModValue(em, warrior, 1, out int rows), 1e-3f, "ABSOLUTE upsert: 15 * tier2 (R-F1)"); Assert.AreEqual(1, rows, "one modifier row — an incremental append would double-count in recompute"); Assert.AreEqual(5, StorageMath.TotalOf(em.GetBuffer(dir, true), ResourceId.Aether), "linear ramp: 30 - 10 - 15"); } } [Test] public void Rejects_WrongClassMask_MaxTierCap_NonStaging() { var (world, group) = MakeWorld(); using (world) { var em = world.EntityManager; var dir = MakeDirector(em, RunLifecycle.Staging, 999); var warrior = MakePlayer(em, 1, ClassTraits.WarriorClass); // (a) Ranger-masked upgrade requested by a Warrior — dropped, nothing withdrawn. SendRequest(em, warrior, 7); // Ranger's Longshot (mask 2) group.Update(); Assert.AreEqual(999, StorageMath.TotalOf(em.GetBuffer(dir, true), ResourceId.Aether), "class-mask reject leaves the ledger untouched"); // (b) at MaxTier — dropped. Fleet Stride (id 4) MaxTier 3. var record = em.GetBuffer(dir); record.Add(new MetaTierState { ClassId = ClassTraits.WarriorClass, UpgradeId = 4, Tier = 3 }); SendRequest(em, warrior, 4); group.Update(); Assert.AreEqual(3, MetaMath.TierOf(em.GetBuffer(dir, true), ClassTraits.WarriorClass, 4), "MaxTier cap holds"); Assert.AreEqual(999, StorageMath.TotalOf(em.GetBuffer(dir, true), ResourceId.Aether)); // (c) mid-run — dropped (N4: the shop is a between-runs surface). em.SetComponentData(dir, new RunInfo { Lifecycle = RunLifecycle.InRoom }); SendRequest(em, warrior, 1); group.Update(); Assert.AreEqual(0, MetaMath.TierOf(em.GetBuffer(dir, true), ClassTraits.WarriorClass, 1), "non-Staging purchase dropped"); Assert.AreEqual(999, StorageMath.TotalOf(em.GetBuffer(dir, true), ResourceId.Aether)); Assert.AreEqual(0, PendingRequests(em), "every request consumed, accepted or not"); } } } }