using NUnit.Framework;
using ProjectM.Server;
using ProjectM.Simulation;
using Unity.Collections;
using Unity.Core;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
using Unity.Transforms;
namespace ProjectM.Tests
{
///
/// Pins Step 12a — the born-correct PERMANENT meta seeding in : a spawning
/// player replays its class's persisted tiers as meta-band StatModifiers
/// (Value = ValuePerTier * tier, SourceId = MetaSourceIdBase + id); other-class rows and unknown ids are
/// skipped; an over-MaxTier saved row is CLAMPED (D-F5); and the availability guard blocks the WHOLE spawn
/// (request preserved, nothing consumed) when the catalog is absent (N2).
///
public class MetaSeedingTests
{
static (World world, SimulationSystemGroup group) MakeWorld()
{
var world = new World("MetaSeedTest");
var group = world.GetOrCreateSystemManaged();
group.AddSystemToUpdateList(world.GetOrCreateSystem());
group.SortSystems();
world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f));
return (world, group);
}
static Entity MakePlayerPrefab(EntityManager em)
{
var e = em.CreateEntity(typeof(LocalTransform), typeof(GhostOwner), typeof(FrameId), typeof(PlayerTag));
em.SetComponentData(e, LocalTransform.Identity);
em.AddBuffer(e);
em.AddBuffer(e); // the spawn path SetBuffers the frame loadout unconditionally (LANTERN purge)
em.AddComponent(e);
return e;
}
static void MakeSpawnRequest(EntityManager em, byte classId)
{
var conn = em.CreateEntity(typeof(NetworkId));
em.SetComponentData(conn, new NetworkId { Value = 1 });
em.AddBuffer(conn);
var req = em.CreateEntity(typeof(GoInGameRequest), typeof(ReceiveRpcCommandRequest));
em.SetComponentData(req, new GoInGameRequest { ClassId = classId });
em.SetComponentData(req, new ReceiveRpcCommandRequest { SourceConnection = conn });
}
static int MetaRows(EntityManager em, Entity player, out float firstValue, out uint firstSource)
{
firstValue = 0f; firstSource = 0;
var mods = em.GetBuffer(player);
int n = 0;
for (int i = 0; i < mods.Length; i++)
if (mods[i].SourceId >= Tuning.MetaSourceIdBase
&& mods[i].SourceId < Tuning.MetaSourceIdBase + Tuning.MetaSourceIdSpan)
{
if (n == 0) { firstValue = mods[i].Value; firstSource = mods[i].SourceId; }
n++;
}
return n;
}
[Test]
public void Spawn_SeedsClassTiers_SkipsOthers_ClampsOverMax()
{
var (world, group) = MakeWorld();
using (world)
{
var em = world.EntityManager;
var prefab = MakePlayerPrefab(em);
var spawnerE = em.CreateEntity(typeof(PlayerSpawner));
em.SetComponentData(spawnerE, new PlayerSpawner { PlayerPrefab = prefab, SpawnRingRadius = 2f, RingSlots = 8 });
var catalogE = em.CreateEntity(typeof(MetaUpgradeCatalog));
em.SetComponentData(catalogE, new MetaUpgradeCatalog { Value = MetaCatalogData.BuildDefault() });
var record = em.AddBuffer(catalogE); // the tier record rides any singleton entity in tests
byte warrior = ClassTraits.WarriorClass; // normalized FrameKind (2)
record.Add(new MetaTierState { ClassId = warrior, UpgradeId = 1, Tier = 2 }); // Reinforced Frame t2 -> +30
record.Add(new MetaTierState { ClassId = warrior, UpgradeId = 5, Tier = 9 }); // Warrior's Might, saved OVER MaxTier(4) -> clamp
record.Add(new MetaTierState { ClassId = warrior, UpgradeId = 200, Tier = 1 }); // unknown id -> skipped
record.Add(new MetaTierState { ClassId = ClassTraits.RangerClass, UpgradeId = 2, Tier = 3 }); // other class -> skipped
record.Add(new MetaTierState { ClassId = warrior, UpgradeId = 7, Tier = 1 }); // Ranger-masked (Longshot) -> skipped
MakeSpawnRequest(em, warrior);
group.Update();
var pq = em.CreateEntityQuery(typeof(PlayerTag), typeof(PlayerClass));
var players = pq.ToEntityArray(Allocator.Temp);
Assert.AreEqual(1, players.Length, "player spawned");
var player = players[0];
players.Dispose(); pq.Dispose(); // BEFORE the world
var mods = em.GetBuffer(player);
float frameValue = 0f, mightValue = 0f;
int metaRows = 0;
for (int i = 0; i < mods.Length; i++)
{
if (mods[i].SourceId == Tuning.MetaSourceIdBase + 1) { frameValue = mods[i].Value; metaRows++; }
else if (mods[i].SourceId == Tuning.MetaSourceIdBase + 5) { mightValue = mods[i].Value; metaRows++; }
else if (mods[i].SourceId >= Tuning.MetaSourceIdBase
&& mods[i].SourceId < Tuning.MetaSourceIdBase + Tuning.MetaSourceIdSpan) metaRows++;
}
Assert.AreEqual(2, metaRows, "exactly the two legal Warrior tiers seeded (unknown/other-class/other-mask skipped)");
Assert.AreEqual(30f, frameValue, 1e-3f, "Reinforced Frame tier 2 = 15 * 2");
Assert.AreEqual(0.40f, mightValue, 1e-3f, "Warrior's Might CLAMPED to MaxTier 4 = 0.10 * 4 (D-F5)");
}
}
[Test]
public void MissingCatalog_BlocksSpawn_PreservesRequest()
{
var (world, group) = MakeWorld();
using (world)
{
var em = world.EntityManager;
var prefab = MakePlayerPrefab(em);
var spawnerE = em.CreateEntity(typeof(PlayerSpawner));
em.SetComponentData(spawnerE, new PlayerSpawner { PlayerPrefab = prefab, SpawnRingRadius = 2f, RingSlots = 8 });
// NO catalog, NO tier record.
MakeSpawnRequest(em, ClassTraits.WarriorClass);
group.Update();
var pq = em.CreateEntityQuery(typeof(PlayerClass));
Assert.AreEqual(0, pq.CalculateEntityCount(), "no player spawned while blocked (N2)");
var rq = em.CreateEntityQuery(typeof(GoInGameRequest));
Assert.AreEqual(1, rq.CalculateEntityCount(), "request PRESERVED — the spawn retries when the catalog streams in");
pq.Dispose(); rq.Dispose(); // BEFORE the world (a using-var here would outlive it)in");
}
}
}
}