using Unity.Collections; using Unity.Mathematics; namespace ProjectM.Simulation { /// /// Pure, deterministic, ORDER-INDEPENDENT conveyor move resolver (the server ConveyorTransportSystem /// applies the result). Determinism: sources are processed sorted by (NEVER hashmap /// order); a destination belt cell accepts AT MOST ONE item and only if it was EMPTY in the pre-move snapshot /// (double-buffering -> exactly one cell/tick); ties break to the lowest-CellKey source and losers STALL with no /// loss; machine-input SINK cells always accept (a merge). World-free so it is exhaustively unit-tested. /// public static class ConveyorMath { /// Cardinal grid step for a belt direction byte (0=+X,1=-X,2=+Z,3=-Z). public static int2 DirOffset(byte dir) { switch (dir) { case 1: return new int2(-1, 0); case 2: return new int2(0, 1); case 3: return new int2(0, -1); default: return new int2(1, 0); // 0 = +X } } /// A stable, collision-free total order over grid cells (the deterministic tie-break key). public static long CellKey(int2 cell) => ((long)cell.x << 32) | (uint)cell.y; /// /// Resolve, for each belt holding an item, whether it advances one cell toward its direction this tick. /// Inputs are read-only snapshots; outputs are the accepted moves ( -> /// ), length . A move is accepted when the /// destination is a SINK cell (always, a merge) or an EMPTY, unclaimed belt cell. Sources are iterated in /// CellKey order so the result is identical regardless of input array order. Scratch is Temp + disposed. /// public static void ResolveMoves( NativeArray srcCells, NativeArray dirs, NativeArray itemRes, NativeArray itemCnt, NativeHashMap cellToIndex, NativeHashSet sinkCells, NativeArray outMoveDst, NativeArray outMoveSrcIdx, out int moveCount) { int n = srcCells.Length; moveCount = 0; // Stable iteration order = sources sorted by CellKey (insertion sort; n is small). var order = new NativeArray(n, Allocator.Temp); for (int i = 0; i < n; i++) order[i] = i; for (int i = 1; i < n; i++) { int cur = order[i]; long curKey = CellKey(srcCells[cur]); int j = i - 1; while (j >= 0 && CellKey(srcCells[order[j]]) > curKey) { order[j + 1] = order[j]; j--; } order[j + 1] = cur; } var claimed = new NativeHashSet(n, Allocator.Temp); for (int oi = 0; oi < n; oi++) { int i = order[oi]; if (itemCnt[i] <= 0) continue; // nothing to move int2 dst = srcCells[i] + DirOffset(dirs[i]); bool accept = false; bool isSink = sinkCells.Contains(dst); if (isSink) { accept = true; // sinks merge -> unlimited acceptors, never claimed } else if (cellToIndex.TryGetValue(dst, out int dstIdx)) { // dst is a belt cell: accept only if EMPTY in the snapshot AND not already claimed this tick. if (itemCnt[dstIdx] == 0 && !claimed.Contains(dst)) accept = true; } // else: dst is neither a belt nor a sink -> dead end -> stall. if (accept) { if (!isSink) claimed.Add(dst); outMoveDst[moveCount] = dst; outMoveSrcIdx[moveCount] = i; moveCount++; } } order.Dispose(); claimed.Dispose(); } } }