Stabilize lister type cache during thing movement
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,12 @@ public sealed class ThingsPrepatches : ClassWithFishPrepatches
|
|||||||
|
|
||||||
lock (__instance.LockObject())
|
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);
|
AddToDefList(__instance, t);
|
||||||
if (t is IHaulSource haulSource)
|
if (t is IHaulSource haulSource)
|
||||||
_haulSources(__instance).Add(haulSource);
|
_haulSources(__instance).Add(haulSource);
|
||||||
@@ -572,8 +578,12 @@ public sealed class Things : ClassWithFishPatches
|
|||||||
};
|
};
|
||||||
|
|
||||||
private static readonly MethodInfo _thingsListInitializerDefinition
|
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
|
public static unsafe IndexedFishSet<T> InitializeThingsList<T>() where T : Thing
|
||||||
#pragma warning disable CS8622
|
#pragma warning disable CS8622
|
||||||
=> new(static () => new(0,
|
=> new(static () => new(0,
|
||||||
|
|||||||
Reference in New Issue
Block a user