f3f65bccbf
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>
98 lines
4.2 KiB
C#
98 lines
4.2 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|