135 lines
6.3 KiB
C#
135 lines
6.3 KiB
C#
using NUnit.Framework;
|
|
using ProjectM.Server;
|
|
using ProjectM.Simulation;
|
|
using Unity.Core;
|
|
using Unity.Entities;
|
|
using Unity.NetCode;
|
|
|
|
namespace ProjectM.Tests
|
|
{
|
|
/// <summary>
|
|
/// Plain-Entities EditMode tests for the server-only <see cref="TimedModifierExpirySystem"/> — timed/removable
|
|
/// StatModifiers. A bare world is seeded with a NetworkTime singleton and a player carrying paired StatModifier
|
|
/// + TimedModifier buffers (same SourceId). Pins: a modifier persists until its tick then is removed by SourceId
|
|
/// (along with its tracker row); independent expiry of multiple timed mods; the clear-by-SourceId helper; and
|
|
/// the TickUtil.NonZero guard so a wrap-tick expiry never collides with the 0 = "inert" sentinel. The
|
|
/// replicated StatModifier layout is untouched (separate tracker buffer) so there is no ghost re-bake.
|
|
/// </summary>
|
|
public class TimedModifierExpirySystemTests
|
|
{
|
|
static (World world, SimulationSystemGroup group) MakeWorld(string name, uint serverTick)
|
|
{
|
|
var world = new World(name);
|
|
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
|
|
group.AddSystemToUpdateList(world.GetOrCreateSystem<TimedModifierExpirySystem>());
|
|
group.SortSystems();
|
|
world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f));
|
|
SetServerTick(world, serverTick);
|
|
return (world, group);
|
|
}
|
|
|
|
static void SetServerTick(World world, uint tick)
|
|
{
|
|
var em = world.EntityManager;
|
|
using var q = em.CreateEntityQuery(typeof(NetworkTime));
|
|
Entity e = q.IsEmpty ? em.CreateEntity(typeof(NetworkTime)) : q.GetSingletonEntity();
|
|
em.SetComponentData(e, new NetworkTime { ServerTick = new NetworkTick(tick) });
|
|
}
|
|
|
|
static Entity MakePlayer(EntityManager em)
|
|
{
|
|
var e = em.CreateEntity();
|
|
em.AddBuffer<StatModifier>(e);
|
|
em.AddBuffer<TimedModifier>(e);
|
|
return e;
|
|
}
|
|
|
|
static void AddTimedMod(EntityManager em, Entity e, byte target, float value, uint sourceId, uint untilTick)
|
|
{
|
|
em.GetBuffer<StatModifier>(e).Add(new StatModifier { Target = target, Op = (byte)ModOp.Flat, Value = value, SourceId = sourceId });
|
|
em.GetBuffer<TimedModifier>(e).Add(new TimedModifier { SourceId = sourceId, UntilTick = untilTick });
|
|
}
|
|
|
|
[Test]
|
|
public void Modifier_Persists_Before_Expiry_And_Is_Removed_After()
|
|
{
|
|
var (world, group) = MakeWorld("TimedExpire", serverTick: 200);
|
|
using (world)
|
|
{
|
|
var em = world.EntityManager;
|
|
var p = MakePlayer(em);
|
|
AddTimedMod(em, p, (byte)StatTarget.Damage, 10f, sourceId: 0xABCDu, untilTick: 300);
|
|
|
|
group.Update(); // serverTick 200 < 300 -> not due
|
|
Assert.AreEqual(1, em.GetBuffer<StatModifier>(p).Length, "Modifier persists before its expiry tick.");
|
|
Assert.AreEqual(1, em.GetBuffer<TimedModifier>(p).Length);
|
|
|
|
SetServerTick(world, 300);
|
|
group.Update(); // due (300 not newer than 300)
|
|
Assert.AreEqual(0, em.GetBuffer<StatModifier>(p).Length, "Expired modifier is removed by SourceId.");
|
|
Assert.AreEqual(0, em.GetBuffer<TimedModifier>(p).Length, "The timed-tracker row is removed too.");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Timed_Modifiers_Expire_Independently()
|
|
{
|
|
var (world, group) = MakeWorld("TimedIndependent", serverTick: 200);
|
|
using (world)
|
|
{
|
|
var em = world.EntityManager;
|
|
var p = MakePlayer(em);
|
|
AddTimedMod(em, p, (byte)StatTarget.Damage, 10f, sourceId: 1u, untilTick: 250);
|
|
AddTimedMod(em, p, (byte)StatTarget.MoveSpeed, 0.2f, sourceId: 2u, untilTick: 350);
|
|
|
|
SetServerTick(world, 250);
|
|
group.Update();
|
|
var mods = em.GetBuffer<StatModifier>(p);
|
|
Assert.AreEqual(1, mods.Length, "Only the first timed modifier expires at tick 250.");
|
|
Assert.AreEqual(2u, mods[0].SourceId, "The longer-lived modifier (SourceId 2) survives.");
|
|
|
|
SetServerTick(world, 350);
|
|
group.Update();
|
|
Assert.AreEqual(0, em.GetBuffer<StatModifier>(p).Length, "The second expires at its own tick.");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void RemoveBySourceId_Clears_On_Demand()
|
|
{
|
|
var (world, group) = MakeWorld("TimedClearByType", serverTick: 200);
|
|
using (world)
|
|
{
|
|
var em = world.EntityManager;
|
|
var p = MakePlayer(em);
|
|
var b = em.GetBuffer<StatModifier>(p);
|
|
b.Add(new StatModifier { Target = (byte)StatTarget.Damage, Op = (byte)ModOp.Flat, Value = 5f, SourceId = 7u });
|
|
b.Add(new StatModifier { Target = (byte)StatTarget.Damage, Op = (byte)ModOp.Flat, Value = 9f, SourceId = 8u });
|
|
|
|
int removed = TimedModifierUtil.RemoveBySourceId(em.GetBuffer<StatModifier>(p), 7u);
|
|
Assert.AreEqual(1, removed, "Clear-by-SourceId removes exactly the matching row.");
|
|
var mods = em.GetBuffer<StatModifier>(p);
|
|
Assert.AreEqual(1, mods.Length);
|
|
Assert.AreEqual(8u, mods[0].SourceId, "The non-matching modifier is untouched.");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void NonZero_UntilTick_Never_Collides_With_The_Zero_Sentinel()
|
|
{
|
|
var (world, group) = MakeWorld("TimedWrap", serverTick: 1);
|
|
using (world)
|
|
{
|
|
var em = world.EntityManager;
|
|
var p = MakePlayer(em);
|
|
uint until = TickUtil.NonZero(0u); // a grant/death exactly at tick 0 must not read as 'inert'
|
|
Assert.AreNotEqual(0u, until, "TickUtil.NonZero coerces 0 -> 1 so a scheduled expiry is never the inert sentinel.");
|
|
AddTimedMod(em, p, (byte)StatTarget.Damage, 3f, sourceId: 9u, untilTick: until);
|
|
|
|
group.Update(); // serverTick 1 >= until(1) -> due
|
|
Assert.AreEqual(0, em.GetBuffer<StatModifier>(p).Length, "A modifier scheduled at the wrap sentinel still expires.");
|
|
}
|
|
}
|
|
}
|
|
}
|