feat(physics): Stage 1 — CellArray ordered/deduped cell collection (retail CELLARRAY)

Ports retail CELLARRAY::add_cell (acclient_2013_pseudo_c.txt:701036): ordered list,
dedup by cell_id, append at end. The order is load-bearing for the verbatim
find_cell_list current-cell-first interior-wins pick (next commits) that fixes the
R1 cottage membership flap. Implements ICollection<uint> (helper-facing) +
IReadOnlyCollection<uint> (consumer-facing). 5 unit tests.

Also lands the membership-port pseudocode (workflow step 3) + the Stage-1 plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-03 08:54:45 +02:00
parent 1438d73a43
commit b44dd147bc
4 changed files with 992 additions and 0 deletions

View file

@ -0,0 +1,56 @@
using System.Collections;
using System.Collections.Generic;
namespace AcDream.Core.Physics;
/// <summary>
/// Ordered, deduped cell-id collection — a faithful model of retail's CELLARRAY
/// (<c>CELLARRAY::add_cell</c> @ <c>acclient_2013_pseudo_c.txt:701036</c>: linear
/// dedup by cell_id, append at the END, insertion order preserved). The order is
/// load-bearing for <c>CObjCell::find_cell_list</c>'s current-cell-first,
/// interior-wins pick (pc:308742) — the current cell is added at index 0 and the
/// pick iterates in order, so the current cell wins a boundary straddle and the
/// membership does not ping-pong (the R1 flap fix). Replaces the unordered
/// <see cref="HashSet{T}"/> the candidate build used to use.
///
/// <para>Implements <see cref="ICollection{T}"/> (so the candidate-building
/// helpers can take it where they used to take <c>HashSet&lt;uint&gt;</c>) and
/// <see cref="IReadOnlyCollection{T}"/> (so it satisfies the
/// <c>out IReadOnlyCollection&lt;uint&gt;</c> on <c>FindCellSet</c> and the
/// <c>CheckOtherCells</c> / diagnostics consumers). Enumeration is always
/// insertion order.</para>
/// </summary>
public sealed class CellArray : ICollection<uint>, IReadOnlyCollection<uint>
{
private readonly List<uint> _order = new();
private readonly HashSet<uint> _seen = new();
public int Count => _order.Count;
public bool IsReadOnly => false;
/// <summary>Ordered cell ids; index 0 is the cell added first (the current cell).</summary>
public IReadOnlyList<uint> OrderedIds => _order;
/// <summary>Append <paramref name="id"/> iff not already present (retail add_cell dedup).</summary>
public void Add(uint id)
{
if (_seen.Add(id))
_order.Add(id);
}
public bool Contains(uint id) => _seen.Contains(id);
public void Clear() { _order.Clear(); _seen.Clear(); }
public bool Remove(uint id)
{
if (!_seen.Remove(id)) return false;
_order.Remove(id);
return true;
}
public void CopyTo(uint[] array, int arrayIndex) => _order.CopyTo(array, arrayIndex);
public IEnumerator<uint> GetEnumerator() => _order.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _order.GetEnumerator();
}