M7 Automation: deterministic Harvester to Conveyor to Fabricator chains
Server-only production chains (never predicted): components + server systems + pure byte-only math (ProductionMath/ConveyorMath/MachineSlotMath), authoring + 3 machine prefabs wired into the Gameplay subscene, StructureCatalog rows, BuildPlace Direction/RuntimePlacedTag, Tuning, and 35 EditMode tests (catch-up gating, conveyor shuffle-invariance, SaveData v2 round-trip). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
using Unity.Mathematics;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Pure, deterministic catch-up + cooldown math shared by the M7 production systems (Harvester/Conveyor/
|
||||
/// Fabricator). No RNG/wall-clock -> server-authoritative. The single GATED catch-up path: a never-processed
|
||||
/// machine (<see cref="NeedsInit"/>) initializes first; a cooling machine yields 0; a due machine yields
|
||||
/// floor(elapsed/period) clamped to [0, maxCatchup]; period is guarded by max(1,...). Cooldown is persisted as
|
||||
/// REMAINING ticks (epoch-independent) so a save survives the server-tick origin reset on a fresh session.
|
||||
/// </summary>
|
||||
public static class ProductionMath
|
||||
{
|
||||
/// <summary>True for a never-processed machine (baked/just-placed) — initialize the baseline before producing.</summary>
|
||||
public static bool NeedsInit(uint lastProcessedTick) => lastProcessedTick == 0u;
|
||||
|
||||
/// <summary>
|
||||
/// Cycles to award THIS process. 0 if cooling (<paramref name="nextTick"/> newer than <paramref name="now"/>)
|
||||
/// or nothing elapsed; otherwise floor(elapsed/period) clamped to [0, <paramref name="maxCatchup"/>].
|
||||
/// <paramref name="nextTick"/>==0 is the inactive sentinel (never read as a future cooling tick). The lower
|
||||
/// bound is 0 (not 1): when genuinely due the NextTick gate guarantees elapsed>=period, so a sub-period
|
||||
/// edge (e.g. a freshly restored remaining==0 machine) floors to 0 rather than minting prematurely.
|
||||
/// <paramref name="period"/> is guarded by max(1,...) so a 0 never divides.
|
||||
/// </summary>
|
||||
public static int CyclesDue(NetworkTick now, uint nextTick, uint lastProcessedTick, int period, int maxCatchup)
|
||||
{
|
||||
int p = math.max(1, period);
|
||||
|
||||
if (nextTick != 0u)
|
||||
{
|
||||
var next = new NetworkTick(nextTick);
|
||||
if (next.IsValid && next.IsNewerThan(now))
|
||||
return 0; // still cooling down
|
||||
}
|
||||
|
||||
int since = now.TicksSince(new NetworkTick(TickUtil.NonZero(lastProcessedTick)));
|
||||
if (since <= 0)
|
||||
return 0;
|
||||
|
||||
return math.clamp(since / p, 0, maxCatchup);
|
||||
}
|
||||
|
||||
/// <summary>Remaining cooldown ticks to PERSIST (epoch-independent): 0 if inactive or already due, else nextTick-now.</summary>
|
||||
public static uint RemainingTicks(uint nextTick, uint nowTick) =>
|
||||
nextTick == 0u ? 0u : (nextTick > nowTick ? nextTick - nowTick : 0u);
|
||||
|
||||
/// <summary>Re-anchor a persisted remaining cooldown to the current tick origin on restore (NonZero-guarded).</summary>
|
||||
public static uint RestoreNextTick(uint nowTick, uint remaining) => TickUtil.NonZero(nowTick + remaining);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user