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,76 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-only, deterministic harvester production — the FRONT of the M7 auto-gather chain
|
||||
/// (Harvester → Conveyor → Fabricator). Each <see cref="Harvester"/> machine is a fixed-yield generator:
|
||||
/// every <see cref="Harvester.PeriodTicks"/> server ticks it deposits <see cref="Harvester.Yield"/> of its
|
||||
/// configured (byte) resource into its OWN server-only <see cref="MachineOutput"/> buffer (NOT the global
|
||||
/// ledger — a conveyor pulls it onward, or it sits buffered). Mirrors <c>TurretFireSystem</c>'s exact
|
||||
/// now-extraction (<c>NetworkTime.ServerTick.TickIndexForValidTick</c>) + <see cref="PlacedStructure.NextTick"/>
|
||||
/// cooldown idiom, and runs in the plain server <c>SimulationSystemGroup</c>
|
||||
/// <c>[UpdateAfter(PredictedSimulationSystemGroup)]</c> (the predicted group is OrderFirst → UpdateBefore is
|
||||
/// ignored). Production mutates a DynamicBuffer in place (not a structural change) → no ECB needed.
|
||||
/// <para>
|
||||
/// SINGLE GATED CATCH-UP PATH (offline-quit safe, NO wall-clock minting): a never-processed machine
|
||||
/// (LastProcessedTick==0) is initialised this tick and produces nothing; otherwise
|
||||
/// <see cref="ProductionMath.CyclesDue"/> awards <c>floor((now-LastProcessedTick)/period)</c> cycles, clamped
|
||||
/// to <see cref="Tuning.MaxProductionCatchup"/>, and the tick fields are re-stamped. All catch-up is
|
||||
/// WITHIN-SESSION tick math; the stockpile is preserved across quit by the persistence layer, never re-minted
|
||||
/// from a saved wall-clock.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
||||
[UpdateAfter(typeof(PredictedSimulationSystemGroup))]
|
||||
public partial struct HarvesterProductionSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<NetworkTime>();
|
||||
state.RequireForUpdate(state.GetEntityQuery(ComponentType.ReadOnly<Harvester>()));
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
|
||||
if (!serverTick.IsValid)
|
||||
return;
|
||||
uint now = serverTick.TickIndexForValidTick;
|
||||
|
||||
foreach (var (ps, harvester, output) in
|
||||
SystemAPI.Query<RefRW<PlacedStructure>, RefRO<Harvester>, DynamicBuffer<MachineOutput>>())
|
||||
{
|
||||
int period = harvester.ValueRO.PeriodTicks; // CyclesDue clamps to max(1, period)
|
||||
|
||||
// Never-processed (baked/just-placed) machine: initialise the catch-up baseline, produce nothing.
|
||||
if (ProductionMath.NeedsInit(ps.ValueRO.LastProcessedTick))
|
||||
{
|
||||
ps.ValueRW.LastProcessedTick = TickUtil.NonZero(now);
|
||||
ps.ValueRW.NextTick = TickUtil.NonZero(now + (uint)System.Math.Max(1, period));
|
||||
continue;
|
||||
}
|
||||
|
||||
int cycles = ProductionMath.CyclesDue(
|
||||
serverTick, ps.ValueRO.NextTick, ps.ValueRO.LastProcessedTick, period, Tuning.MaxProductionCatchup);
|
||||
if (cycles <= 0)
|
||||
continue; // still cooling down / nothing due
|
||||
|
||||
// Fixed-yield generation into the machine's own output slot (byte id; ledger conversion happens
|
||||
// only at the global-ledger boundary, which this machine never crosses directly).
|
||||
MachineSlotMath.Deposit(output, harvester.ValueRO.ResourceId, harvester.ValueRO.Yield * cycles);
|
||||
|
||||
uint p = (uint)System.Math.Max(1, period);
|
||||
ps.ValueRW.LastProcessedTick = TickUtil.NonZero(now);
|
||||
ps.ValueRW.NextTick = TickUtil.NonZero(now + p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user