Tests: inventory EditMode coverage (DR-026)
InventoryMathTests (stack/slot-cap/withdraw/CountOf); InventoryHarvestTests (owned harvest -> inventory with ledger untouched, full-bag spill, no-matching-player -> ledger fallback); InventoryDepositSystemTests (specific/all/unresolvable). The 8 existing ResourceHarvestSystemTests stay green via the genuine no-owner fallback. 228/228 EditMode pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,131 @@
|
|||||||
|
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="InventoryDepositSystem"/> — the RPC that
|
||||||
|
/// moves a player's PERSONAL inventory into the shared ledger. Mirrors RegionTransitSystemTests' seeding:
|
||||||
|
/// a ResourceLedger singleton, a mock connection (NetworkId), a player (GhostOwner + InventorySlot +
|
||||||
|
/// PlayerTag), and an InventoryDepositRequest + ReceiveRpcCommandRequest. Pins: a specific-item deposit
|
||||||
|
/// moves the clamped amount; ItemId 0 deposits everything and empties the bag; an unresolvable connection
|
||||||
|
/// moves nothing; the request is consumed either way.
|
||||||
|
/// </summary>
|
||||||
|
public class InventoryDepositSystemTests
|
||||||
|
{
|
||||||
|
static (World world, SimulationSystemGroup group, Entity ledger) MakeWorld(string name)
|
||||||
|
{
|
||||||
|
var world = new World(name);
|
||||||
|
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
|
||||||
|
group.AddSystemToUpdateList(world.GetOrCreateSystem<InventoryDepositSystem>());
|
||||||
|
group.SortSystems();
|
||||||
|
world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f));
|
||||||
|
var em = world.EntityManager;
|
||||||
|
var ledger = em.CreateEntity(typeof(ResourceLedger));
|
||||||
|
em.AddBuffer<StorageEntry>(ledger);
|
||||||
|
return (world, group, ledger);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Entity MakeConnection(EntityManager em, int networkId)
|
||||||
|
{
|
||||||
|
var e = em.CreateEntity();
|
||||||
|
em.AddComponentData(e, new NetworkId { Value = networkId });
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Entity MakePlayer(EntityManager em, int networkId, params (ushort id, int count)[] items)
|
||||||
|
{
|
||||||
|
var e = em.CreateEntity();
|
||||||
|
em.AddComponentData(e, new GhostOwner { NetworkId = networkId });
|
||||||
|
em.AddComponent<PlayerTag>(e);
|
||||||
|
var bag = em.AddBuffer<InventorySlot>(e);
|
||||||
|
foreach (var it in items)
|
||||||
|
bag.Add(new InventorySlot { ItemId = it.id, Count = it.count });
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void MakeRequest(EntityManager em, ushort itemId, int count, Entity conn)
|
||||||
|
{
|
||||||
|
var e = em.CreateEntity();
|
||||||
|
em.AddComponentData(e, new InventoryDepositRequest { ItemId = itemId, Count = count });
|
||||||
|
em.AddComponentData(e, new ReceiveRpcCommandRequest { SourceConnection = conn });
|
||||||
|
}
|
||||||
|
|
||||||
|
static int LedgerCount(EntityManager em, Entity ledger, ushort itemId)
|
||||||
|
{
|
||||||
|
var buf = em.GetBuffer<StorageEntry>(ledger);
|
||||||
|
for (int i = 0; i < buf.Length; i++)
|
||||||
|
if (buf[i].ItemId == itemId) return buf[i].Count;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int InvCount(EntityManager em, Entity player, ushort itemId)
|
||||||
|
{
|
||||||
|
var buf = em.GetBuffer<InventorySlot>(player);
|
||||||
|
return InventoryMath.CountOf(buf, itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Deposit_Specific_Item_Moves_Clamped_Amount_To_Ledger()
|
||||||
|
{
|
||||||
|
var (world, group, ledger) = MakeWorld("DepositSpecific");
|
||||||
|
using (world)
|
||||||
|
{
|
||||||
|
var em = world.EntityManager;
|
||||||
|
var conn = MakeConnection(em, 1);
|
||||||
|
var player = MakePlayer(em, 1, (ResourceId.Ore, 30));
|
||||||
|
MakeRequest(em, ResourceId.Ore, 20, conn);
|
||||||
|
|
||||||
|
group.Update();
|
||||||
|
|
||||||
|
Assert.AreEqual(10, InvCount(em, player, ResourceId.Ore), "20 of 30 Ore moved out of the bag.");
|
||||||
|
Assert.AreEqual(20, LedgerCount(em, ledger, ResourceId.Ore), "20 Ore landed in the shared ledger.");
|
||||||
|
using var q = em.CreateEntityQuery(typeof(InventoryDepositRequest));
|
||||||
|
Assert.AreEqual(0, q.CalculateEntityCount(), "The request is consumed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Deposit_All_Empties_Bag_Into_Ledger()
|
||||||
|
{
|
||||||
|
var (world, group, ledger) = MakeWorld("DepositAll");
|
||||||
|
using (world)
|
||||||
|
{
|
||||||
|
var em = world.EntityManager;
|
||||||
|
var conn = MakeConnection(em, 1);
|
||||||
|
var player = MakePlayer(em, 1, (ResourceId.Ore, 30), (ResourceId.Aether, 5));
|
||||||
|
MakeRequest(em, itemId: 0, count: 0, conn); // 0 = deposit all
|
||||||
|
|
||||||
|
group.Update();
|
||||||
|
|
||||||
|
Assert.AreEqual(0, InvCount(em, player, ResourceId.Ore), "Deposit-all empties the bag.");
|
||||||
|
Assert.AreEqual(0, InvCount(em, player, ResourceId.Aether));
|
||||||
|
Assert.AreEqual(30, LedgerCount(em, ledger, ResourceId.Ore));
|
||||||
|
Assert.AreEqual(5, LedgerCount(em, ledger, ResourceId.Aether));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Deposit_From_Unresolvable_Connection_Moves_Nothing()
|
||||||
|
{
|
||||||
|
var (world, group, ledger) = MakeWorld("DepositUnknown");
|
||||||
|
using (world)
|
||||||
|
{
|
||||||
|
var em = world.EntityManager;
|
||||||
|
var player = MakePlayer(em, 1, (ResourceId.Ore, 30));
|
||||||
|
MakeRequest(em, ResourceId.Ore, 20, Entity.Null); // no NetworkId on Entity.Null
|
||||||
|
|
||||||
|
group.Update();
|
||||||
|
|
||||||
|
Assert.AreEqual(30, InvCount(em, player, ResourceId.Ore), "An unresolvable sender moves nothing.");
|
||||||
|
Assert.AreEqual(0, LedgerCount(em, ledger, ResourceId.Ore));
|
||||||
|
using var q = em.CreateEntityQuery(typeof(InventoryDepositRequest));
|
||||||
|
Assert.AreEqual(0, q.CalculateEntityCount(), "The request is still consumed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e0a222e00ad08444793bf5b1ffccc71a
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
using NUnit.Framework;
|
||||||
|
using ProjectM.Server;
|
||||||
|
using ProjectM.Simulation;
|
||||||
|
using Unity.Core;
|
||||||
|
using Unity.Entities;
|
||||||
|
using Unity.Mathematics;
|
||||||
|
using Unity.NetCode;
|
||||||
|
using Unity.Transforms;
|
||||||
|
|
||||||
|
namespace ProjectM.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Plain-Entities EditMode tests for the harvest -> PERSONAL inventory reroute in
|
||||||
|
/// <see cref="ResourceHarvestSystem"/>. A bare world is seeded with a ResourceLedger singleton, a node,
|
||||||
|
/// a player (GhostOwner + InventorySlot + PlayerTag) and an OWNED projectile (matching GhostOwner). Pins:
|
||||||
|
/// an owned hit lands in the player's inventory and leaves the ledger untouched; a full bag spills the
|
||||||
|
/// remainder to the ledger; an owned projectile whose NetworkId has no live player falls back to the ledger.
|
||||||
|
/// The 8 owner-less tests in <see cref="ResourceHarvestSystemTests"/> pin the un-owned -> ledger fallback.
|
||||||
|
/// </summary>
|
||||||
|
public class InventoryHarvestTests
|
||||||
|
{
|
||||||
|
static (World world, SimulationSystemGroup group, Entity ledger) MakeWorld(string name)
|
||||||
|
{
|
||||||
|
var world = new World(name);
|
||||||
|
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
|
||||||
|
group.AddSystemToUpdateList(world.GetOrCreateSystem<ResourceHarvestSystem>());
|
||||||
|
group.SortSystems();
|
||||||
|
world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f));
|
||||||
|
var em = world.EntityManager;
|
||||||
|
var ledger = em.CreateEntity(typeof(ResourceLedger));
|
||||||
|
em.AddBuffer<StorageEntry>(ledger);
|
||||||
|
return (world, group, ledger);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Entity MakeNode(EntityManager em, float3 pos, float hitRadius, byte resourceId, int remaining, float perHit)
|
||||||
|
{
|
||||||
|
var e = em.CreateEntity();
|
||||||
|
em.AddComponentData(e, LocalTransform.FromPosition(pos));
|
||||||
|
em.AddComponentData(e, new HitRadius { Value = hitRadius });
|
||||||
|
em.AddComponentData(e, new ResourceNode { ResourceId = resourceId, Remaining = remaining, HarvestPerHit = perHit });
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Entity MakePlayer(EntityManager em, int networkId)
|
||||||
|
{
|
||||||
|
var e = em.CreateEntity();
|
||||||
|
em.AddComponentData(e, new GhostOwner { NetworkId = networkId });
|
||||||
|
em.AddComponent<PlayerTag>(e);
|
||||||
|
em.AddBuffer<InventorySlot>(e);
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Entity MakeOwnedProjectile(EntityManager em, float3 pos, float2 dir, float lastStep, int networkId)
|
||||||
|
{
|
||||||
|
var e = em.CreateEntity();
|
||||||
|
em.AddComponentData(e, LocalTransform.FromPosition(pos));
|
||||||
|
em.AddComponentData(e, new Projectile { Direction = dir, LastStep = lastStep });
|
||||||
|
em.AddComponentData(e, new GhostOwner { NetworkId = networkId });
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int LedgerCount(EntityManager em, Entity ledger, ushort itemId)
|
||||||
|
{
|
||||||
|
var buf = em.GetBuffer<StorageEntry>(ledger);
|
||||||
|
for (int i = 0; i < buf.Length; i++)
|
||||||
|
if (buf[i].ItemId == itemId) return buf[i].Count;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int InvCount(EntityManager em, Entity player, ushort itemId)
|
||||||
|
{
|
||||||
|
var buf = em.GetBuffer<InventorySlot>(player);
|
||||||
|
return InventoryMath.CountOf(buf, itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Owned_Harvest_Lands_In_Player_Inventory_Ledger_Untouched()
|
||||||
|
{
|
||||||
|
var (world, group, ledger) = MakeWorld("OwnedHarvest");
|
||||||
|
using (world)
|
||||||
|
{
|
||||||
|
var em = world.EntityManager;
|
||||||
|
var player = MakePlayer(em, networkId: 1);
|
||||||
|
var node = MakeNode(em, new float3(10, 1, 10), 1f, ResourceId.Aether, remaining: 100, perHit: 25f);
|
||||||
|
var proj = MakeOwnedProjectile(em, new float3(10, 1, 10), new float2(1, 0), 5f, networkId: 1);
|
||||||
|
|
||||||
|
group.Update();
|
||||||
|
|
||||||
|
Assert.AreEqual(25, InvCount(em, player, ResourceId.Aether), "The owner's harvest lands in their personal inventory.");
|
||||||
|
Assert.AreEqual(0, LedgerCount(em, ledger, ResourceId.Aether), "The shared ledger is untouched by an owned harvest.");
|
||||||
|
Assert.AreEqual(75, em.GetComponentData<ResourceNode>(node).Remaining);
|
||||||
|
Assert.IsFalse(em.Exists(proj), "The projectile is consumed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Full_Bag_Spills_Remainder_To_Ledger()
|
||||||
|
{
|
||||||
|
var (world, group, ledger) = MakeWorld("FullBagSpill");
|
||||||
|
using (world)
|
||||||
|
{
|
||||||
|
var em = world.EntityManager;
|
||||||
|
var player = MakePlayer(em, networkId: 1);
|
||||||
|
// Fill all InventoryMaxSlots with distinct dummy items so no slot is free for the harvested id.
|
||||||
|
var bag = em.GetBuffer<InventorySlot>(player);
|
||||||
|
for (int i = 0; i < Tuning.InventoryMaxSlots; i++)
|
||||||
|
bag.Add(new InventorySlot { ItemId = (ushort)(100 + i), Count = 1 });
|
||||||
|
|
||||||
|
var node = MakeNode(em, new float3(10, 1, 10), 1f, ResourceId.Ore, remaining: 100, perHit: 25f);
|
||||||
|
var proj = MakeOwnedProjectile(em, new float3(10, 1, 10), new float2(1, 0), 5f, networkId: 1);
|
||||||
|
|
||||||
|
group.Update();
|
||||||
|
|
||||||
|
Assert.AreEqual(0, InvCount(em, player, ResourceId.Ore), "A full bag cannot take the harvested item.");
|
||||||
|
Assert.AreEqual(25, LedgerCount(em, ledger, ResourceId.Ore), "The full amount spills to the shared ledger.");
|
||||||
|
Assert.AreEqual(75, em.GetComponentData<ResourceNode>(node).Remaining, "The node decrements by the FULL amount, not just the part that fit.");
|
||||||
|
Assert.IsFalse(em.Exists(proj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Owned_Projectile_With_No_Matching_Player_Falls_Back_To_Ledger()
|
||||||
|
{
|
||||||
|
var (world, group, ledger) = MakeWorld("NoMatchingPlayer");
|
||||||
|
using (world)
|
||||||
|
{
|
||||||
|
var em = world.EntityManager;
|
||||||
|
var player = MakePlayer(em, networkId: 1);
|
||||||
|
var node = MakeNode(em, new float3(10, 1, 10), 1f, ResourceId.Aether, remaining: 100, perHit: 25f);
|
||||||
|
// Projectile owned by NetworkId 99 — no live player has that id.
|
||||||
|
var proj = MakeOwnedProjectile(em, new float3(10, 1, 10), new float2(1, 0), 5f, networkId: 99);
|
||||||
|
|
||||||
|
group.Update();
|
||||||
|
|
||||||
|
Assert.AreEqual(0, InvCount(em, player, ResourceId.Aether), "Player 1 gets nothing — it didn't fire this shot.");
|
||||||
|
Assert.AreEqual(25, LedgerCount(em, ledger, ResourceId.Aether), "An unresolvable owner falls back to the shared ledger.");
|
||||||
|
Assert.IsFalse(em.Exists(proj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 42fed61a7f84fd740b17ec2c4cff8204
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using NUnit.Framework;
|
||||||
|
using ProjectM.Simulation;
|
||||||
|
using Unity.Entities;
|
||||||
|
|
||||||
|
namespace ProjectM.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Plain-Entities EditMode tests for the pure <see cref="InventoryMath"/> stacking logic (the per-player
|
||||||
|
/// bag math): top-up-then-append with a per-item stack cap, a max slot count returning a remainder, and
|
||||||
|
/// back-to-front withdraw clamped to availability. A bare world hosts an entity that owns the
|
||||||
|
/// <see cref="InventorySlot"/> buffer (a DynamicBuffer needs an entity); no systems run.
|
||||||
|
/// </summary>
|
||||||
|
public class InventoryMathTests
|
||||||
|
{
|
||||||
|
static (World world, DynamicBuffer<InventorySlot> buffer) MakeBuffer()
|
||||||
|
{
|
||||||
|
var world = new World("InventoryMathTest");
|
||||||
|
var em = world.EntityManager;
|
||||||
|
var e = em.CreateEntity();
|
||||||
|
var buffer = em.AddBuffer<InventorySlot>(e);
|
||||||
|
return (world, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Deposit_TopsUpExistingStack_ThenAppends_NewStack()
|
||||||
|
{
|
||||||
|
var (world, buf) = MakeBuffer();
|
||||||
|
using (world)
|
||||||
|
{
|
||||||
|
int r1 = InventoryMath.Deposit(buf, itemId: 1, count: 5, stackMax: 10, maxSlots: 4);
|
||||||
|
Assert.AreEqual(0, r1, "5 fits in one fresh stack.");
|
||||||
|
Assert.AreEqual(1, buf.Length);
|
||||||
|
|
||||||
|
int r2 = InventoryMath.Deposit(buf, itemId: 1, count: 8, stackMax: 10, maxSlots: 4);
|
||||||
|
Assert.AreEqual(0, r2, "8 more tops the first stack to 10 then appends 3.");
|
||||||
|
Assert.AreEqual(2, buf.Length, "A second stack is appended once the first fills.");
|
||||||
|
Assert.AreEqual(13, InventoryMath.CountOf(buf, 1));
|
||||||
|
Assert.AreEqual(10, buf[0].Count, "First stack is capped at stackMax.");
|
||||||
|
Assert.AreEqual(3, buf[1].Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Deposit_FillsMultipleStacks_UpToSlotCap_ReturnsRemainder()
|
||||||
|
{
|
||||||
|
var (world, buf) = MakeBuffer();
|
||||||
|
using (world)
|
||||||
|
{
|
||||||
|
// 2 slots * stackMax 10 = 20 capacity; depositing 25 leaves a remainder of 5.
|
||||||
|
int r = InventoryMath.Deposit(buf, itemId: 2, count: 25, stackMax: 10, maxSlots: 2);
|
||||||
|
Assert.AreEqual(5, r, "Past the slot cap, the overflow is returned as a remainder.");
|
||||||
|
Assert.AreEqual(2, buf.Length);
|
||||||
|
Assert.AreEqual(20, InventoryMath.CountOf(buf, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Withdraw_TakesAcrossStacks_BackToFront_Clamped_ReturnsTaken()
|
||||||
|
{
|
||||||
|
var (world, buf) = MakeBuffer();
|
||||||
|
using (world)
|
||||||
|
{
|
||||||
|
InventoryMath.Deposit(buf, itemId: 3, count: 25, stackMax: 10, maxSlots: 4); // 10,10,5
|
||||||
|
int taken = InventoryMath.Withdraw(buf, itemId: 3, count: 12);
|
||||||
|
Assert.AreEqual(12, taken);
|
||||||
|
Assert.AreEqual(13, InventoryMath.CountOf(buf, 3));
|
||||||
|
|
||||||
|
int takenAll = InventoryMath.Withdraw(buf, itemId: 3, count: 100);
|
||||||
|
Assert.AreEqual(13, takenAll, "Withdraw clamps to what is available.");
|
||||||
|
Assert.AreEqual(0, InventoryMath.CountOf(buf, 3));
|
||||||
|
Assert.AreEqual(0, buf.Length, "Emptied stacks are dropped.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Deposit_ZeroItemId_OrNonPositiveCount_AreNoOps()
|
||||||
|
{
|
||||||
|
var (world, buf) = MakeBuffer();
|
||||||
|
using (world)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(7, InventoryMath.Deposit(buf, itemId: 0, count: 7, stackMax: 10, maxSlots: 4),
|
||||||
|
"Depositing the empty id deposits nothing and returns the full count.");
|
||||||
|
Assert.AreEqual(0, InventoryMath.Deposit(buf, itemId: 1, count: 0, stackMax: 10, maxSlots: 4));
|
||||||
|
Assert.AreEqual(0, InventoryMath.Deposit(buf, itemId: 1, count: -3, stackMax: 10, maxSlots: 4));
|
||||||
|
Assert.AreEqual(0, buf.Length, "No rows were written.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: dbdfefb1a0acad849b84fadfb2938d9e
|
||||||
Reference in New Issue
Block a user