Stabilize lister type cache during thing movement

This commit is contained in:
2026-10-07 01:45:29 -04:00
parent e766ac833c
commit b6f255ebbb
2 changed files with 161 additions and 1 deletions
@@ -0,0 +1,150 @@
// Copyright (c) 2026 Bradson
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
using System.Collections;
using System.Runtime.CompilerServices;
namespace PerformanceFish.Listers;
// ListerThings contains object instances. An index map keeps removals constant-time without
// using FishTable's collision-chain relocation, which can fail during removal.
internal sealed class IndexedReferenceList<T> : IList<T>, IList, IReadOnlyList<T> where T : Thing
{
private readonly List<T> _items = [];
private readonly Dictionary<T, int> _indices = new(ReferenceComparer.Instance);
public T this[int index]
{
get => _items[index];
set
{
if (value is null)
throw new ArgumentNullException(nameof(value));
var previous = _items[index];
if (ReferenceEquals(previous, value))
return;
if (_indices.ContainsKey(value))
throw new InvalidOperationException("Thing is already in the type cache.");
_indices.Remove(previous);
_items[index] = value;
_indices.Add(value, index);
}
}
object? IList.this[int index]
{
get => this[index];
set => this[index] = RequireThing(value);
}
public int Count => _items.Count;
public bool IsReadOnly => false;
public bool IsFixedSize => false;
public bool IsSynchronized => false;
public object SyncRoot => this;
public void Add(T item)
{
if (item is null)
throw new ArgumentNullException(nameof(item));
if (_indices.ContainsKey(item))
return;
_indices.Add(item, _items.Count);
_items.Add(item);
}
int IList.Add(object? value)
{
var item = RequireThing(value);
if (_indices.TryGetValue(item, out var index))
return index;
Add(item);
return _items.Count - 1;
}
public bool Remove(T item)
{
if (item is null || !_indices.TryGetValue(item, out var index))
return false;
RemoveAt(index);
return true;
}
void IList.Remove(object? value)
{
if (value is T item)
Remove(item);
}
public void RemoveAt(int index)
{
var item = _items[index];
var lastIndex = _items.Count - 1;
var lastItem = _items[lastIndex];
_indices.Remove(item);
if (index != lastIndex)
{
_items[index] = lastItem;
_indices[lastItem] = index;
}
_items.RemoveAt(lastIndex);
}
public void Insert(int index, T item)
{
if (item is null)
throw new ArgumentNullException(nameof(item));
if ((uint)index > (uint)_items.Count)
throw new ArgumentOutOfRangeException(nameof(index));
if (_indices.ContainsKey(item))
return;
if (index == _items.Count)
{
Add(item);
return;
}
var displaced = _items[index];
_items.Add(displaced);
_items[index] = item;
_indices[displaced] = _items.Count - 1;
_indices.Add(item, index);
}
void IList.Insert(int index, object? value) => Insert(index, RequireThing(value));
public bool Contains(T item) => item is not null && _indices.ContainsKey(item);
bool IList.Contains(object? value) => value is T item && Contains(item);
public int IndexOf(T item) => item is not null && _indices.TryGetValue(item, out var index) ? index : -1;
int IList.IndexOf(object? value) => value is T item ? IndexOf(item) : -1;
public void Clear()
{
_indices.Clear();
_items.Clear();
}
public void CopyTo(T[] array, int arrayIndex) => _items.CopyTo(array, arrayIndex);
void ICollection.CopyTo(Array array, int index) => ((ICollection)_items).CopyTo(array, index);
public List<T>.Enumerator GetEnumerator() => _items.GetEnumerator();
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private static T RequireThing(object? value)
=> value is T item ? item : throw new ArgumentException("Expected a thing of the cached type.", nameof(value));
private sealed class ReferenceComparer : IEqualityComparer<T>
{
public static readonly ReferenceComparer Instance = new();
public bool Equals(T? x, T? y) => ReferenceEquals(x, y);
public int GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj);
}
}
+11 -1
View File
@@ -33,6 +33,12 @@ public sealed class ThingsPrepatches : ClassWithFishPrepatches
lock (__instance.LockObject())
{
// Re-registering the same thing must not duplicate the def and group lists.
if (__instance.IndexMapByDef().ContainsKey(t.GetKey())
&& __instance.listsByDef.TryGetValue(t.def, out var existing)
&& existing.Contains(t))
return;
AddToDefList(__instance, t);
if (t is IHaulSource haulSource)
_haulSources(__instance).Add(haulSource);
@@ -572,8 +578,12 @@ public sealed class Things : ClassWithFishPatches
};
private static readonly MethodInfo _thingsListInitializerDefinition
= methodof(InitializeThingsList<Thing>).GetGenericMethodDefinition();
= methodof(InitializeReferenceThingsList<Thing>).GetGenericMethodDefinition();
private static IndexedReferenceList<T> InitializeReferenceThingsList<T>() where T : Thing => new();
// Keep the public factory for mods that call it directly; the ListerThings type cache uses the
// reference-indexed implementation above.
public static unsafe IndexedFishSet<T> InitializeThingsList<T>() where T : Thing
#pragma warning disable CS8622
=> new(static () => new(0,