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,97 @@
|
||||
using Unity.Collections;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Pure, deterministic, ORDER-INDEPENDENT conveyor move resolver (the server <c>ConveyorTransportSystem</c>
|
||||
/// applies the result). Determinism: sources are processed sorted by <see cref="CellKey"/> (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.
|
||||
/// </summary>
|
||||
public static class ConveyorMath
|
||||
{
|
||||
/// <summary>Cardinal grid step for a belt direction byte (0=+X,1=-X,2=+Z,3=-Z).</summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A stable, collision-free total order over grid cells (the deterministic tie-break key).</summary>
|
||||
public static long CellKey(int2 cell) => ((long)cell.x << 32) | (uint)cell.y;
|
||||
|
||||
/// <summary>
|
||||
/// 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 (<paramref name="outMoveSrcIdx"/> ->
|
||||
/// <paramref name="outMoveDst"/>), length <paramref name="moveCount"/>. 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.
|
||||
/// </summary>
|
||||
public static void ResolveMoves(
|
||||
NativeArray<int2> srcCells, NativeArray<byte> dirs,
|
||||
NativeArray<int> itemRes, NativeArray<int> itemCnt,
|
||||
NativeHashMap<int2, int> cellToIndex, NativeHashSet<int2> sinkCells,
|
||||
NativeArray<int2> outMoveDst, NativeArray<int> 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<int>(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<int2>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user