diff --git a/Source/PerformanceFish/Listers/IndexedReferenceList.cs b/Source/PerformanceFish/Listers/IndexedReferenceList.cs new file mode 100644 index 0000000..4430e86 --- /dev/null +++ b/Source/PerformanceFish/Listers/IndexedReferenceList.cs @@ -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 : IList, IList, IReadOnlyList where T : Thing +{ + private readonly List _items = []; + private readonly Dictionary _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.Enumerator GetEnumerator() => _items.GetEnumerator(); + IEnumerator IEnumerable.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 + { + public static readonly ReferenceComparer Instance = new(); + public bool Equals(T? x, T? y) => ReferenceEquals(x, y); + public int GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj); + } +} diff --git a/Source/PerformanceFish/Listers/Things.cs b/Source/PerformanceFish/Listers/Things.cs index 000fec3..42dc9ff 100644 --- a/Source/PerformanceFish/Listers/Things.cs +++ b/Source/PerformanceFish/Listers/Things.cs @@ -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).GetGenericMethodDefinition(); + = methodof(InitializeReferenceThingsList).GetGenericMethodDefinition(); + private static IndexedReferenceList InitializeReferenceThingsList() 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 InitializeThingsList() where T : Thing #pragma warning disable CS8622 => new(static () => new(0,