using ProjectM.Simulation; using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Mathematics; using Unity.NetCode; namespace ProjectM.Server { /// /// Server-only, deterministic conveyor transport — the MIDDLE of the M7 auto-gather chain /// (Harvester → Conveyor → Fabricator). Unlike the per-machine catch-up production systems, a conveyor is a /// single TRANSPORT STEP: each period-due, empty first PULLS one item off an adjacent /// upstream (the cell at myCell − DirOffset(dir) — i.e. the machine feeding /// INTO this belt) onto its own ; then every loaded conveyor advances its item /// EXACTLY one cell toward myCell + DirOffset(dir). The move resolution is delegated to the pure, /// unit-tested so determinism is provable WITHOUT a world: sources are /// processed sorted by (NOT hashmap order), occupancy is read from a /// pre-move double-buffer snapshot, a destination conveyor cell accepts at most one item (only if it was empty /// in the snapshot; ties → lowest CellKey wins, losers stall with no silent loss), and machine-input sink /// cells always accept (deposit). Sinks are a separate set so an item leaving the belt into a fabricator's /// never collides with belt occupancy. /// /// Mirrors TurretFireSystem's now-extraction (NetworkTime.ServerTick.TickIndexForValidTick) + /// cooldown idiom (each conveyor is period-gated the same way), and /// ResourceHarvestSystem's Temp-collection foreach idiom. Runs in the plain server /// SimulationSystemGroup [UpdateAfter(HarvesterProductionSystem)] (after harvesters deposit, so /// fresh output is pull-eligible this tick; before the fabricator consumes). All buffer/enableable mutation is /// in place (toggling an enableable bit is NOT a structural change) → no ECB. /// /// [BurstCompile] [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [UpdateInGroup(typeof(SimulationSystemGroup))] [UpdateAfter(typeof(HarvesterProductionSystem))] public partial struct ConveyorTransportSystem : 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; // ---- Snapshot every conveyor once (entity, cell, direction, item, period-due) ---- var convEntity = new NativeList(Allocator.Temp); var convCell = new NativeList(Allocator.Temp); var convDir = new NativeList(Allocator.Temp); var convItemRes = new NativeList(Allocator.Temp); // 0 = empty var convItemCnt = new NativeList(Allocator.Temp); // 0 = empty var convDue = new NativeList(Allocator.Temp); // period-gate satisfied this tick foreach (var (ps, conveyor, e) in SystemAPI.Query, RefRO>().WithEntityAccess()) { int period = math.max(1, conveyor.ValueRO.PeriodTicks); // Period-gate each conveyor through NextTick exactly like the production systems. A never-processed // belt initialises its baseline this tick and is NOT due (mirrors NeedsInit on the machines). bool due; if (ProductionMath.NeedsInit(ps.ValueRO.LastProcessedTick)) { ps.ValueRW.LastProcessedTick = TickUtil.NonZero(now); ps.ValueRW.NextTick = TickUtil.NonZero(now + (uint)period); due = false; } else { int cycles = ProductionMath.CyclesDue( serverTick, ps.ValueRO.NextTick, ps.ValueRO.LastProcessedTick, period, Tuning.MaxProductionCatchup); due = cycles > 0; if (due) { // A belt moves at most one cell per period; collapse any catch-up to a single step but keep // the baseline advancing so it re-evaluates next period. ps.ValueRW.LastProcessedTick = TickUtil.NonZero(now); ps.ValueRW.NextTick = TickUtil.NonZero(now + (uint)period); } } int res = 0, cnt = 0; if (SystemAPI.IsComponentEnabled(e)) { var item = SystemAPI.GetComponent(e); if (item.Count > 0) { res = item.ResourceId; cnt = item.Count; } } convEntity.Add(e); convCell.Add(ps.ValueRO.Cell); convDir.Add(conveyor.ValueRO.Direction); convItemRes.Add(res); convItemCnt.Add(cnt); convDue.Add(due); } int n = convEntity.Length; // Cell → conveyor snapshot index (belt occupancy map for ResolveMoves + the pull lookup). var cellToIndex = new NativeHashMap(n, Allocator.Temp); for (int i = 0; i < n; i++) cellToIndex.TryAdd(convCell[i], i); // duplicate cells can't occur (one structure per cell) // Sink cells = cells hosting a machine-input buffer (fabricators); these always accept a deposit. Map // each sink cell to its owning entity so an arriving item can be deposited into its MachineInput. var sinkCells = new NativeHashSet(8, Allocator.Temp); var sinkCellToEntity = new NativeHashMap(8, Allocator.Temp); foreach (var (ps, _, e) in SystemAPI.Query, DynamicBuffer>().WithEntityAccess()) { sinkCells.Add(ps.ValueRO.Cell); sinkCellToEntity.TryAdd(ps.ValueRO.Cell, e); } // Source cells = cells hosting a machine-OUTPUT buffer (harvesters/fabricators) a belt can pull from. // Built once so the pull phase is a single hash lookup per belt (no nested per-belt query). var outputCellToEntity = new NativeHashMap(8, Allocator.Temp); foreach (var (ps, _, e) in SystemAPI.Query, DynamicBuffer>().WithEntityAccess()) { outputCellToEntity.TryAdd(ps.ValueRO.Cell, e); } // ---- PULL: each empty, due belt draws one item off an adjacent UPSTREAM MachineOutput ---- // Upstream cell = myCell − DirOffset(dir): the machine feeding INTO this belt sits there. We pull a // single unit so a harvester's buffered output flows onto the belt one item per period. for (int i = 0; i < n; i++) { if (!convDue[i] || convItemCnt[i] > 0) continue; int2 srcCell = convCell[i] - ConveyorMath.DirOffset(convDir[i]); // The feeder must be a machine with a MachineOutput buffer (harvester or fabricator), NOT another // conveyor (belts hand off in the move phase, not via pull). Single hash lookup on the prebuilt map. if (!outputCellToEntity.TryGetValue(srcCell, out var feeder)) continue; var output = SystemAPI.GetBuffer(feeder); // Pull the first available resource row off the feeder (deterministic: first non-empty row order). byte pulledId = 0; for (int r = 0; r < output.Length; r++) { if (output[r].ResourceId != 0 && output[r].Count > 0) { pulledId = output[r].ResourceId; break; } } if (pulledId == 0) continue; int taken = MachineSlotMath.Withdraw(output, pulledId, 1); if (taken <= 0) continue; // Load the belt in the snapshot so it participates in THIS tick's move resolution. convItemRes[i] = pulledId; convItemCnt[i] = taken; } // ---- MOVE: resolve all belt advances from the pre-move (post-pull) snapshot, then apply ---- var srcCells = new NativeArray(n, Allocator.Temp); var dirs = new NativeArray(n, Allocator.Temp); var itemRes = new NativeArray(n, Allocator.Temp); var itemCnt = new NativeArray(n, Allocator.Temp); for (int i = 0; i < n; i++) { srcCells[i] = convCell[i]; dirs[i] = convDir[i]; // Pass the FULL post-pull occupancy (due AND non-due) so the resolver blocks a due belt from moving // INTO an occupied non-due cell. Non-due belts must still not ADVANCE themselves — that is enforced // after resolution by skipping any returned move whose source belt is not due this tick. itemRes[i] = convItemRes[i]; itemCnt[i] = convItemCnt[i]; } var outMoveDst = new NativeArray(n, Allocator.Temp); var outMoveSrcIdx = new NativeArray(n, Allocator.Temp); ConveyorMath.ResolveMoves( srcCells, dirs, itemRes, itemCnt, cellToIndex, sinkCells, outMoveDst, outMoveSrcIdx, out int moveCount); // Track which belts END this tick holding an item so we can settle enableable bits exactly once. var endRes = new NativeArray(n, Allocator.Temp); var endCnt = new NativeArray(n, Allocator.Temp); for (int i = 0; i < n; i++) { // Default: every snapshot item stays put (stalls / non-due / no valid move). Moves below override. endRes[i] = convItemRes[i]; endCnt[i] = convItemCnt[i]; } for (int m = 0; m < moveCount; m++) { int srcIdx = outMoveSrcIdx[m]; // A non-due belt contributes its occupancy to the snapshot (so due belts can't overrun it) but must // NOT advance its own item — skip its move and leave its item parked (the endRes/endCnt defaults). if (!convDue[srcIdx]) continue; int2 dst = outMoveDst[m]; int movRes = convItemRes[srcIdx]; int movCnt = convItemCnt[srcIdx]; // The source belt empties (its item left this cell). endRes[srcIdx] = 0; endCnt[srcIdx] = 0; if (sinkCells.Contains(dst)) { // Item leaves the belt network into a machine input slot. if (sinkCellToEntity.TryGetValue(dst, out var sinkEntity)) { var input = SystemAPI.GetBuffer(sinkEntity); MachineSlotMath.Deposit(input, (byte)movRes, movCnt); } } else if (cellToIndex.TryGetValue(dst, out int dstIdx)) { // Item advances onto the next belt cell (resolver guaranteed it was empty in the snapshot). endRes[dstIdx] = movRes; endCnt[dstIdx] = movCnt; } } // ---- Settle each conveyor's ConveyorItem to its end-of-tick state (single write per belt) ---- for (int i = 0; i < n; i++) { var e = convEntity[i]; if (endCnt[i] > 0) { SystemAPI.SetComponent(e, new ConveyorItem { ResourceId = (byte)endRes[i], Count = endCnt[i] }); SystemAPI.SetComponentEnabled(e, true); } else { SystemAPI.SetComponent(e, new ConveyorItem { ResourceId = 0, Count = 0 }); SystemAPI.SetComponentEnabled(e, false); } } convEntity.Dispose(); convCell.Dispose(); convDir.Dispose(); convItemRes.Dispose(); convItemCnt.Dispose(); convDue.Dispose(); cellToIndex.Dispose(); sinkCells.Dispose(); sinkCellToEntity.Dispose(); outputCellToEntity.Dispose(); srcCells.Dispose(); dirs.Dispose(); itemRes.Dispose(); itemCnt.Dispose(); outMoveDst.Dispose(); outMoveSrcIdx.Dispose(); endRes.Dispose(); endCnt.Dispose(); } } }