Tests: EB-2 Charge spend + ledger-fed Fabricator (318/318 EditMode)
- TurretFireSystem: seed a Charge pool for existing tests; add soft-fail-when-dry, consume-one-Charge-per-shot, two-turrets-share-a-finite-pool. - FabricatorProductionSystem: ledger-fed withdraw/deposit, two machines split via the live in-loop read, and a catch-up affordability-clamp regression pin. See DR-033. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,11 +9,13 @@ namespace ProjectM.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Plain-Entities EditMode tests for the server-only <see cref="FabricatorProductionSystem"/> — the recipe machine
|
||||
/// that consumes <c>InAmount</c> of its input resource per run from its OWN <see cref="MachineInput"/> buffer and
|
||||
/// deposits <c>OutAmount * runs</c> into the GLOBAL resource ledger (resolved via <see cref="ResourceLedger"/>,
|
||||
/// never GetSingleton<StorageEntry>). Pins: it INITIALIZES on first touch without producing; it is strictly
|
||||
/// INPUT-LIMITED (runs = min(cycles, affordable) — no mint-from-nothing when the input slot is empty); it consumes
|
||||
/// exactly InAmount*runs from the input buffer; and catch-up after skipped ticks awards the exact capped amount.
|
||||
/// that consumes <c>InAmount</c> of its input resource per run and deposits <c>OutAmount * runs</c> into the GLOBAL
|
||||
/// resource ledger (resolved via <see cref="ResourceLedger"/>, never GetSingleton<StorageEntry>). Pins: it
|
||||
/// INITIALIZES on first touch without producing; it is strictly INPUT-LIMITED (runs = min(cycles, affordable) — no
|
||||
/// mint-from-nothing); it consumes exactly InAmount*runs; and catch-up after skipped ticks awards the exact capped
|
||||
/// amount. EB-2 adds the LEDGER-FED mode (<c>InputFromLedger != 0</c>): input is withdrawn from the shared ledger
|
||||
/// (read LIVE inside the loop so two ledger-fed machines split a finite pool correctly) instead of MachineInput —
|
||||
/// this is how mined Ore becomes turret-ammo Charge.
|
||||
/// </summary>
|
||||
public class FabricatorProductionSystemTests
|
||||
{
|
||||
@@ -39,7 +41,8 @@ namespace ProjectM.Tests
|
||||
em.SetComponentData(e, new NetworkTime { ServerTick = new NetworkTick(tick) });
|
||||
}
|
||||
|
||||
static Entity MakeFabricator(EntityManager em, byte inId, int inAmt, byte outId, int outAmt, int periodTicks, int seedInput)
|
||||
static Entity MakeFabricator(EntityManager em, byte inId, int inAmt, byte outId, int outAmt,
|
||||
int periodTicks, int seedInput, byte fromLedger = 0)
|
||||
{
|
||||
var e = em.CreateEntity();
|
||||
em.AddComponentData(e, new RegionTag { Region = RegionId.Base });
|
||||
@@ -56,6 +59,7 @@ namespace ProjectM.Tests
|
||||
OutResourceId = outId,
|
||||
OutAmount = outAmt,
|
||||
PeriodTicks = periodTicks,
|
||||
InputFromLedger = fromLedger,
|
||||
});
|
||||
var input = em.AddBuffer<MachineInput>(e);
|
||||
if (seedInput > 0)
|
||||
@@ -64,6 +68,12 @@ namespace ProjectM.Tests
|
||||
return e;
|
||||
}
|
||||
|
||||
static void SeedLedger(EntityManager em, Entity ledger, ushort itemId, int count)
|
||||
{
|
||||
var buf = em.GetBuffer<StorageEntry>(ledger);
|
||||
buf.Add(new StorageEntry { ItemId = itemId, Count = count });
|
||||
}
|
||||
|
||||
static int LedgerCount(EntityManager em, Entity ledger, ushort itemId)
|
||||
{
|
||||
var buf = em.GetBuffer<StorageEntry>(ledger);
|
||||
@@ -176,5 +186,75 @@ namespace ProjectM.Tests
|
||||
Assert.AreEqual(995, InputOf(em, f, ResourceId.Ore), "5 runs * InAmount(1) = 5 consumed (1000 - 5).");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- EB-2: ledger-fed Fabricator (mined Ore -> turret-ammo Charge) ----
|
||||
|
||||
[Test]
|
||||
public void LedgerFed_Withdraws_Input_From_Ledger_And_Deposits_Output()
|
||||
{
|
||||
var (world, group, ledger) = MakeWorld("FabLedgerFed", serverTick: 100);
|
||||
using (world)
|
||||
{
|
||||
var em = world.EntityManager;
|
||||
SeedLedger(em, ledger, ResourceId.Ore, 10);
|
||||
// InputFromLedger=1, MachineInput intentionally EMPTY (seedInput:0) — input must come from the ledger.
|
||||
var f = MakeFabricator(em, ResourceId.Ore, inAmt: 1, outId: ResourceId.Charge, outAmt: 3, periodTicks: 30, seedInput: 0, fromLedger: 1);
|
||||
|
||||
group.Update(); // init (NextTick -> 130)
|
||||
SetServerTick(world, 130);
|
||||
group.Update(); // one period: withdraw 1 Ore FROM the ledger, deposit 3 Charge
|
||||
|
||||
Assert.AreEqual(3, LedgerCount(em, ledger, ResourceId.Charge), "One run deposits OutAmount Charge into the ledger.");
|
||||
Assert.AreEqual(9, LedgerCount(em, ledger, ResourceId.Ore), "One run withdraws InAmount Ore FROM the ledger (10 - 1).");
|
||||
Assert.AreEqual(0, InputOf(em, f, ResourceId.Ore), "A ledger-fed Fabricator never touches its MachineInput buffer.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Two_LedgerFed_Fabricators_Split_A_Finite_Ledger_Via_The_Live_Read()
|
||||
{
|
||||
// 3 Ore in the ledger, two ledger-fed Fabricators each needing 2 Ore/run. The system reads the ledger
|
||||
// LIVE inside the loop, so the first withdraws 2 (Ore 3->1) and the second sees only 1 (floor(1/2)=0) and
|
||||
// starves. A HOISTED/stale read would let both see 3, both run, and drive the ledger negative.
|
||||
var (world, group, ledger) = MakeWorld("FabLedgerSplit", serverTick: 100);
|
||||
using (world)
|
||||
{
|
||||
var em = world.EntityManager;
|
||||
SeedLedger(em, ledger, ResourceId.Ore, 3);
|
||||
MakeFabricator(em, ResourceId.Ore, inAmt: 2, outId: ResourceId.Charge, outAmt: 1, periodTicks: 30, seedInput: 0, fromLedger: 1);
|
||||
MakeFabricator(em, ResourceId.Ore, inAmt: 2, outId: ResourceId.Charge, outAmt: 1, periodTicks: 30, seedInput: 0, fromLedger: 1);
|
||||
|
||||
group.Update(); // init both (NextTick -> 130)
|
||||
SetServerTick(world, 130);
|
||||
group.Update(); // one period: only ONE fab can afford 2 Ore from the shared pool
|
||||
|
||||
Assert.AreEqual(1, LedgerCount(em, ledger, ResourceId.Charge),
|
||||
"Only one of the two ledger-fed Fabricators affords a run from the shared 3 Ore — the live read prevents a double-spend.");
|
||||
Assert.AreEqual(1, LedgerCount(em, ledger, ResourceId.Ore), "2 of 3 Ore withdrawn; the ledger never goes negative.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LedgerFed_Runs_Are_Clamped_To_Affordable_Ledger_Under_CatchUp()
|
||||
{
|
||||
// 5 periods become due (150 ticks / 30), but the shared ledger only affords 3 runs (7 Ore / 2 per run).
|
||||
// Pins the LEDGER branch of runs = min(cyclesDue, affordable) as the strict binding minimum under catch-up.
|
||||
var (world, group, ledger) = MakeWorld("FabLedgerCatchUp", serverTick: 100);
|
||||
using (world)
|
||||
{
|
||||
var em = world.EntityManager;
|
||||
SeedLedger(em, ledger, ResourceId.Ore, 7);
|
||||
var f = MakeFabricator(em, ResourceId.Ore, inAmt: 2, outId: ResourceId.Charge, outAmt: 1, periodTicks: 30, seedInput: 0, fromLedger: 1);
|
||||
|
||||
group.Update(); // init at 100
|
||||
SetServerTick(world, 250); // floor(150/30) = 5 cycles due
|
||||
group.Update();
|
||||
|
||||
Assert.AreEqual(3, LedgerCount(em, ledger, ResourceId.Charge),
|
||||
"runs = min(cyclesDue=5, ledgerAffordable=floor(7/2)=3) = 3 — clamped by the LEDGER, not the cycle count.");
|
||||
Assert.AreEqual(1, LedgerCount(em, ledger, ResourceId.Ore), "3 runs withdraw 6 of 7 Ore from the ledger, leaving 1.");
|
||||
Assert.AreEqual(0, InputOf(em, f, ResourceId.Ore), "Ledger-fed mode never touches MachineInput.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user