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"); } } } }