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();
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");
world.Dispose();
}
[Test]
public void TwoSameTick_BarelyEnough_ExactlyOneSucceeds()
{
var (world, group) = MakeWorld();
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");
world.Dispose();
}
[Test]
public void SecondPurchase_AbsoluteUpsert_SingleRow_RampedCost()
{
var (world, group) = MakeWorld();
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");
world.Dispose();
}
[Test]
public void Rejects_WrongClassMask_MaxTierCap_NonStaging()
{
var (world, group) = MakeWorld();
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");
world.Dispose();
}
}
}