using ProjectM.Simulation; using Unity.Burst; using Unity.Entities; using Unity.NetCode; namespace ProjectM.Server { /// /// Server-only, deterministic harvester production — the FRONT of the M7 auto-gather chain /// (Harvester → Conveyor → Fabricator). Each machine is a fixed-yield generator: /// every server ticks it deposits of its /// configured (byte) resource into its OWN server-only buffer (NOT the global /// ledger — a conveyor pulls it onward, or it sits buffered). Mirrors TurretFireSystem's exact /// now-extraction (NetworkTime.ServerTick.TickIndexForValidTick) + /// cooldown idiom, and runs in the plain server SimulationSystemGroup /// [UpdateAfter(PredictedSimulationSystemGroup)] (the predicted group is OrderFirst → UpdateBefore is /// ignored). Production mutates a DynamicBuffer in place (not a structural change) → no ECB needed. /// /// 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 /// awards floor((now-LastProcessedTick)/period) cycles, clamped /// to , 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. /// /// [BurstCompile] [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [UpdateInGroup(typeof(SimulationSystemGroup))] [UpdateAfter(typeof(PredictedSimulationSystemGroup))] public partial struct HarvesterProductionSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate(); state.RequireForUpdate(state.GetEntityQuery(ComponentType.ReadOnly())); } [BurstCompile] public void OnUpdate(ref SystemState state) { var serverTick = SystemAPI.GetSingleton().ServerTick; if (!serverTick.IsValid) return; uint now = serverTick.TickIndexForValidTick; foreach (var (ps, harvester, output) in SystemAPI.Query, RefRO, DynamicBuffer>()) { 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); } } } }