Added gas grid optimization, various despawn optimizations, new hauling optimizations utilizing events and regions, bugfixes for old patches and for vanilla functions too, as well as smaller additions. 1.3 files removed.

This commit is contained in:
bbradson
2024-01-27 10:24:24 +01:00
parent 0247ad59a1
commit c8b764de68
151 changed files with 7318 additions and 2145 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+42
View File
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<PerformanceFish.GasDef>
<defName>BlindSmoke</defName>
<label>blind smoke</label>
<dissipationRate>4</dissipationRate>
<diffuses>false</diffuses>
<color>(200,200,200)</color>
</PerformanceFish.GasDef>
<PerformanceFish.GasDef>
<defName>ToxGas</defName>
<label>tox gas</label>
<dissipationRate>3</dissipationRate>
<diffuses>true</diffuses>
<color>(180,214,24)</color>
</PerformanceFish.GasDef>
<PerformanceFish.GasDef>
<defName>RotStink</defName>
<label>rot stink</label>
<dissipationRate>4</dissipationRate>
<diffuses>true</diffuses>
<color>(214,90,24)</color>
</PerformanceFish.GasDef>
<PerformanceFish.GasDef>
<defName>FirstTestGas</defName>
<label>first test gas</label>
<dissipationRate>2</dissipationRate>
<diffuses>false</diffuses>
<color>(229,52,235)</color>
</PerformanceFish.GasDef>
<PerformanceFish.GasDef>
<defName>SecondTestGas</defName>
<label>second test gas</label>
<dissipationRate>1</dissipationRate>
<diffuses>true</diffuses>
<color>(52,113,235)</color>
</PerformanceFish.GasDef>
</Defs>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<!-- MainButtonDef>
<defName>FISHAllocationProfiler</defName>
<label>profiler</label>
<description>Open the allocation profiler.</description>
<tabWindowClass>PerformanceFish.AllocationProfiling+MainTabWindow</tabWindowClass>
<canBeTutorDenied>false</canBeTutorDenied>
<order>79</order>
<validWithoutMap>true</validWithoutMap>
<minimized>True</minimized>
<iconPath>LogButton/Log</iconPath>
</MainButtonDef -->
</Defs>
-1
View File
@@ -3,7 +3,6 @@
<name>Performance Fish</name>
<packageId>bs.performance</packageId>
<supportedVersions>
<li>1.3</li>
<li>1.4</li>
</supportedVersions>
<author>bradson</author>
+2 -2
View File
@@ -7,9 +7,9 @@ using System.Linq;
namespace PerformanceFish;
public class AccessToolsCaching : ClassWithFishPatches
public sealed class AccessToolsCaching : ClassWithFishPatches
{
public class AllTypes : FishPatch
public sealed class AllTypes : FishPatch
{
public override string? Description { get; } = "Caches AccessTools.AllTypes to only recalculate when the "
+ "loaded assembly count changes. Improves load times.";
@@ -0,0 +1,207 @@
// Copyright (c) 2023 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/.
#if notWorking
using System.Linq;
using System.Reflection.Emit;
using JetBrains.Annotations;
namespace PerformanceFish;
[UsedImplicitly]
public sealed class AllocationProfiling : ClassWithFishPatches
{
public override bool RequiresLoadedGameForPatching => false;
public static bool Active
{
get => _active;
set
{
return;
_active = value;
FishPatch.Get<ObjectPatch>().Enabled = value;
}
}
public static FishTable<Type, TypeData> Data = new();
private static bool _active;
/// <summary>
/// The idea here was to patch object..ctor, fetch the type, count calls and get the allocation count for every
/// type by doing so, while ignoring structs which are expected to not construct as full object with type.
/// This however doesn't work. The IL method bodies of class constructors do contain calls to object..ctor, but
/// they appear to only go through for static constructor invocations and literal object construction. Everything
/// else somehow skips the call.
/// </summary>
public sealed class ObjectPatch : FishPatch
{
public override bool DefaultState => true;
public override MethodBase? TargetMethodInfo { get; }
= MainTabWindow._objectConstructor;
// = AccessTools.Constructor(typeof(NotAClass));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void P0stf1x(object __instance)
{
CallCount++;
ref var typeData = ref Data.GetOrAddReference(__instance.GetType());
typeData.Count++;
// if ((typeData.Count & 1023) != 0)
// return;
typeData.StackTraces.GetOrAddReference(StackTraceUtility.ExtractStackTrace())++;
}
public static CodeInstructions Transpiler(CodeInstructions instructions)
{
var codes = instructions.AsOrToList();
Log.Message(string.Join("\n", codes));
var retIndex = codes.FindIndex(static code => code.opcode == OpCodes.Ret);
if (retIndex == -1)
retIndex = codes.Count - 1;
codes.Insert(retIndex, new(OpCodes.Ldarg_0));
codes.Insert(retIndex + 1, new(OpCodes.Call, methodof(P0stf1x)));
return codes;
}
// ReSharper disable once VirtualMemberCallInConstructor
//private ObjectPatch() => TryPatch();
public static class NotAClass
{
}
}
public static ulong CallCount;
public record struct TypeData
{
public int Count;
public FishTable<string, int> StackTraces = new();
[UsedImplicitly]
public TypeData()
{
}
}
[UsedImplicitly]
public sealed class MainTabWindow : RimWorld.MainTabWindow
{
public MainTabWindow()
{
draggable = true;
resizeable = true;
absorbInputAroundWindow = false;
closeOnClickedOutside = false;
}
private GUIScope.ScrollViewStatus _scrollViewStatus = new();
private Listing_Standard _listing = new();
public override void PreOpen()
{
Active = true;
base.PreOpen();
}
public override void PostClose()
{
base.PostClose();
Active = false;
}
internal static MethodBase _objectConstructor
= (MethodBase)MethodBodyReader.GetInstructions(null, AccessTools.DeclaredConstructor(typeof(PreceptComp)))
.First(static code => code.opcode == OpCodes.Call
&& code.operand is MethodBase method
&& method.Name.Contains("ctor"))
.operand;
// = typeof(object).GetMembers(AccessTools.all).OfType<MethodBase>()
// .First(static member => member.Name.Contains("ctor"));
// = AccessTools.Constructor(typeof(ObjectPatch.NotAClass));
public override void DoWindowContents(Rect inRect)
{
absorbInputAroundWindow = false;
closeOnClickedOutside = false;
if (GUIHelper.CanSkip)
return;
Widgets.Label(inRect with { height = Text.LineHeight }, $"Data: {Data}, count: {
Data.Count.ToStringCached()}, callCount: {CallCount.ToString()}");
inRect.yMin += Text.LineHeight;
var objectPatches = Harmony.GetPatchInfo(_objectConstructor);
var patchWidgetLabel = $"Patches on {_objectConstructor.FullDescription()}: pre {
objectPatches?.Prefixes?.Select(static patch => patch.PatchMethod.FullDescription()).ToStringSafeEnumerable()}, post: {
objectPatches?.Postfixes?.Select(static patch => patch.PatchMethod.FullDescription()).ToStringSafeEnumerable()}, trans: {
objectPatches?.Transpilers?.Select(static patch => patch.PatchMethod.FullDescription()).ToStringSafeEnumerable()}, final: {
objectPatches?.Finalizers?.Select(static patch => patch.PatchMethod.FullDescription()).ToStringSafeEnumerable()}, Active? {
Active.ToString()}, Enabled? {FishPatch.Get<ObjectPatch>().Enabled.ToString()}";
var patchWidgetHeight = Text.CalcHeight(patchWidgetLabel, inRect.width);
Widgets.Label(inRect with { height = patchWidgetHeight }, patchWidgetLabel);
inRect.yMin += patchWidgetHeight;
var objectMembers = typeof(object).GetMembers(AccessTools.all).OfType<MethodBase>()
.Where(static member => member.Name.Contains("ctor"));
var objectMembersLabel = $"object members:\n{string.Join("\n --- ", objectMembers.Select(static member
=> member.FullDescription()))}";
var objectMembersHeight = Text.CalcHeight(objectMembersLabel, inRect.width);
Widgets.Label(inRect with { height = objectMembersHeight }, objectMembersLabel);
inRect.yMin += objectMembersHeight;
using var scrollView = new GUIScope.ScrollableListingStandard(inRect, _scrollViewStatus, _listing);
var listing = _listing;
using var keyValuePairsPooled = new PooledArray<KeyValuePair<Type, TypeData>>(Data);
keyValuePairsPooled.Sort(_typeDataComparer);
var keyValuePairs = keyValuePairsPooled.Array;
var typeCount = keyValuePairsPooled.Length;
for (var i = 0; i < typeCount; i++)
{
if (scrollView.TryCull(Text.LineHeight))
continue;
var kvp = keyValuePairs[i];
var labelRect = listing.GetRect(Text.LineHeight);
Widgets.Label(labelRect,
string.Concat(kvp.Key, " ", kvp.Value.Count.ToStringCached()));
if (Mouse.IsOver(labelRect))
{
var traces = kvp.Value.StackTraces;
using var traceArray = new PooledArray<KeyValuePair<string, int>>(traces);
traceArray.Sort(_traceComparer);
// listing.Label(traceArray.Array[0].Key);
listing.Label(traceArray.FirstOrDefault(static kvp => !kvp.Key.Contains("cctor")).Key ?? "NULL");
// TooltipHandler.TipRegion(labelRect,
// string.Join("\n", traceArray.Select(static kvp => kvp.Key)));
}
}
}
private static IComparer<KeyValuePair<Type, TypeData>> _typeDataComparer
= Comparer<KeyValuePair<Type, TypeData>>.Create(static (x, y) => x.Value.Count.CompareTo(y.Value.Count));
private static IComparer<KeyValuePair<string, int>> _traceComparer
= Comparer<KeyValuePair<string, int>>.Create(static (x, y) => x.Value.CompareTo(y.Value));
}
}
#endif
+28 -13
View File
@@ -15,17 +15,31 @@ public static class AnalyzerFixes
{
public static void Patch()
{
var modBase = AccessTools.Constructor(Reflection.Type("PerformanceAnalyzer", "Analyzer.Modbase"),
new[] { typeof(ModContentPack) });
if (modBase is null)
var entryPoint = AccessTools.DeclaredMethod(ModCompatibility.Types.PerformanceAnalyzer.Window_Analyzer,
"LoadEntries") as MethodBase;
var prefixEntryPoint = false;
if (entryPoint != null)
{
prefixEntryPoint = true;
}
else
{
entryPoint = AccessTools.DeclaredConstructor(ModCompatibility.Types.PerformanceAnalyzer.ModBase,
[typeof(ModContentPack)]);
}
if (entryPoint is null)
return /*false*/;
var success = PerformanceFishMod.Harmony!.Patch(modBase, postfix: new(methodof(XmlParser.CollectXmlData)))
!= null;
var entryPatchMethod = new HarmonyMethod(methodof(XmlParser.CollectXmlData));
var success = PerformanceFishMod.Harmony!.Patch(entryPoint, prefix: prefixEntryPoint ? entryPatchMethod : null,
postfix: prefixEntryPoint ? null : entryPatchMethod) != null;
var methodTransplanting = Reflection.MethodInfo("PerformanceAnalyzer", "Analyzer.Profiling.MethodTransplanting",
"UpdateMethod",
new[] { typeof(Type), typeof(MethodInfo) });
[typeof(Type), typeof(MethodInfo)]);
if (methodTransplanting is null
|| PerformanceFishMod.Harmony.Patch(methodTransplanting, transpiler: new(methodof(UpdateMethod_Transpiler)))
@@ -145,7 +159,7 @@ public static class AnalyzerFixes
public static class OverridingMethodUtility
{
public static HashSet<MethodInfo> PatchedOverrides = new();
public static HashSet<MethodInfo> PatchedOverrides = [];
public static void ClearCaches() => PatchedOverrides.Clear();
@@ -247,7 +261,7 @@ public static class AnalyzerFixes
public static CodeInstructions Parse_Transpiler(CodeInstructions CodeInstructions)
{
var codes = CodeInstructions.ToList();
var codes = CodeInstructions.AsOrToList();
var parseMethod = FishTranspiler.Call("PerformanceAnalyzer", "Analyzer.Profiling.XmlParser", "ParseMethod");
var errorMethod = FishTranspiler.Call("PerformanceAnalyzer", "Analyzer.Profiling.ThreadSafeLogger", "Error");
@@ -357,7 +371,7 @@ public static class AnalyzerFixes
private static Action<int, object, Type> MakeGUIController_AddEntryMethod()
{
var analyzer_Profiling_EntryType = Reflection.Type("PerformanceAnalyzer", "Analyzer.Profiling.Entry");
var analyzer_Profiling_EntryType = ModCompatibility.Types.PerformanceAnalyzer.ProfilingEntry;
if (analyzer_Profiling_EntryType == null)
Log.Error("analyzer_Profiling_EntryType is null");
@@ -378,13 +392,13 @@ public static class AnalyzerFixes
Log.Error("entry_Create is null");*/
var dictionary_Add = AccessTools.Method(
typeof(Dictionary<,>).MakeGenericType(new[] { analyzer_Profiling_EntryType, typeof(Type) }),
"Add", new[] { analyzer_Profiling_EntryType, typeof(Type) });
typeof(Dictionary<,>).MakeGenericType([analyzer_Profiling_EntryType, typeof(Type)]),
"Add", [analyzer_Profiling_EntryType, typeof(Type)]);
if (dictionary_Add == null)
Log.Error("dictionary_Add is null");
var dynamicMethod = new DynamicMethod("GUIController_AddEntryMethod", typeof(void),
new[] { typeof(int), typeof(object), typeof(Type) });
[typeof(int), typeof(object), typeof(Type)]);
var il = dynamicMethod.GetILGenerator();
il.Emit(FishTranspiler.Argument(0)); //Category
@@ -416,6 +430,7 @@ public static class AnalyzerFixes
//Copied from Wiri's analyzer fork, with above fixes integrated. The steam version lacks this functionality
public static class XmlParser
{
[SuppressMessage("Security", "CA3075")]
public static void CollectXmlData()
{
foreach (var dir in ModLister.AllActiveModDirs)
@@ -484,7 +499,7 @@ public static class AnalyzerFixes
var type = DynamicTypeBuilder_CreateType(node.Name, methods);
var entry = Entry_Create(node.Name, category, type, false, true);
if (tip != string.Empty)
if (!string.IsNullOrEmpty(tip))
Entry_tip(entry) = tip;
GUIController_AddSpecificEntry(category, entry, type);
@@ -0,0 +1,43 @@
// Copyright (c) 2023 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/.
namespace PerformanceFish.Cache;
public sealed class BitCellGrid
{
private long[] _innerArray;
private CellIndices _cellIndices;
public BitCellGrid(Map map)
{
_innerArray = null!;
_cellIndices = null!;
map.InvokeWhenCellIndicesReady(Initialize);
}
private void Initialize(Map map)
{
_cellIndices = map.cellIndices;
_innerArray = new long[(Math.Max(0, _cellIndices.NumGridCells - 1) >> 6) + 1];
}
public bool this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => (_innerArray[index >> 6] & (1L << (index & 63))) != 0L;
set
{
ref var bucket = ref _innerArray[index >> 6];
bucket ^= (-(long)value.AsInt() ^ bucket) & (1L << (index & 63));
}
}
public bool this[in IntVec3 c]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this[c.CellToIndex(_cellIndices)];
set => this[c.CellToIndex(_cellIndices)] = value;
}
}
+1 -1
View File
@@ -194,7 +194,7 @@ public class ByIndex<T_first, T_second, T_out> : ByIndex<T_first, ByIndex<T_seco
ResizeIfNecessary(firstIndex);
ref var inner = ref _items[firstIndex];
inner ??= new();
inner ??= [];
return inner;
}
+49 -16
View File
@@ -12,16 +12,51 @@ namespace PerformanceFish.Cache;
[PublicAPI]
public record struct ByInt<T, TResult> where T : notnull where TResult : new()
{
private static FishTable<ByInt<T, TResult>, TResult> _get
= Utility.AddNew<ByInt<T, TResult>, TResult>();
public int Key;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe ByInt(T key) => Key = FunctionPointers.IndexGetter<T>.Default(key);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ByInt(int key) => Key = key;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(ByInt<T, TResult> other) => Key == other.Key;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode() => Key;
private static object _valueInitializerLock = new();
private static FishTable<ByInt<T, TResult>, TResult> _get = InitializeNew();
[ThreadStatic]
private static FishTable<ByInt<T, TResult>, TResult>? _getThreadStatic;
private static Func<ByInt<T, TResult>, TResult>? _valueInitializer;
public static Func<ByInt<T, TResult>, TResult>? ValueInitializer
{
get
{
lock (_valueInitializerLock)
return _valueInitializer;
}
set
{
lock (_valueInitializerLock)
{
value ??= static _ => Reflection.New<TResult>();
_valueInitializer = value;
_get.ValueInitializer = value;
}
}
}
public static FishTable<ByInt<T, TResult>, TResult> Get
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _getThreadStatic ??= Utility.AddNew<ByInt<T, TResult>, TResult>();
get => _getThreadStatic ??= InitializeNew();
}
public static FishTable<ByInt<T, TResult>, TResult> GetDirectly
@@ -40,6 +75,15 @@ public record struct ByInt<T, TResult> where T : notnull where TResult : new()
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ref TResult GetOrAddReference(ref ByInt<T, TResult> key) => ref Get.GetOrAddReference(ref key);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TResult GetOrAdd(int key) => Get.GetOrAdd(Unsafe.As<int, ByInt<T, TResult>>(ref key));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TResult GetOrAdd(ByInt<T, TResult> key) => Get.GetOrAdd(key);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TResult GetOrAdd(ref ByInt<T, TResult> key) => Get.GetOrAdd(ref key);
[MethodImpl(MethodImplOptions.NoInlining)]
public static ref TResult GetExistingReference(int key)
=> ref Get.GetReference(Unsafe.As<int, ByInt<T, TResult>>(ref key));
@@ -47,19 +91,8 @@ public record struct ByInt<T, TResult> where T : notnull where TResult : new()
[MethodImpl(MethodImplOptions.NoInlining)]
public static ref TResult GetExistingReference(T key) => ref Get.GetReference(new(key));
public int Key;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe ByInt(T key) => Key = FunctionPointers.IndexGetter<T>.Default(key);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ByInt(int key) => Key = key;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(ByInt<T, TResult> other) => Key == other.Key;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode() => Key;
[MethodImpl(MethodImplOptions.NoInlining)]
private static FishTable<ByInt<T, TResult>, TResult> InitializeNew() => Utility.AddNew(ValueInitializer);
}
[PublicAPI]
+9 -6
View File
@@ -29,9 +29,9 @@ public static class ByMap<T> where T : new()
private static Func<Map, T> Initialize { get; }
= typeof(MapComponent).IsAssignableFrom(typeof(T))
? AccessTools.MethodDelegate<Func<Map, T>>(AccessTools.Method(typeof(Map), nameof(Map.GetComponent),
Type.EmptyTypes, new[] { typeof(T) }))
Type.EmptyTypes, [typeof(T)]))
// missing T constraints cause the compiler to complain when trying to just use map => map.GetComponent<T>
: static _ => new();
: static _ => Reflection.New<T>();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T GetFor(Map map)
@@ -39,7 +39,7 @@ public static class ByMap<T> where T : new()
var cache = GetCache;
for (var i = 0; i < cache.Count; i++)
{
ref var cacheEntry = ref cache.GetReferenceUnverifiable(i);
ref var cacheEntry = ref cache.GetReferenceUnsafe(i);
if (cacheEntry.map == map)
return cacheEntry.value;
@@ -59,7 +59,7 @@ public static class ByMap<T> where T : new()
var cache = GetCache;
for (var i = 0; i < cache.Count; i++)
{
ref var cacheEntry = ref cache.GetReferenceUnverifiable(i);
ref var cacheEntry = ref cache.GetReferenceUnsafe(i);
if (cacheEntry.map == map)
return ref cacheEntry.value;
@@ -74,7 +74,10 @@ public static class ByMap<T> where T : new()
}
private static bool EntryValid(int i, Map map)
=> Find.Maps.Count > i && Find.Maps.Contains(map);
{
var maps = Current.Game.Maps;
return maps.Count > i && maps.Contains(map);
}
private static T AddEntry(Map map, List<(Map map, T value)> cache)
{
@@ -85,6 +88,6 @@ public static class ByMap<T> where T : new()
private static ref T AddEntryByRef(Map map, List<(Map map, T value)> cache)
{
cache.Add((map, Initialize(map)));
return ref cache.GetReferenceUnverifiable(cache.Count - 1).value;
return ref cache.GetReferenceUnsafe(cache.Count - 1).value;
}
}
+45 -6
View File
@@ -134,7 +134,7 @@ public static class ByReferenceUnclearable<T, TResult>
Log.Error($"{e}");
}
return result ?? throw new();
return result ?? ThrowHelper.ThrowInvalidOperationException<TCacheResult>();
}
[SecuritySafeCritical]
@@ -153,6 +153,7 @@ public static class ByReferenceUnclearable<T, TResult>
cacheCopy.Task = null;
}
// ReSharper disable once SuspiciousTypeConversion.Global
// ReSharper disable once PatternNeverMatches
else if (centralCache.Task is Countdown taskCountdown)
{
Interlocked.Decrement(ref taskCountdown.Value);
@@ -216,7 +217,7 @@ public static class ByReferenceUnclearable<T, TResult>
Log.Error($"{e}");
}
return result ?? throw new();
return result ?? ThrowHelper.ThrowInvalidOperationException<TCacheResult>();
}
public static unsafe void Initialize()
@@ -239,17 +240,39 @@ public static class ByReferenceUnclearable<T, TResult>
public static class ByReference<T, TResult>
where T : notnull where TResult : new()
{
private static object _valueInitializerLock = new();
public static object SyncLock = new();
private static FishTable<T, TResult> _get = Utility.AddNew<T, TResult>();
private static FishTable<T, TResult> _get = InitializeNew();
[ThreadStatic]
private static FishTable<T, TResult>? _getThreadStatic;
private static Func<T, TResult>? _valueInitializer;
public static Func<T, TResult>? ValueInitializer
{
get
{
lock (_valueInitializerLock)
return _valueInitializer;
}
set
{
lock (_valueInitializerLock)
{
value ??= static _ => Reflection.New<TResult>();
_valueInitializer = value;
_get.ValueInitializer = value;
}
}
}
public static FishTable<T, TResult> Get
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _getThreadStatic ??= Utility.AddNew<T, TResult>();
get => _getThreadStatic ??= InitializeNew();
}
public static FishTable<T, TResult> GetDirectly
@@ -258,6 +281,12 @@ public static class ByReference<T, TResult>
get => _get;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TResult GetOrAdd(T key) => Get.GetOrAdd(key);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TResult GetOrAdd(ref T key) => Get.GetOrAdd(ref key);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ref TResult GetOrAddReference(T key) => ref Get.GetOrAddReference(key);
@@ -267,6 +296,12 @@ public static class ByReference<T, TResult>
[MethodImpl(MethodImplOptions.NoInlining)]
public static ref TResult GetExistingReference(T key) => ref Get.GetReference(key);
public static void Remove(T key)
{
Get.Remove(key);
GetDirectly.Remove(key);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Update<VResult>(T key) where VResult : TResult, ICacheable<T>
=> Unsafe.As<TResult, VResult>(ref Get.GetOrAddReference(ref key)).Update(ref key);
@@ -355,7 +390,7 @@ public static class ByReference<T, TResult>
Log.Error($"{e}");
}
return result ?? throw new();
return result ?? ThrowHelper.ThrowInvalidOperationException<TCacheResult>();
}
public static Task<TCacheResult> RequestFromCacheAsync<TCacheValue, TArgument2, TCacheResult>(T key, TArgument2 second)
@@ -406,7 +441,7 @@ public static class ByReference<T, TResult>
Log.Error($"{e}");
}
return result ?? throw new();
return result ?? ThrowHelper.ThrowInvalidOperationException<TCacheResult>();
}
public static unsafe void Initialize()
@@ -423,6 +458,9 @@ public static class ByReference<T, TResult>
typeof(ByReference<T, TResult>).FullName}\n{ex}");
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static FishTable<T, TResult> InitializeNew() => Utility.AddNew(ValueInitializer);
}
[PublicAPI]
@@ -528,6 +566,7 @@ public record struct ByReference<T1, T2, TResult> : IInitializable<T1, T2>
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
// ReSharper disable once ConvertToPrimaryConstructor
public ByReference(T1 first, T2 second)
{
First = first;
+46
View File
@@ -0,0 +1,46 @@
// Copyright (c) 2023 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.Diagnostics.CodeAnalysis;
namespace PerformanceFish.Cache;
public sealed class CellGrid<T>
{
private T[] _innerArray;
private CellIndices _cellIndices;
public CellGrid(Map map)
{
_innerArray = null!;
_cellIndices = null!;
map.InvokeWhenCellIndicesReady(Initialize);
}
[MemberNotNull(nameof(_innerArray), nameof(_cellIndices))]
private void Initialize(Map map)
{
_cellIndices = map.cellIndices;
_innerArray = new T[_cellIndices.NumGridCells];
}
public ref T this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => ref _innerArray[index];
}
public ref T this[in IntVec3 c]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => ref _innerArray[c.CellToIndex(_cellIndices)];
}
public ref T this[CellIndex cellIndex]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => ref _innerArray[cellIndex.Value];
}
}
+1 -1
View File
@@ -134,7 +134,7 @@ public interface IAsyncCacheable<in TKey, in T2, TResult> : IAsyncCacheable<TRes
public ValueTask<TResult?> MakeResultAsync(TKey key, T2 second);
}
internal record Countdown(int Value)
internal sealed record Countdown(int Value)
{
public volatile int Value = Value;
}
+95 -74
View File
@@ -5,16 +5,16 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection.Emit;
using JetBrains.Annotations;
using PerformanceFish.Events;
namespace PerformanceFish.Cache;
public static class Utility
{
public static event Action<Thing>? ThingDestroyed; // TODO
public static event Action? Cleared;
private static volatile int _destroyedThingCounter, _lastGCTick, _gcRate = 1000;
@@ -25,10 +25,20 @@ public static class Utility
private static object _gcLock = new();
public static void NotifyThingDestroyed(Thing thing)
{
ThingDestroyed?.Invoke(thing);
public static ConcurrentBag<ICollection> All { get; } = [];
public static void Clear()
{
foreach (var cache in All)
cache.Clear();
Cleared?.Invoke();
}
internal static void Initialize() => ThingEvents.Destroyed += TryContinueGC;
public static void TryContinueGC(Thing thing)
{
if (Interlocked.Increment(ref _destroyedThingCounter) < 100
|| Current.programStateInt != ProgramState.Playing
|| TickHelper.TicksGame - _lastGCTick < _gcRate)
@@ -77,48 +87,59 @@ public static class Utility
var count = 0;
for (var i = collection.Length; i-- > 0;)
{
if (collection.Length > 0)
if (collection[i].Count > 0)
count++;
}
return count;
}
public static ConcurrentBag<ICollection> All { get; } = new();
public static void Clear()
{
foreach (var cache in All)
cache.Clear();
Cleared?.Invoke();
}
public static void LogCurrentCacheUtilization()
{
var allCaches = All.ToArray();
Log.Message($"Total utilized caches: {allCaches.CountNonEmpty()}\n\n"
+ $"Individual counts:\n{string.Join("\n",
allCaches.Select(static cache => cache.GetType()).Distinct().Select(type
=> $"{type.FullDescription()} :: {allCaches.Where(cache => cache.GetType() == type)
.Select(static cache => cache.Count).Sum()}{TryCalculateSize(
type, allCaches)}"))}\n=============================================================");
var allCaches = All.Where(static c => c.Count > 0).ToArray();
var allCacheTypes = allCaches.Select(static cache => cache.GetType()).Distinct();
var cacheData = allCacheTypes.Select(type
=>
{
var cachesOfType = allCaches.Where(cache => cache.GetType() == type).ToList();
return (type, cachesOfType.Select(static cache => cache.Count).Sum(),
TryCalculateSize(type, cachesOfType));
}).ToArray();
Array.Sort(cacheData, static (x, y)
=> y.Item3 is { } size ? size.CompareTo(x.Item3 ?? 0L)
: y.Item2 != x.Item2 ? y.Item2.CompareTo(x.Item2)
: string.Compare(x.Item1.ToString(), y.Item1.ToString(), StringComparison.Ordinal));
Log.Message($"Total utilized caches: {allCaches.Length} ({
ToByteString(cacheData.Select(static tuple => tuple.Item3 ?? 0L).Sum())})\n\n"
+ $"Individual counts:\n{string.Join("\n", cacheData.Select(static tuple
=> $"{tuple.Item1.FullDescription()} :: {tuple.Item2.ToStringCached()}{
(tuple.Item3 is { } size ? $" ({ToByteString(size)})"
: "")}"))}\n=============================================================");
}
private static string TryCalculateSize(Type type, ICollection[] allCaches)
=> (type.GetProperty(nameof(FishTable<object, object>.SizeEstimate))?.GetMethod
private static long? TryCalculateSize(Type type, IEnumerable<ICollection> cachesOfType)
{
var sizeGetter = type.GetProperty(nameof(FishTable<object, object>.SizeEstimate))?.GetMethod
?? (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)
? AccessTools.Method(typeof(FisheryLib.CollectionExtensions),
nameof(FisheryLib.CollectionExtensions.GetSizeEstimate), generics: type.GetGenericArguments())
: null)) is { } sizeGetter
? $" ({(allCaches.Where(cache => cache.GetType() == type)
.Select(cache => (long)(uint)sizeGetter.Invoke(sizeGetter.IsStatic ? null : cache,
sizeGetter.IsStatic ? new object[] { cache } : null)).Sum()
is var sizeInBytes
&& sizeInBytes > 1024L * 1024L ? $"{Math.Round(sizeInBytes / (1024d * 1024d), 2)} MiB"
: sizeInBytes > 1024L ? $"{Math.Round(sizeInBytes / 1024d, 2)} KiB"
: $"{sizeInBytes} B")})"
: "";
: null);
return sizeGetter is null
? null
: cachesOfType.Select(cache
=> (long)(uint)sizeGetter.Invoke(sizeGetter.IsStatic ? null : cache,
sizeGetter.IsStatic ? [cache] : null)).Sum();
}
private static string ToByteString(long sizeInBytes)
=> sizeInBytes > 1024L * 1024L
? $"{Math.Round(sizeInBytes / (1024d * 1024d), 2).ToString(CultureInfo.CurrentCulture)} MiB"
: sizeInBytes > 1024L
? $"{Math.Round(sizeInBytes / 1024d, 2).ToString(CultureInfo.CurrentCulture)} KiB"
: $"{((int)sizeInBytes).ToStringCached()} B";
private static void Clear(this ICollection cache)
{
@@ -167,14 +188,29 @@ public static class Utility
}
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
public static FishTable<TKey, TValue> AddNew<TKey, TValue>(Action<KeyValuePair<TKey, TValue>>? onEntryAdded = null,
public static FishTable<TKey, TValue> AddNew<TKey, TValue>(Func<TKey, TValue>? valueInitializer = null,
Action<KeyValuePair<TKey, TValue>>? onEntryAdded = null,
Action<KeyValuePair<TKey, TValue>>? onEntryRemoved = null)
{
var newCollection = new FishTable<TKey, TValue>();
if (valueInitializer != null)
newCollection.ValueInitializer = valueInitializer;
if (onEntryAdded != null)
newCollection.EntryAdded += onEntryAdded;
if (typeof(TKey).IsAssignableTo(typeof(Thing)))
{
newCollection.EntryAdded
+= entry =>
{
var data = new OnDestroyActionData<TKey, TValue>(entry.Key, newCollection);
data.Action = data.Invoke;
data.Thing.Events().Destroyed += data.Action;
};
}
if (onEntryRemoved != null)
newCollection.EntryRemoved += onEntryRemoved;
@@ -182,6 +218,26 @@ public static class Utility
return newCollection;
}
private sealed record OnDestroyActionData<TKey, TValue>(TKey Key, FishTable<TKey, TValue> Collection)
{
public Action<Thing>? Action;
public Thing Thing => Unsafe.As<Thing>(Key);
public void Invoke(Thing thing)
{
Collection.Remove(Key);
Thing.Events().Destroyed -= Action;
}
}
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
public static FishTable<TKey, TValue> AddNew<TKey, TValue>(Func<FishTable<TKey, TValue>> collectionInitializer)
{
var newCollection = collectionInitializer();
All.Add(newCollection);
return newCollection;
}
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
public static T AddNewUnclearable<T>() where T : class, ICollection, new()
{
@@ -229,48 +285,13 @@ public interface IClearable
public void Clear();
}
public interface IFishBool
{
public bool Value
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get;
}
}
public struct FishBool : IFishBool
{
private byte _value;
public bool Value
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _value == 1;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set => _value = value ? (byte)1 : (byte)0;
}
public struct True : IFishBool
{
public bool Value => true;
}
public struct False : IFishBool
{
public bool Value => false;
}
}
[PublicAPI]
public class CollectionRef<T> : IClearable, ICollection where T : ICollection
public sealed class CollectionRef<T>(AccessTools.FieldRef<T> fieldRef) : IClearable, ICollection
where T : ICollection
{
private readonly AccessTools.FieldRef<T> _fieldRef;
private static readonly ClearInvoker? _clearInvoker = CreateClearDelegate();
public CollectionRef(AccessTools.FieldRef<T> fieldRef) => _fieldRef = fieldRef;
public ref T Get => ref _fieldRef();
public ref T Get => ref fieldRef();
public void Create() => Get = Reflection.New<T>();
@@ -304,7 +325,7 @@ public class CollectionRef<T> : IClearable, ICollection where T : ICollection
return null;
var dm = new DynamicMethod($"ClearInvoker_{typeof(T).FullName}", null,
new[] { typeof(T).MakeByRefType() }, typeof(CollectionRef<T>), true);
[typeof(T).MakeByRefType()], typeof(CollectionRef<T>), true);
var il = dm.GetILGenerator();
il.Emit(FishTranspiler.Argument(0));
+21
View File
@@ -0,0 +1,21 @@
// Copyright (c) 2023 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/.
namespace PerformanceFish;
public record struct CellIndex(int Value)
{
public int Value = Value;
public CellIndex(IntVec3 cell, Map map) : this(cell, map.cellIndices.mapSizeX) {}
public CellIndex(IntVec3 cell, int mapSizeX) : this(cell.CellToIndex(mapSizeX)) {}
public IntVec3 ToCell(Map map) => ToCell(map.cellIndices.mapSizeX);
public IntVec3 ToCell(int mapSizeX) => CellIndicesUtility.IndexToCell(Value, mapSizeX);
public List<Thing> GetThingList(Map map) => map.thingGrid.ThingsListAtFast(Value);
}
@@ -7,9 +7,9 @@ using PerformanceFish.Prepatching;
namespace PerformanceFish;
public class ContentFinderCaching : ClassWithFishPrepatches
public sealed class ContentFinderCaching : ClassWithFishPrepatches
{
public class Get_Patch : FishPrepatch
public sealed class Get_Patch : FishPrepatch
{
public override string? Description { get; }
= "Caches texture lookups on the ContentFinder. This has a relatively large performance impact when used "
+1 -1
View File
@@ -104,7 +104,7 @@ public static class DDSLoader
if ((dds_pxlf_dwFlags & DDPF_FOURCC) != 0)
{
// Texture dos not contain RGB data, check FourCC for format
// Texture does not contain RGB data, check FourCC for format
isCompressed = true;
if (FourCcEquals(dds_pxlf_dwFourCC, "DXT1"))
+56
View File
@@ -0,0 +1,56 @@
// Copyright (c) 2023 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/.
#if RAINBOW_ERRORS
using JetBrains.Annotations;
namespace PerformanceFish;
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public static class DebugActions
{
[DebugOutput("Performance Fish", true, name = "Toggle Rainbow Errors")]
public static void SpamErrors()
{
lock (_debugLock)
{
if (_errorSpamEnabled)
{
_errorSpamEnabled = false;
Application.onBeforeRender -= _errorAction;
return;
}
if (_random is null)
{
_random = new();
_random.NextBytes(_colorBytes = new byte[3000]);
}
_errorSpamEnabled = true;
Application.onBeforeRender += _errorAction;
}
}
private static readonly object _debugLock = new();
private static global::System.Random? _random;
private static bool _errorSpamEnabled;
private static byte[]? _colorBytes;
private static volatile int _logErrorCount = -1;
private static UnityEngine.Events.UnityAction _errorAction = () =>
{
var i = Interlocked.Increment(ref _logErrorCount);
if (i >= 1000)
{
_random!.NextBytes(_colorBytes!);
_logErrorCount = 0;
i = 0;
}
Log.Error("ERROR".Colorize(new ColorInt(_colorBytes![i], _colorBytes[i + 1000], _colorBytes[i + 2000])
.ToColor));
};
}
#endif
@@ -0,0 +1,38 @@
// Copyright (c) 2023 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 Mono.Cecil;
using Mono.Cecil.Cil;
using PerformanceFish.Prepatching;
namespace PerformanceFish;
public sealed class DeepResourceGridOptimization : ClassWithFishPrepatches
{
public sealed class AnyActiveDeepScannersOnMapPatch : FishPrepatch
{
public override string? Description { get; }
= "Optimizes lookups for active deep scanners to directly return the result of cached data instead of "
+ "iterating over all buildings on the map trying to find any matches";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(DeepResourceGrid), nameof(DeepResourceGrid.AnyActiveDeepScannersOnMap));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static bool ReplacementBody(DeepResourceGrid __instance)
{
foreach (var item in __instance.map.listerBuildings.Cache().ColonistDeepScanners)
{
var compDeepScanner = item.TryGetComp<CompDeepScanner>();
if (compDeepScanner != null && compDeepScanner.ShouldShowDeepResourceOverlay())
return true;
}
return false;
}
}
}
@@ -8,11 +8,11 @@
using Mono.Cecil.Cil;
using PerformanceFish.Prepatching;
namespace PerformanceFish;
namespace PerformanceFish.Defs;
public class DefDatabasePatches : ClassWithFishPrepatches
public sealed class DefDatabasePatches : ClassWithFishPrepatches
{
public class GetNamedPatch : FishPrepatch
public sealed class GetNamedPatch : FishPrepatch
{
public override MethodBase TargetMethodBase { get; } = methodof(DefDatabase<Def>.GetNamed);
@@ -36,7 +36,7 @@ public class DefDatabasePatches : ClassWithFishPrepatches
DefDatabase<T>.defsList.Count} defs of this type loaded.");
}
public class GetByShortHashPatch : FishPrepatch
public sealed class GetByShortHashPatch : FishPrepatch
{
public override MethodBase TargetMethodBase { get; } = methodof(DefDatabase<Def>.GetByShortHash);
@@ -0,0 +1,18 @@
// Copyright (c) 2023 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/.
namespace PerformanceFish.Defs;
public record struct DefThingPair(int DefShortHash, int ThingIDNumber)
{
public int
DefShortHash = DefShortHash,
ThingIDNumber = ThingIDNumber;
public bool Equals(DefThingPair other)
=> Unsafe.As<DefThingPair, ulong>(ref this) == Unsafe.As<DefThingPair, ulong>(ref other);
public override int GetHashCode() => HashCode.Combine(DefShortHash, ThingIDNumber);
}
@@ -5,7 +5,8 @@
/*using System.Linq;
namespace PerformanceFish;
namespace PerformanceFish.Defs;
[StaticConstructorOnStartup]
public static class FishDefOfOrNull
{
@@ -5,15 +5,15 @@
using RecipeUsersCache
= PerformanceFish.Cache.ByInt<Verse.RecipeDef,
PerformanceFish.RecipeDefCaching.AllRecipeUsers_Patch.RecipeUsersCacheValue>;
PerformanceFish.Defs.RecipeDefCaching.AllRecipeUsers_Patch.RecipeUsersCacheValue>;
namespace PerformanceFish;
namespace PerformanceFish.Defs;
public class RecipeDefCaching : ClassWithFishPatches
public sealed class RecipeDefCaching : ClassWithFishPatches
{
public override bool RequiresLoadedGameForPatching => true;
public class AllRecipeUsers_Patch : FirstPriorityFishPatch
public sealed class AllRecipeUsers_Patch : FirstPriorityFishPatch
{
public override string Description { get; }
= "Caches results of the AllRecipeUsers method. Becomes impactful with lots of unfinished things around, "
@@ -68,7 +68,7 @@ public class RecipeDefCaching : ClassWithFishPatches
}
else
{
RecipeUsers = new(result);
RecipeUsers = [..result];
}
_def = instance;
@@ -86,7 +86,7 @@ public class RecipeDefCaching : ClassWithFishPatches
}
}
/*public class PotentiallyMissingIngredients_Patch : FirstPriorityFishPatch
/*public sealed class PotentiallyMissingIngredients_Patch : FirstPriorityFishPatch
{
public override Expression<Action> TargetMethod { get; }
= () => new RecipeDef().PotentiallyMissingIngredients(null, null);
@@ -0,0 +1,112 @@
// Copyright (c) 2023 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.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;
using PerformanceFish.Prepatching;
using FieldAttributes = Mono.Cecil.FieldAttributes;
using MethodImplAttributes = Mono.Cecil.MethodImplAttributes;
namespace PerformanceFish.Defs;
public sealed class ThingDefPatches : ClassWithFishPrepatches
{
public const string FISH_HASH_FIELD_NAME = "FishHash";
public sealed class GetHashCodePatch : FishClassPrepatch
{
public override string? Description { get; }
= "Def.GetHashCode is overridden to return defName.GetHashCode, which is rather slow as that's a string. "
+ "This patch caches the hashcode in a field added through prepatcher and simply returns that instead.";
public override Type Type { get; } = typeof(BuildableDef);
public override void FreePatch(TypeDefinition typeDefinition)
{
var module = typeDefinition.Module;
var fishHashField = new FieldDefinition(FISH_HASH_FIELD_NAME, FieldAttributes.Private,
module.TypeSystem.Int32);
typeDefinition.Fields.Insert(0, fishHashField);
var constructorBody = typeDefinition.GetConstructors().First().Body;
constructorBody.GetILProcessor().InsertRange(constructorBody.Instructions.Count - 2,
OpCodes.Ldarg_0,
(OpCodes.Ldc_I4, -42),
(OpCodes.Stfld, fishHashField));
constructorBody.OptimizeMacros();
var getHashCodeBody = typeDefinition.GetMethod(nameof(BuildableDef.GetHashCode))!.Body;
var variables = getHashCodeBody.Variables;
variables.Clear();
var hashVariable = new VariableDefinition(module.TypeSystem.Int32);
variables.Add(hashVariable);
var instructions = getHashCodeBody.Instructions;
instructions.Clear();
var ilProcessor = getHashCodeBody.GetILProcessor();
ilProcessor.InsertRange(0,
OpCodes.Ldarg_0,
(OpCodes.Ldfld, fishHashField),
(OpCodes.Stloc, hashVariable),
(OpCodes.Ldloc, hashVariable),
(OpCodes.Ldc_I4, -42),
// 5 bne_un_s ^2
OpCodes.Ldarg_0,
(OpCodes.Call, methodof(CreateLongHash)),
OpCodes.Ret,
(OpCodes.Ldloc, hashVariable),
OpCodes.Ret);
ilProcessor.InsertAt(5, OpCodes.Bne_Un_S, instructions[^2]);
getHashCodeBody.MaxStackSize = 2;
getHashCodeBody.Method.ImplAttributes |= MethodImplAttributes.AggressiveInlining;
getHashCodeBody.OptimizeMacros();
}
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
// public static int ReplacementBody(BuildableDef instance)
// {
// var hash = instance.FishHash;
// return hash != -42 ? hash : CreateLongHash(instance);
// }
[MethodImpl(MethodImplOptions.NoInlining)]
public static int CreateLongHash(BuildableDef instance)
{
var defName = instance.defName;
if (defName is null)
return 0;
var defHash = defName.GetHashCode();
if (defName is "" or Def.DefaultDefName)
return defHash;
ref var cacheHash = ref Lazy.FishHashField(instance);
cacheHash = defHash != -42 ? defHash : defHash + 1;
return cacheHash;
}
private static class Lazy
{
internal static readonly AccessTools.FieldRef<BuildableDef, int> FishHashField
= AccessTools.FieldRefAccess<BuildableDef, int>(FISH_HASH_FIELD_NAME);
static Lazy()
{
// prevent beforefieldinit
}
}
}
}
@@ -16,9 +16,9 @@ using DesignationsOfDefCache =
namespace PerformanceFish;
public class DesignationManagerCaching : ClassWithFishPatches
public sealed class DesignationManagerCaching : ClassWithFishPatches
{
public class SpawnedDesignationsOfDef_Patch : FishPatch
public sealed class SpawnedDesignationsOfDef_Patch : FishPatch
{
public override string Description => "Designation caching";
@@ -62,7 +62,7 @@ public class DesignationManagerCaching : ClassWithFishPatches
dict[new(instance, key)] = cache;
}
public class AnySpawnedDesignationOfDef_Patch : FishPatch
public sealed class AnySpawnedDesignationOfDef_Patch : FishPatch
{
public override string Description => "Designation caching";
@@ -76,7 +76,7 @@ public class DesignationManagerCaching : ClassWithFishPatches
=> Reflection.GetCodeInstructions(AnySpawnedDesignationOfDef);
}
public class AllDesignationsOn_Patch : FishPatch
public sealed class AllDesignationsOn_Patch : FishPatch
{
public override string Description => "Designation caching";
public override Expression<Action> TargetMethod => () => default(DesignationManager)!.AllDesignationsOn(null);
@@ -99,7 +99,7 @@ public class DesignationManagerCaching : ClassWithFishPatches
}
}
public class DesignationOn_Patch : FishPatch
public sealed class DesignationOn_Patch : FishPatch
{
public override string Description => "Designation caching";
public override Expression<Action> TargetMethod => () => default(DesignationManager)!.DesignationOn(null);
@@ -111,7 +111,7 @@ public class DesignationManagerCaching : ClassWithFishPatches
=> Reflection.GetCodeInstructions(DesignationOn);
}
public class DesignationOn_ByDef_Patch : FishPatch
public sealed class DesignationOn_ByDef_Patch : FishPatch
{
public override string Description => "Designation caching";
public override Expression<Action> TargetMethod => () => default(DesignationManager)!.DesignationOn(null, null);
@@ -149,7 +149,7 @@ public class DesignationManagerCaching : ClassWithFishPatches
=> Reflection.GetCodeInstructions(DesignationOn);
}
public class HasMapDesignationOn_Patch : FishPatch
public sealed class HasMapDesignationOn_Patch : FishPatch
{
public override string Description => "Designation caching";
public override Expression<Action> TargetMethod => () => default(DesignationManager)!.HasMapDesignationOn(null);
@@ -0,0 +1,59 @@
// Copyright (c) 2023 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 PerformanceFish.Prepatching;
namespace PerformanceFish.Events;
public sealed class MapEvents : ClassWithFishPrepatches
{
public sealed class Instanced
{
public event Action<Map>?
ComponentsConstructed,
ComponentsLoaded;
internal void OnComponentsConstructed(Map map) => ComponentsConstructed?.Invoke(map);
internal void OnComponentsLoaded(Map map) => ComponentsLoaded?.Invoke(map);
}
public sealed class MapConstructComponentsPatch : FishPrepatch
{
public override string? Description { get; }
= "Hook to raise an event when map components finish constructing. Does nothing by itself, but certain "
+ "functions require this.";
public override bool Enabled => true;
public override bool ShowSettings => false;
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(Map), nameof(Map.ConstructComponents));
public static void Postfix(Map __instance) => __instance.Events().OnComponentsConstructed(__instance);
}
public sealed class MapExposeComponentsPatch : FishPrepatch
{
public override string? Description { get; }
= "Maps apparently construct all components twice when loading a save. Once in ConstructComponents and "
+ "afterwards another time here. This is the companion patch to MapConstructCompanions, raising the "
+ "ComponentsExposed event.";
public override bool Enabled => true;
public override bool ShowSettings => false;
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(Map), nameof(Map.ExposeComponents));
public static void Postfix(Map __instance)
{
if (Scribe.mode == LoadSaveMode.LoadingVars)
__instance.Events().OnComponentsLoaded(__instance);
}
}
}
@@ -0,0 +1,15 @@
// Copyright (c) 2023 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/.
namespace PerformanceFish.Events;
public static class StaticEvents
{
public static event Action CacheCleared
{
add => Cache.Utility.Cleared += value;
remove => Cache.Utility.Cleared -= value;
}
}
@@ -0,0 +1,167 @@
// Copyright (c) 2023 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 PerformanceFish.Prepatching;
namespace PerformanceFish.Events;
public sealed class ThingEvents : ClassWithFishPrepatches
{
public static event Action<Thing>?
Initialized,
Destroyed;
public sealed class Instanced
{
public event ThingGridEvents.EventHandler?
RegisteredAtThingGrid,
DeregisteredAtThingGrid;
public event Action<Thing, Map>? Spawned;
public event Action<Thing>?
DeSpawning,
DeSpawned,
Destroying,
Destroyed;
internal void OnSpawned(Thing thing, Map map) => Spawned?.Invoke(thing, map);
internal void OnDeSpawning(Thing thing) => DeSpawning?.Invoke(thing);
internal void OnDeSpawned(Thing thing) => DeSpawned?.Invoke(thing);
internal void OnDestroying(Thing thing) => Destroying?.Invoke(thing);
internal void OnDestroyed(Thing thing) => Destroyed?.Invoke(thing);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void OnRegisteredAtThingGrid(Thing thing, Map map, in IntVec3 cell)
=> RegisteredAtThingGrid?.Invoke(thing, map, cell);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void OnDeregisteredAtThingGrid(Thing thing, Map map, in IntVec3 cell)
=> DeregisteredAtThingGrid?.Invoke(thing, map, cell);
}
public sealed class SpawnSetupPatch : FishPrepatch
{
public override string? Description { get; }
= "Hook to invoke an event whenever things spawn. Does nothing by itself, but certain functions require "
+ "this.";
public override bool Enabled => true;
public override bool ShowSettings => false;
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(Thing), nameof(Thing.SpawnSetup));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Postfix(Thing __instance, Map map) => __instance.Events().OnSpawned(__instance, map);
}
public sealed class DeSpawnPatch : FishPrepatch
{
public override string? Description { get; }
= "Hook to invoke an event whenever things despawn. Does nothing by itself, but certain functions require "
+ "this.";
public override bool Enabled => true;
public override bool ShowSettings => false;
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(Thing), nameof(Thing.DeSpawn));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Prefix(Thing __instance) => __instance.Events().OnDeSpawning(__instance);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Postfix(Thing __instance) => __instance.Events().OnDeSpawned(__instance);
}
public sealed class DestroyPatch : FishPrepatch
{
public override string? Description { get; }
= "Hook to invoke an event whenever things get destroyed. Does nothing by itself, but certain functions "
+ "require this.";
public override bool Enabled => true;
public override bool ShowSettings => false;
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(Thing), nameof(Thing.Destroy));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Prefix(Thing __instance) => __instance.Events().OnDestroying(__instance);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Postfix(Thing __instance)
{
__instance.Events().OnDestroyed(__instance);
Destroyed?.Invoke(__instance);
}
}
public sealed class ExposeDataInitializePatch : FishPrepatch
{
public override string? Description { get; }
= "Hook to invoke an event when things finish loading from a save file. Does nothing by itself, but "
+ "certain functions require this.";
public override bool Enabled => true;
public override bool ShowSettings => false;
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(Thing), nameof(Thing.ExposeData));
public static void Postfix(Thing __instance)
{
if (Scribe.mode == LoadSaveMode.LoadingVars && __instance is not ThingWithComps)
OnThingInitialized(__instance);
}
}
public sealed class PostMakePatch : FishPrepatch
{
public override string? Description { get; }
= "Hook to invoke an event when things get created. Does nothing by itself, but certain functions require "
+ "this.";
public override bool Enabled => true;
public override bool ShowSettings => false;
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(Thing), nameof(Thing.PostMake));
public static void Postfix(Thing __instance)
{
if (__instance is not ThingWithComps)
OnThingInitialized(__instance);
}
}
public sealed class InitializeCompsPatch : FishPrepatch
{
public override string? Description { get; }
= "Hook to invoke an event when things with comps get initialized. Does nothing by itself, but certain "
+ "functions require this.";
public override bool Enabled => true;
public override bool ShowSettings => false;
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ThingWithComps), nameof(ThingWithComps.InitializeComps));
public static void Postfix(ThingWithComps __instance) => OnThingInitialized(__instance);
}
private static void OnThingInitialized(Thing thing) => Initialized?.Invoke(thing);
}
@@ -0,0 +1,74 @@
// Copyright (c) 2023 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 Mono.Cecil;
using Mono.Cecil.Cil;
using PerformanceFish.Prepatching;
namespace PerformanceFish.Events;
public sealed class ThingGridEvents : ClassWithFishPrepatches
{
/// <summary>
/// Invoked for every thing.Position change, including for motes and pawns
/// </summary>
public sealed class RegisterInCellPatch : FishPrepatch
{
public override string? Description { get; }
= "Hook to invoke an event whenever things register in a cell. Does nothing by itself, but certain "
+ "functions require this.";
public override bool Enabled => true;
public override bool ShowSettings => false;
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ThingGrid), nameof(ThingGrid.RegisterInCell));
public static void OnRegistered(ThingGrid __instance, Thing t, in IntVec3 c)
=> t.Events().OnRegisteredAtThingGrid(t, __instance.map, c);
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.InsertRange(ilProcessor.instructions.FirstIndexOf(static code
=> code.OpCode == OpCodes.Callvirt
&& code.Operand is MethodReference { Name: "Add" } method
&& method.DeclaringType.Name.Contains("List"))
+ 1,
OpCodes.Ldarg_0,
OpCodes.Ldarg_1,
(OpCodes.Ldarga_S, ilProcessor.Body.GetParameter(2)),
(OpCodes.Call, methodof(OnRegistered)));
}
public sealed class DeregisterInCellPatch : FishPrepatch
{
public override string? Description { get; }
= "Hook to invoke an event whenever things deregister from a cell. Does nothing by itself, but certain "
+ "functions require this.";
public override bool Enabled => true;
public override bool ShowSettings => false;
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ThingGrid), nameof(ThingGrid.DeregisterInCell));
public static void OnDeregistered(ThingGrid __instance, Thing t, in IntVec3 c)
=> t.Events().OnDeregisteredAtThingGrid(t, __instance.map, c);
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.InsertRange(ilProcessor.instructions.FirstIndexOf(static code
=> code.OpCode == OpCodes.Callvirt
&& code.Operand is MethodReference { Name: "Remove" } method
&& method.DeclaringType.Name.Contains("List"))
+ 1,
OpCodes.Ldarg_0,
OpCodes.Ldarg_1,
(OpCodes.Ldarga, ilProcessor.Body.GetParameter(2)),
(OpCodes.Call, methodof(OnDeregistered)));
}
public delegate void EventHandler(Thing thing, Map map, in IntVec3 cell);
}
+68 -10
View File
@@ -13,16 +13,22 @@ using PerformanceFish.Prepatching;
namespace PerformanceFish;
[PublicAPI]
public class FishSettings : ModSettings
public sealed class FishSettings : ModSettings
{
public static bool SettingsLoaded { get; private set; }
private const string VERSION_SCRIBE_LABEL = "Version";
public override void ExposeData()
{
base.ExposeData();
if (!TryScribeVersion())
goto DoneLoading;
Scribe_Values.Look(ref _threadingEnabled, "ThreadingEnabled");
Scribe_Values.Look(ref MothballEverything, "MothballEverything");
Scribe_Values.Look(ref ImproveHaulingAccuracy, "ImproveHaulingAccuracy", true);
if (PerformanceFishMod.AllPatchClasses is { } allPatchClasses)
{
@@ -36,9 +42,33 @@ public class FishSettings : ModSettings
patchClass.Patches.Scribe();
}
DoneLoading:
SettingsLoaded = true;
}
private static bool TryScribeVersion()
{
var currentVersion = PerformanceFishMod.LoadedVersion;
var lastSavedVersion = new Version().ToString();
var scribeMode = Scribe.mode;
if (scribeMode == LoadSaveMode.Saving)
{
lastSavedVersion = currentVersion.ToString();
Scribe_Values.Look(ref lastSavedVersion, VERSION_SCRIBE_LABEL);
}
else if (scribeMode == LoadSaveMode.LoadingVars)
{
var defaultVersion = lastSavedVersion;
Scribe_Values.Look(ref lastSavedVersion, VERSION_SCRIBE_LABEL);
if (new Version(lastSavedVersion ?? defaultVersion) < currentVersion)
return false;
}
return true;
}
public static void DoSettingsWindowContents(Rect inRect)
{
var windowRect = Find.WindowStack.currentlyDrawnWindow.windowRect;
@@ -76,6 +106,9 @@ public class FishSettings : ModSettings
{
try
{
if (!patchClass.ShowSettings)
continue;
ls.Label(patchClass.GetType().Name,
tooltip: patchClass is IHasDescription classWithDescription
? classWithDescription.Description
@@ -84,6 +117,9 @@ public class FishSettings : ModSettings
ls.GapLine(2f);
foreach (var patch in patchClass.Patches)
{
if (!patch.ShowSettings)
continue;
if (ShouldSkipForScrollView(inRect.height, Text.LineHeight, ls.curY, _scrollPosition.y))
{
ls.curY += Text.LineHeight;
@@ -91,14 +127,14 @@ public class FishSettings : ModSettings
}
var check = patch.Enabled;
if (patch.Description == null)
if (patch.DescriptionWithNotes == null)
Widgets.DrawHighlightIfMouseover(_scrollRect with { y = ls.curY, height = Text.LineHeight });
var label = patch.Name ?? patch.GetType().Name;
if (label.EndsWith("_Patch"))
label = label.Remove(label.Length - 6);
else if (label.EndsWith("Patch"))
label = label.Remove(label.Length - 5);
ls.CheckboxLabeled(label, ref check, patch.Description);
ls.CheckboxLabeled(label, ref check, patch.DescriptionWithNotes);
if (check != patch.Enabled)
patch.Enabled = check;
}
@@ -122,6 +158,9 @@ public class FishSettings : ModSettings
{
try
{
if (!patchClass.ShowSettings)
continue;
ls.Label(patchClass.GetType().Name,
// ReSharper disable once SuspiciousTypeConversion.Global
tooltip: patchClass is IHasDescription classWithDescription
@@ -131,6 +170,9 @@ public class FishSettings : ModSettings
ls.GapLine(2f);
foreach (var patch in patchClass.Patches)
{
if (!patch.ShowSettings)
continue;
if (ShouldSkipForScrollView(inRect.height, Text.LineHeight, ls.curY, _scrollPosition.y))
{
ls.curY += Text.LineHeight;
@@ -138,14 +180,14 @@ public class FishSettings : ModSettings
}
var check = patch.Enabled;
if (patch.Description == null)
if (patch.DescriptionWithNotes == null)
Widgets.DrawHighlightIfMouseover(_scrollRect with { y = ls.curY, height = Text.LineHeight });
var label = patch.Name ?? patch.GetType().Name;
if (label.EndsWith("_Patch"))
label = label.Remove(label.Length - 6);
else if (label.EndsWith("Patch"))
label = label.Remove(label.Length - 5);
ls.CheckboxLabeled(label, ref check, patch.Description);
ls.CheckboxLabeled(label, ref check, patch.DescriptionWithNotes);
if (check != patch.Enabled)
patch.Enabled = check;
}
@@ -158,6 +200,8 @@ public class FishSettings : ModSettings
}
}
ls.Gap();
ls.CheckboxLabeled("Improve hauling accuracy", ref ImproveHaulingAccuracy, ImproveHaulingAccuracyDescription);
ls.Gap();
ls.CheckboxLabeled("Mothball everything", ref MothballEverything, MothballEverythingDescription);
@@ -201,10 +245,16 @@ public class FishSettings : ModSettings
};
}
public static string MothballEverythingDescription
= MothballOptimization.BASIC_DESCRIPTION + " This setting removes all conditions, enabling every world "
public static string
MothballEverythingDescription
= MothballOptimization.BASIC_DESCRIPTION
+ " This setting removes all conditions, enabling every world "
+ "pawn for mothballing. Note, health conditions do not progress on mothballed pawns. Requires the "
+ "WorldPawnsDefPreventingMothball patch to apply";
+ "WorldPawnsDefPreventingMothball patch to apply",
ImproveHaulingAccuracyDescription
= "Requires the StoreUtilityPrepatches:TryFindBestBetterStoreCellForWorker patch to function. Having this "
+ "enabled makes hauling lookups more thorough, raising the odds of getting closer haul destinations at "
+ "the cost of a usually minor performance hit";
private static void ToggleAllPatches(bool newState) => ToggleAllPatches(_ => newState, _ => newState);
@@ -214,14 +264,20 @@ public class FishSettings : ModSettings
foreach (var patchClass in PerformanceFishMod.AllPatchClasses!)
{
foreach (var patch in patchClass.Patches)
{
if (patch.ShowSettings)
patch.Enabled = fishPatchFunc(patch);
}
}
foreach (var patchClass in PerformanceFishMod.AllPrepatchClasses!)
{
foreach (var patch in patchClass.Patches)
{
if (patch.ShowSettings)
patch.Enabled = fishPrepatchFunc(patch);
}
}
Cache.Utility.Clear();
}
@@ -259,7 +315,9 @@ public class FishSettings : ModSettings
private static bool _modEnabled = true;
public static bool MothballEverything;
public static bool
MothballEverything,
ImproveHaulingAccuracy = true;
public static bool ThreadingEnabled
{
@@ -275,7 +333,7 @@ public class FishSettings : ModSettings
}
private static bool _threadingEnabled;
public static List<FishPatch> ThreadingPatches { get; } = new();
public static List<FishPatch> ThreadingPatches { get; } = [];
private static Rect _scrollRect = new(0f, 0f, 500f, 9001f);
private static Vector2 _scrollPosition;
@@ -68,14 +68,15 @@ public static class IndexGetter
Delegate func;
while (!_indexFuncs.TryGetValue(type, out func!))
{
type = type.BaseType
?? throw new($"Performance Fish failed to find a fallback method to handle type {
type = type?.BaseType
?? ThrowHelper.ThrowInvalidOperationException<Type>(
$"Performance Fish failed to find a fallback method to handle type {
typeof(T).FullName} from mod {typeof(T).Assembly.GetName().Name}");
}
if (!isThing)
{
Log.Message($"Found fallback of type {type.FullName}.{
Log.Message($"Found fallback of type {type?.FullName ?? "NULL"}.{
(!isDef ? " This likely means we can safely continue from here." : "")}");
}
+25
View File
@@ -0,0 +1,25 @@
// Copyright (c) 2023 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 JetBrains.Annotations;
namespace PerformanceFish;
[PublicAPI]
public class GasDef : Def
{
public int dissipationRate;
public bool diffuses;
public Color color;
public static GasDef? OfGasType(GasType gasType)
=> gasType switch
{
GasType.BlindSmoke => GasDefOf.BlindSmoke,
GasType.RotStink => GasDefOf.RotStink,
GasType.ToxGas => GasDefOf.ToxGas,
_ => null
};
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright (c) 2023 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 JetBrains.Annotations;
namespace PerformanceFish;
[DefOf]
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
#pragma warning disable CS8618
public static class GasDefOf
{
public static GasDef
BlindSmoke,
ToxGas,
RotStink;
static GasDefOf() => DefOfHelper.EnsureInitializedInCtor(typeof(GasDefOf));
}
#pragma warning restore CS8618
@@ -0,0 +1,47 @@
// Copyright (c) 2023 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 JetBrains.Annotations;
namespace PerformanceFish;
[PublicAPI]
public static class GasGridExtensions
{
public static GasGridOptimization.ParallelGasGrid ForDef(this GasGridOptimization.ParallelGasGrid[] grids,
GasDef def)
=> grids[def.index];
public static GasGridOptimization.ParallelGasGrid ForDef(this GasGrid grid, GasDef def)
=> grid.ParallelGasGrids().ForDef(def);
public static void AddGas(this GasGrid grid, in IntVec3 cell, GasDef gas, int amount, bool canOverflow = true)
=> grid.ForDef(gas).AddGas(cell, amount, canOverflow);
public static void AddGas(this GasGrid grid, in IntVec3 cell, GasDef gas, float radius)
=> grid.AddGas(cell, gas, 255 * GenRadial.NumCellsInRadius(radius));
public static void SetDirect(this GasGrid grid, in IntVec3 cell, GasDef gas, byte density)
=> grid.ForDef(gas).SetDirect(cell, density);
public static byte DensityAt(this GasGrid grid, in IntVec3 cell, GasDef gas) => grid.ForDef(gas).DensityAt(cell);
public static float DensityPercentAt(this GasGrid grid, in IntVec3 cell, GasDef gas)
=> grid.DensityAt(cell, gas) / 255f;
/// <summary>
/// For modders. Performance Fish doesn't automatically scribe added custom defs.
/// </summary>
public static void ScribeForDef(this GasGrid grid, GasDef gas, string label)
{
if (gas == GasDefOf.BlindSmoke || gas == GasDefOf.ToxGas || gas == GasDefOf.RotStink)
{
Log.Error($"Tried scribing vanilla gas '{gas}'. This should never be manually scribed by a mod.");
return;
}
grid.ForDef(gas).Scribe(label);
}
}
@@ -0,0 +1,995 @@
// Copyright (c) 2023 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/.
// #define GAS_DEBUG_L1 // erroneous gas changes
// #define GAS_DEBUG_L2 // total gas grid changes
// #define GAS_DEBUG_L3 // individual cell changes
using System.Diagnostics;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
using MonoMod.Utils;
using PerformanceFish.Prepatching;
namespace PerformanceFish;
public sealed class GasGridOptimization : ClassWithFishPrepatches
{
// public static readonly List<ParallelGasGrid> ParallelGasGrids = new();
// public static GasGrid[] RegisteredGasGrids = Array.Empty<GasGrid>();
public sealed class SetDirectPatch : FishPrepatch
{
public override bool ShowSettings => false;
public override string? Description { get; }
= "Required by the gas grid optimization";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.SetDirect));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static void ReplacementBody(GasGrid __instance, int index, byte smoke, byte toxic, byte rotStink)
{
if (!ModsConfig.BiotechActive)
toxic = 0;
var gasGrids = __instance.ParallelGasGrids();
gasGrids[0].SetDirect(index, smoke);
gasGrids[1].SetDirect(index, toxic);
gasGrids[2].SetDirect(index, rotStink);
}
}
public sealed class TickPatch : FishPrepatch
{
public override List<Type> LinkedPatches { get; } =
[
typeof(SetDirectPatch), typeof(ColorAtPatch), typeof(AddGasPatch), typeof(AnyGasAtPatch),
typeof(DensityAtPatch), typeof(Debug_ClearAllPatch), typeof(Debug_FillAllPatch),
typeof(Notify_ThingSpawnedPatch), typeof(ExposeDataPatch), typeof(MouseOverReadOutOnGUI),
typeof(DebugToolsGeneralPushGas)
];
// public static EventWaitHandle? WaitHandle { get; private set; }
public static object? MonitorObject { get; private set; }
public override string? Description { get; }
= "Optimizes the gas grid to utilize highly performant bit manipulation for much faster processing of "
+ "cells, usually in buckets of 64 simultaneously checked cells, and threading to process the different "
+ "gas types in parallel. Additionally exposes gas to xml through the PerformanceFish.GasDef type. "
+ "Performance impact scales with map size, but is generally large. Currently only utilizing single core "
+ "parallelism, threading is planned";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.Tick));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static void ReplacementBody(GasGrid __instance)
{
// Parallel.For(0, ParallelGasGrids.Length, static i => ParallelGasGrids[i].Tick());
// (WaitHandle ?? Setup()).Set();
// if (!RegisteredGasGrids.Contains(__instance))
// Setup(__instance);
// ParallelNoAlloc.PulseRegisteredBackgroundWorkers(__instance.MonitorObject());
var gasGrids = __instance.ParallelGasGrids();
for (var i = 1; i < gasGrids.Length; i++)
gasGrids[i].Tick();
if (gasGrids.Length < 1)
return;
var firstGasGrid = gasGrids[0];
firstGasGrid.Tick();
__instance.cycleIndexDiffusion = firstGasGrid.CycleIndexDiffusion;
__instance.cycleIndexDissipation = firstGasGrid.CycleIndexDissipation;
}
// [MemberNotNull(nameof(WaitHandle))]
// public static EventWaitHandle Setup()
// {
// WaitHandle = new ManualResetEvent(false);
// for (var i = ParallelGasGrids.Length; i-- > 1;)
// {
// var gasGrid = ParallelGasGrids[i];
// ThreadPool.RegisterWaitForSingleObject(WaitHandle, (_, _) => gasGrid.Tick(), null, -1, false);
// }
//
// return WaitHandle;
// }
// public static ParallelGasGrid[] Setup(GasGrid instance)
// {
// // lock (ParallelGasGrids)
// // {
// // if (RegisteredGasGrids.Contains(instance))
// // {
// // // only possible if 2 threads try registering the same gas grid instance. Rimthreaded? Maybe throw?
// // SpinWait.SpinUntil(static () => MonitorObject != null);
// // #pragma warning disable CS8774
// // return;
// // #pragma warning restore CS8774
// // }
// //
// // RegisteredGasGrids = RegisteredGasGrids.Add(instance);
// // }
//
// return new ParallelGasGrid[DefDatabase<GasDef>.DefCount];
//
// var gasDefsList = DefDatabase<GasDef>.AllDefsListForReading;
//
// var newGasGrids = new ParallelGasGrid[gasDefsList.Count];
// try
// {
// for (var i = 0; i < newGasGrids.Length; i++)
// newGasGrids[i] = new(instance.map, gasDefsList[i]);
//
// // ParallelGasGrids.AddRangeFast(newGasGrids);
//
// instance.MonitorObject() = ParallelNoAlloc.RegisterBackgroundWaitingWorkers(MakeTickActions(newGasGrids,
// Math.Max(newGasGrids.Length - 1, 0), 1)); // 1st grid ticked on main thread
// }
// catch (Exception e)
// {
// Log.Error($"Caught exception in map with ID {instance.map.uniqueID} and hashCode {instance.map.GetHashCode()}: {e}");
// return Array.Empty<ParallelGasGrid>();
// }
//
// return newGasGrids;
// }
private static Action[] MakeTickActions<T>(T gasGrids, int count, int startIndex)
where T : IList<ParallelGasGrid>
{
var actions = new Action[count];
count += startIndex;
for (var i = startIndex; i < count; i++)
{
var gasGrid = gasGrids[i];
actions[i - startIndex] = () => gasGrid.Tick();
}
return actions;
}
// for save-game compatibility. The two cycle indices get scribed with ExposeData
public static void AdvanceCycleIndex(ref int cycleIndex, int area, int increment)
{
cycleIndex += increment;
if (cycleIndex >= area)
cycleIndex = 0;
}
}
public sealed class ColorAtPatch : FishPrepatch
{
public override bool ShowSettings => false;
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.ColorAt));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static Color ReplacementBody(GasGrid __instance, IntVec3 cell)
{
var cellIndex = cell.CellToIndex(__instance.map);
var gasGrids = __instance.ParallelGasGrids();
var gasGridCount = gasGrids.Length;
Span<float> densities = stackalloc float[gasGridCount];
var sum = 0f;
for (var i = 0; i < gasGridCount; i++)
sum += densities[i] = gasGrids[i].DensityAt(cellIndex);
var resultColor = default(Color);
for (var i = 0; i < gasGridCount; i++)
resultColor += gasGrids[i].Color * (densities[i] / sum);
resultColor.a = Mathf.Lerp(GasGrid.AlphaRange.min, GasGrid.AlphaRange.max,
sum / (/*gasGridCount*/ 3 * byte.MaxValue)); // maintain vanilla transparency
return resultColor;
}
}
public sealed class AddGasPatch : FishPrepatch
{
public override bool ShowSettings => false;
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.AddGas));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static void ReplacementBody(GasGrid __instance, IntVec3 cell, GasType gasType, int amount,
bool canOverflow = true)
=> __instance.ParallelGasGrids()[(int)gasType >> 3].AddGas(cell, amount, canOverflow);
}
public sealed class AnyGasAtPatch : FishPrepatch
{
public override bool ShowSettings => false;
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.AnyGasAt), [typeof(int)]);
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static bool ReplacementBody(GasGrid __instance, int idx)
{
var gasGrids = __instance.ParallelGasGrids();
for (var i = 0; i < gasGrids.Length; i++)
{
if (gasGrids[i].AnyGasAt(idx))
return true;
}
return false;
}
}
public sealed class DensityAtPatch : FishPrepatch
{
public override bool ShowSettings => false;
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.DensityAt),
[typeof(int), typeof(GasType)]);
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static byte ReplacementBody(GasGrid __instance, int index, GasType gasType)
=> __instance.ParallelGasGrids()[(int)gasType >> 3].DensityAt(index);
}
public sealed class Debug_ClearAllPatch : FishPrepatch
{
public override bool ShowSettings => false;
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.Debug_ClearAll));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static void ReplacementBody(GasGrid __instance)
{
var gasGrids = __instance.ParallelGasGrids();
for (var i = 0; i < gasGrids.Length; i++)
gasGrids[i].Clear();
__instance.map.mapDrawer.WholeMapChanged(MapMeshFlag.Gas);
}
}
public sealed class Debug_FillAllPatch : FishPrepatch
{
public override string? Description { get; } = "Adds custom gases to the debug fill all action";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.Debug_FillAll));
public static void Postfix(GasGrid __instance)
{
var gasGrids = __instance.ParallelGasGrids();
if (gasGrids.Length < 3)
return;
var cellCount = __instance.gasDensity.Length;
var cellIndices = __instance.map.cellIndices;
for (var i = 0; i < cellCount; i++)
{
if (!__instance.GasCanMoveTo(cellIndices.IndexToCell(i)))
continue;
for (var j = 3; j < gasGrids.Length; j++)
gasGrids[j].SetDirect(i, 255);
}
}
}
public sealed class Notify_ThingSpawnedPatch : FishPrepatch
{
public override bool ShowSettings => false;
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.Notify_ThingSpawned));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static void ReplacementBody(GasGrid __instance, Thing thing)
{
if (!thing.IsSpawned() || thing.def.Fillage != FillCategory.Full)
return;
var grids = __instance.ParallelGasGrids();
var gridCount = grids.Length;
var map = __instance.map;
var mapDrawer = map.mapDrawer;
var mapSizeX = GetMapSizeX(map);
foreach (var occupiedCell in thing.OccupiedRect())
{
var anyGridDirty = false;
for (var i = gridCount; i-- > 0;)
{
var grid = grids[i];
var cellIndex = occupiedCell.CellToIndex(mapSizeX);
if (!grid.AnyGasAt(cellIndex))
continue;
grid.SetDirect(cellIndex, 0);
anyGridDirty = true;
}
if (anyGridDirty)
mapDrawer.MapMeshDirty(occupiedCell, MapMeshFlag.Gas);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetMapSizeX(Map map) => map.cellIndices.mapSizeX;
}
public sealed class ExposeDataPatch : FishPrepatch
{
public override bool ShowSettings => false;
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.ExposeData));
public static void Prefix(GasGrid __instance)
{
if (Scribe.mode != LoadSaveMode.Saving)
return;
var grids = __instance.ParallelGasGrids();
var smokeGrid = grids[0];
var toxicGrid = grids[1];
var rotStinkGrid = grids[2];
for (var i = smokeGrid.CellCount; i-- > 0;)
{
__instance.gasDensity[i] = (rotStinkGrid.DensityAt(i) << 16)
| (toxicGrid.DensityAt(i) << 8)
| smokeGrid.DensityAt(i);
}
}
public static void Postfix(GasGrid __instance)
{
if (Scribe.mode != LoadSaveMode.LoadingVars)
return;
var grids = __instance.ParallelGasGrids();
var smokeGrid = grids[0];
var toxicGrid = grids[1];
var rotStinkGrid = grids[2];
for (var i = smokeGrid.CellCount; i-- > 0;)
{
var gasDensityAtCell = __instance.gasDensity[i];
smokeGrid.SetDirect(i, (byte)gasDensityAtCell);
toxicGrid.SetDirect(i, (byte)((uint)gasDensityAtCell >> 8));
rotStinkGrid.SetDirect(i, (byte)((uint)gasDensityAtCell >> 16));
}
}
}
public sealed class DebugToolsGeneralPushGas : FishPrepatch
{
public override string? Description { get; } = "Adds a debug action for adding custom gas defs";
public override MethodBase TargetMethodBase { get; } = methodof(DebugToolsGeneral.PushGas);
public static List<DebugActionNode> Postfix(List<DebugActionNode> __result)
{
var gasDefs = DefDatabase<GasDef>.AllDefsListForReading;
for (var i = 3; i < gasDefs.Count; i++)
{
var gasDef = gasDefs[i];
__result.Add(new(gasDef.LabelCap, DebugActionType.ToolMap,
() => Find.CurrentMap.gasGrid.AddGas(UI.MouseCell(), gasDef, 5f)));
}
return __result;
}
}
public sealed class MouseOverReadOutOnGUI : FishPrepatch
{
public override string? Description { get; }
= "Adds custom gas defs to the mouse over readout on the bottom left";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(MouseoverReadout), nameof(MouseoverReadout.MouseoverReadoutOnGUI));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
{
var drawGasMethod = AccessTools.DeclaredMethod(typeof(MouseoverReadout), nameof(MouseoverReadout.DrawGas));
var instructions = ilProcessor.instructions;
var lastDrawGasIndex = instructions.Count - instructions.Reverse().FirstIndexOf(instruction
=> instruction.Operand is MethodReference method && method.Is(drawGasMethod)) - 1;
var heightVariable = instructions[lastDrawGasIndex - 1];
if (heightVariable.OpCode != OpCodes.Ldloca_S && heightVariable.OpCode != OpCodes.Ldloca)
{
ThrowHelper.ThrowInvalidOperationException(
$"Failed to find height variable for MouseOverReadOutOnGUI patch. Expected Ldloca_S or "
+ $"Ldloca, got '{heightVariable}' instead.");
}
var cellVariable = instructions[lastDrawGasIndex - 4];
if (!cellVariable.OpCode.Name.Contains("ldloc", StringComparison.OrdinalIgnoreCase))
{
ThrowHelper.ThrowInvalidOperationException(
$"Failed to cell variable for MouseOverReadOutOnGUI patch. Expected Ldloc, got {
cellVariable} instead.");
}
ilProcessor.InsertRange(lastDrawGasIndex + 1,
cellVariable,
heightVariable,
(OpCodes.Call, methodof(DrawCustomGases)));
}
public static void DrawCustomGases(IntVec3 cell, ref float curYOffset)
{
var map = Find.CurrentMap;
var gasGrids = map.gasGrid.ParallelGasGrids();
var cellIndex = new CellIndex(cell, map);
for (var i = 3; i < gasGrids.Length; i++)
{
var gasGrid = gasGrids[i];
if (!gasGrid.AnyGasAt(cellIndex))
continue;
Widgets.Label(
new(MouseoverReadout.BotLeft.x, UI.screenHeight - MouseoverReadout.BotLeft.y - curYOffset, 999f,
999f),
string.Concat(gasGrid.GasDef.LabelCap, " ",
(gasGrid.DensityAt(cellIndex) / 255f).ToStringPercent("F0")));
curYOffset += 19f;
}
}
}
public class ParallelGasGrid
{
private ulong[] _gasCoverageForIndicesInRandomOrder;
private byte[] _gasDensity;
public event Action<ParallelGasGrid>? Ticked;
public GasDef GasDef { get; private set; }
public int DissipationRate => GasDef.dissipationRate;
public bool Diffuses => GasDef.diffuses;
public Color Color => GasDef.color;
public bool CalculateGasEffects => anyGasEverAdded;
public Map Map;
public int
CycleIndexDiffusion,
CycleIndexDissipation;
public IntVec3[] CardinalDirections;
public List<IntVec3> CellsInRandomOrder = null!;
public int[]
CellIndicesInRandomOrder = null!,
SourceIndicesOfRandomCells = null!;
public bool anyGasEverAdded;
private DissipationTicker _dissipationTicker;
private DiffusionTicker _diffusionTicker;
public int CellCount { get; private set; }
#pragma warning disable CS8618
public ParallelGasGrid(Map map, GasDef gasDef)
#pragma warning restore CS8618
=> map.InvokeWhenCellIndicesReady(m => Initialize(m, gasDef));
private void Initialize(Map map, GasDef gasDef)
{
GasDef = gasDef;
Map = map;
CellCount = map.cellIndices.NumGridCells;
_gasDensity = new byte[CellCount];
_gasCoverageForIndicesInRandomOrder = new ulong[((_gasDensity.Length - 1) >> 6) + 1];
CellIndicesInRandomOrder = new int[CellCount];
SourceIndicesOfRandomCells = new int[CellCount];
CardinalDirections = new IntVec3[GenAdj.CardinalDirections.Length];
Array.Copy(GenAdj.CardinalDirections, CardinalDirections, GenAdj.CardinalDirections.Length);
CycleIndexDiffusion = Rand.Range(0, map.Area >> 1);
_dissipationTicker = new() { GasGrid = this };
_diffusionTicker = new() { GasGrid = this };
Ticked += _onFirstTickAction;
}
private static Action<ParallelGasGrid> _onFirstTickAction = TickInitialize;
private static void TickInitialize(ParallelGasGrid grid)
{
// map.info is somehow null during ExposeData, which itself runs after CreateComponents and initializes a
// 2nd copy of every component for each map instance, of which apparently three get created per map, two
// of those thrown away, so cellsInRandomOrder.GetAll fails and throws when accessed from either
// CreateComponents or ExposeData while loading a game. A total of 6 instances of each component get
// created per map while loading it's stupid
// Also cellsInRandomOrder itself initializes after gasGrid within CreateComponents, but that's easy to
// solve
grid.CellsInRandomOrder = grid.Map.cellsInRandomOrder.GetAll();
grid.UpdateCellIndicesInRandomOrder();
grid.Ticked -= _onFirstTickAction;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int CellToIndex(in IntVec3 cell) => cell.CellToIndex(Map);
public void UpdateCellIndicesInRandomOrder()
{
if (Map.cellIndices.NumGridCells != CellCount)
{
Log.Error($"Map size changed from {CellCount} to {
Map.cellIndices.NumGridCells}. This is not supported.");
return;
}
var mapSizeX = Map.Size.x;
for (var i = CellIndicesInRandomOrder.Length; i-- > 0;)
{
SourceIndicesOfRandomCells[
CellIndicesInRandomOrder[i] = CellIndicesUtility.CellToIndex(CellsInRandomOrder[i], mapSizeX)] = i;
}
_gasCoverageForIndicesInRandomOrder.Clear();
for (var i = _gasDensity.Length; i-- > 0;)
{
if (_gasDensity[i] > 0)
SetDirect(i, _gasDensity[i]);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool AnyGasAt(int idx) => _gasDensity[idx] > 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool AnyGasAt(in IntVec3 cell) => AnyGasAt(CellToIndex(cell));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool AnyGasAt(CellIndex cellIndex) => AnyGasAt(cellIndex.Value);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte DensityAt(int index) => _gasDensity[index];
public byte DensityAt(in IntVec3 cell) => DensityAt(CellToIndex(cell));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte DensityAt(CellIndex cellIndex) => DensityAt(cellIndex.Value);
public void SetDirect(in IntVec3 cell, byte density) => SetDirect(CellToIndex(cell), density);
public void SetDirect(CellIndex cellIndex, byte density) => SetDirect(cellIndex.Value, density);
public void SetDirect(int index, byte density)
{
_gasDensity[index] = density;
index = SourceIndicesOfRandomCells[index];
ref var gasCoverageBucket = ref _gasCoverageForIndicesInRandomOrder[index >> 6];
var hasGas = density > 0;
gasCoverageBucket ^= ((ulong)-(long)hasGas.AsInt() ^ gasCoverageBucket) & (1UL << (index & 63));
anyGasEverAdded |= hasGas;
}
public void AddGas(CellIndex cellIndex, int amount, bool canOverflow = true)
=> AddGas(IndexToCell(cellIndex.Value), amount, canOverflow);
public void AddGas(in IntVec3 cell, int amount, bool canOverflow = true)
{
if (amount <= 0 || !GasCanMoveTo(cell))
return;
anyGasEverAdded = true;
var index = CellToIndex(cell);
SetDirect(index, AdjustedDensity(_gasDensity[index] + amount, out var overflow));
Map.mapDrawer.MapMeshDirty(cell, MapMeshFlag.Gas);
if (canOverflow && overflow > 0)
Overflow(cell, overflow);
}
private static byte AdjustedDensity(int newDensity, out int overflow)
{
if (newDensity > 255)
{
overflow = newDensity - 255;
return byte.MaxValue;
}
overflow = 0;
if (newDensity < 0)
return 0;
return (byte)newDensity;
}
private void Overflow(in IntVec3 cell, int amount)
{
if (amount <= 0)
return;
var remainingAmount = amount;
Map.floodFiller.FloodFill(cell, GasCanMoveTo, c =>
{
var num = Mathf.Min(remainingAmount, 255 - DensityAt(c));
if (num > 0)
{
AddGas(c, num, false);
remainingAmount -= num;
}
return remainingAmount <= 0;
}, GenRadial.NumCellsInRadius(40f), true);
}
public void Clear()
{
_gasCoverageForIndicesInRandomOrder.Clear();
_gasDensity.Clear();
anyGasEverAdded = false;
}
private abstract class BitwiseGasTicker
{
public required ParallelGasGrid GasGrid;
public abstract ref int CellCycleIndex { get; }
public void TickCells(int area, int cellCountToTick)
{
var cellCycleIndex = CellCycleIndex;
if ((cellCycleIndex & 63) != 0)
TickOddCells(area, ref cellCycleIndex);
var gasCoverageArray = GasGrid._gasCoverageForIndicesInRandomOrder;
cellCountToTick >>= 6;
cellCountToTick += 1;
for (var i = 0; i < cellCountToTick; i++)
{
if (area - cellCycleIndex < 64)
TickOddCells(area, ref cellCycleIndex);
var gasCoverage = gasCoverageArray[cellCycleIndex >> 6];
if (gasCoverage != default)
TickBucket(gasCoverage, cellCycleIndex);
cellCycleIndex += 64;
}
CellCycleIndex = cellCycleIndex;
}
private void TickBucket(ulong gasCoverage, int cellCycleIndex)
{
for (var i = 0; i < 64;)
{
var gasCoverageSlice = (gasCoverage >> i) & 0xFF;
if (gasCoverageSlice == default)
{
i += 8;
continue;
}
for (var j = 0; j < 8; j++)
{
if (((gasCoverageSlice >> j) & 1UL) != default)
TickingAction(cellCycleIndex + i);
i++;
}
}
}
public abstract void TickingAction(int cellCycleIndex);
[MethodImpl(MethodImplOptions.NoInlining)]
private void TickOddCells(int area, ref int cellCycleIndex)
{
if (cellCycleIndex < area)
{
var gasCoverage = GasGrid._gasCoverageForIndicesInRandomOrder[cellCycleIndex >> 6];
do
{
if ((cellCycleIndex & 63) == 0)
return;
if (((gasCoverage >> cellCycleIndex) & 1UL) != default)
TickingAction(cellCycleIndex);
cellCycleIndex++;
}
while (cellCycleIndex < area);
}
cellCycleIndex = 0;
}
}
private sealed class DissipationTicker : BitwiseGasTicker
{
public override ref int CellCycleIndex => ref GasGrid.CycleIndexDissipation;
public override void TickingAction(int cellCycleIndex)
=> GasGrid.DissipateGasAt(GasGrid.CellIndicesInRandomOrder[cellCycleIndex]);
}
private sealed class DiffusionTicker : BitwiseGasTicker
{
public override ref int CellCycleIndex => ref GasGrid.CycleIndexDiffusion;
public override void TickingAction(int cellCycleIndex)
=> GasGrid.DiffuseGasAt(GasGrid.CellsInRandomOrder[cellCycleIndex]);
}
public void Tick()
{
if (!CalculateGasEffects)
{
#if GAS_DEBUG_L1
if (CheckCurrentlyContainsGas())
{
anyGasEverAdded = true;
Debug.LogIncorrectAnyGasEverAdded(this);
}
#endif
return;
}
Ticked?.Invoke(this);
var area = Map.Area;
// CellsInRandomOrder = Map.cellsInRandomOrder.GetAll();
// UpdateCellIndicesInRandomOrder();
// Assigned in PostInitialize instead. Ludeon was likely doing this due to component construction order in
// Map.ConstructComponents
#if GAS_DEBUG_L2
using var previousDensity = new PooledArray<byte>(_gasDensity.Length);
Array.Copy(_gasDensity, 0, previousDensity.BackingArray, 0, _gasDensity.Length);
#endif
_dissipationTicker.TickCells(area, (area + 63) >> 6); // Mathf.CeilToInt(area * (1f / 64f))
if (Diffuses)
_diffusionTicker.TickCells(area, (area + 31) >> 5); // Mathf.CeilToInt(area * (1f / 32f))
#if GAS_DEBUG_L2
Debug.LogTotalGasChange(this, previousDensity);
#endif
if (Map.IsHashIntervalTick(600))
RecalculateEverHadGas();
}
[MethodImpl(MethodImplOptions.NoInlining)]
public void RecalculateEverHadGas() => anyGasEverAdded = CheckCurrentlyContainsGas();
private bool CheckCurrentlyContainsGas()
{
for (var i = _gasCoverageForIndicesInRandomOrder.Length; i-- > 0;)
{
if (_gasCoverageForIndicesInRandomOrder[i] != default)
return true;
}
return false;
}
private void DissipateGasAt(int index)
{
#if GAS_DEBUG_L1
var previousDensity = _gasDensity[index];
if (previousDensity == 0)
Debug.LogInvalidDissipationAttempt(this, index);
#endif
var densityAfterDissipation = Math.Max(_gasDensity[index] - DissipationRate, 0);
Debug.LogDissipationRate(this, index, densityAfterDissipation);
SetDirect(index, (byte)densityAfterDissipation);
#if GAS_DEBUG_L1
if (_gasDensity[index] >= previousDensity)
Debug.LogFailedDissipation(this, index, previousDensity);
#endif
if (densityAfterDissipation == 0)
{
Debug.LogMapMeshDirty(this, index);
Map.mapDrawer.MapMeshDirty(IndexToCell(index), MapMeshFlag.Gas);
}
}
private void DiffuseGasAt(IntVec3 cell)
{
var originCellIndex = CellToIndex(cell);
int densityAtOriginCell = _gasDensity[originCellIndex];
if (densityAtOriginCell < 17)
return;
var diffusedToAnyCell = false;
CardinalDirections.Shuffle();
for (var i = 0; i < CardinalDirections.Length; i++)
{
var otherCell = cell + CardinalDirections[i];
if (!GasCanMoveTo(otherCell))
continue;
var otherCellIndex = CellToIndex(otherCell);
int densityOtOtherCell = _gasDensity[otherCellIndex];
if (!TryDiffuseIndividualGas(ref densityAtOriginCell, ref densityOtOtherCell))
continue;
SetDirect(otherCellIndex, (byte)densityOtOtherCell);
Map.mapDrawer.MapMeshDirty(otherCell, MapMeshFlag.Gas);
diffusedToAnyCell = true;
if (densityAtOriginCell < 17)
break;
}
if (!diffusedToAnyCell)
return;
SetDirect(originCellIndex, (byte)densityAtOriginCell);
Map.mapDrawer.MapMeshDirty(cell, MapMeshFlag.Gas);
}
private static bool TryDiffuseIndividualGas(ref int gasA, ref int gasB)
{
if (gasA < 17)
return false;
var num = Mathf.Abs(gasA - gasB) >> 1;
if (gasA <= gasB || num < 17)
return false;
Debug.LogDiffusionRate(gasA, gasB, num);
gasA -= num;
gasB += num;
#if GAS_DEBUG_L1
if (gasA < 0 || gasB < 0)
Log.Error("Diffused below 0!");
#endif
return true;
}
public bool GasCanMoveTo(IntVec3 cell)
{
if (!cell.InBounds(Map))
return false;
if (cell.Filled(Map))
return cell.GetDoor(Map)?.Open ?? false;
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private IntVec3 IndexToCell(int index)
=> CellIndicesUtility.IndexToCell(index, Map.cellIndices.mapSizeX);
/// <summary>
/// For modders. Performance Fish doesn't automatically scribe added custom defs.
/// </summary>
public void Scribe(string label)
=> MapExposeUtility.ExposeUshort(Map,
cell => DensityAt(cell),
(cell, value) => SetDirect(cell, (byte)value),
label);
// ReSharper disable UnusedMember.Local
private static class Debug
{
[MethodImpl(MethodImplOptions.NoInlining)]
[Conditional("GAS_DEBUG_L1")]
internal static void LogIncorrectAnyGasEverAdded(ParallelGasGrid grid)
{
var gasDensities = grid._gasDensity;
var firstCellIndexWithGas = Array.FindIndex(gasDensities, static g => g > 0);
Log.Error($"GasGrid of type '{grid.GasDef.defName}' contains gas at cell '{
grid.IndexToCell(firstCellIndexWithGas)}' with index {
firstCellIndexWithGas}, but anyGasEverAdded=false. This index had a source index of {
grid.SourceIndicesOfRandomCells[firstCellIndexWithGas]} before randomization");
}
[MethodImpl(MethodImplOptions.NoInlining)]
[Conditional("GAS_DEBUG_L1")]
internal static void LogInvalidDissipationAttempt(ParallelGasGrid grid, int index)
=> Log.Error($"Tried to dissipate gas at cell '{
grid.IndexToCell(index)}' with density of 0");
[MethodImpl(MethodImplOptions.NoInlining)]
[Conditional("GAS_DEBUG_L1")]
internal static void LogFailedDissipation(ParallelGasGrid grid, int index, byte previousDensity)
=> Log.Error($"GasDensity went from '{previousDensity}' to '{grid._gasDensity[index]}' at cell '{
grid.IndexToCell(index)}' while trying to dissipate. It should've decreased");
[MethodImpl(MethodImplOptions.NoInlining)]
[Conditional("GAS_DEBUG_L2")]
internal static void LogTotalGasChange(ParallelGasGrid grid, PooledArray<byte> previousDensity)
=> Log.Message($"Total density for {grid.GasDef.defName} changed from {
previousDensity.Sum(static b => b)} to {grid._gasDensity.Sum(static b => b)}");
[Conditional("GAS_DEBUG_L3")]
internal static void LogMapMeshDirty(ParallelGasGrid grid, int index)
=> Log.Message($"Dirtied MapMesh for '{grid.GasDef.defName}' at '{index}'");
[Conditional("GAS_DEBUG_L3")]
internal static void LogDissipationRate(ParallelGasGrid grid, int index, int desiredResult)
=> Log.Message($"Dissipating {grid._gasDensity[index] - desiredResult} gas of type '{
grid.GasDef.defName}' at cell '{grid.IndexToCell(index)}' with target rate {
grid.DissipationRate}. Was {grid._gasDensity[index]}, becomes {desiredResult}");
[Conditional("GAS_DEBUG_L3")]
internal static void LogDiffusionRate(int gasA, int gasB, int num)
=> Log.Message($"Diffusing {num} gas from cell with density {gasA} to cell with density {gasB}");
}
// ReSharper restore UnusedMember.Local
}
}
@@ -10,9 +10,9 @@ using System.Reflection.Emit;
namespace PerformanceFish;
public class GenLocalDateCaching : ClassWithFishPatches
public sealed class GenLocalDateCaching : ClassWithFishPatches
{
public class DayTickByThing_Patch : FishPatch
public sealed class DayTickByThing_Patch : FishPatch
{
public override string Description { get; }
= "Caches results of GenLocalDate.DayTick for the first map. This is similar to Rim73's mind state "
@@ -60,7 +60,7 @@ public class GenLocalDateCaching : ClassWithFishPatches
//}
}
public class DayTickByMap_Patch : FishPatch
public sealed class DayTickByMap_Patch : FishPatch
{
public override string Description { get; }
= "Caches results of GenLocalDate.DayTick for the first map. This is similar to Rim73's mind state "
+6 -6
View File
@@ -10,9 +10,9 @@ using PerformanceFish.Prepatching;
namespace PerformanceFish;
public class GenTypesPatches : ClassWithFishPrepatches
public sealed class GenTypesPatches : ClassWithFishPrepatches
{
public class AllTypes : FishPrepatch
public sealed class AllTypes : FishPrepatch
{
public override string? Description { get; }
= "Fix for thread safety. The vanilla method was rarely breaking and preventing the game from loading";
@@ -29,7 +29,7 @@ public class GenTypesPatches : ClassWithFishPrepatches
public static void Postfix() => Monitor.Exit(Lock); // TODO: replace with finalizer
}
public class AllTypesWithAttribute : FishPrepatch
public sealed class AllTypesWithAttribute : FishPrepatch
{
public override string? Description { get; }
= "Fix for thread safety. The vanilla method was rarely breaking and preventing the game from loading";
@@ -62,7 +62,7 @@ public class GenTypesPatches : ClassWithFishPrepatches
=> static type => type.HasAttribute<TAttr>();
}
public class AllSubclasses : FishPrepatch
public sealed class AllSubclasses : FishPrepatch
{
public override string? Description { get; }
= "Fix for thread safety. The vanilla method was rarely breaking and preventing the game from loading";
@@ -94,14 +94,14 @@ public class GenTypesPatches : ClassWithFishPrepatches
public static Func<Type, bool> Predicate = new PredicateClass().Invoke;
public class PredicateClass
public sealed class PredicateClass
{
public Type? BaseType;
public bool Invoke(Type type) => type.IsSubclassOf(BaseType!);
}
}
public class AllSubclassesNonAbstract : FishPrepatch
public sealed class AllSubclassesNonAbstract : FishPrepatch
{
public override string? Description { get; }
= "Fix for thread safety, paired with a small optimization for faster loading. The vanilla method was "
@@ -10,9 +10,9 @@ using PerformanceFish.Prepatching;
namespace PerformanceFish;
public class GeneTrackerOptimization : ClassWithFishPrepatches
public sealed class GeneTrackerOptimization : ClassWithFishPrepatches
{
public class GeneTrackerTickPatch : FishPrepatch
public sealed class GeneTrackerTickPatch : FishPrepatch
{
public override string? Description { get; }
= "Every pawn has a gene tracker, which is responsible for ticking each of their genes. Normally it ticks "
@@ -41,7 +41,7 @@ public class GeneTrackerOptimization : ClassWithFishPrepatches
genesToTick[i].Tick();
}
if (instance.pawn.Spawned
if (instance.pawn.IsSpawned()
&& instance.Xenotype != XenotypeDefOf.Baseliner
&& instance.pawn.IsHashIntervalTick(300))
{
+134 -9
View File
@@ -10,9 +10,9 @@ using RimWorld.Planet;
namespace PerformanceFish;
public class GetCompCaching : ClassWithFishPrepatches
public sealed class GetCompCaching : ClassWithFishPrepatches
{
public class ThingCompPatch : FishPrepatch
public sealed class ThingCompPatch : FishPrepatch
{
public override string? Description { get; }
= "Optimizes GetComp lookups with a fast custom dictionary implementation. This is often more than 10x "
@@ -73,7 +73,132 @@ public class GetCompCaching : ClassWithFishPrepatches
}
}
public class HediffCompPatch : FishPrepatch
public sealed class ThingCompPropertiesPatch : FishPrepatch
{
public override string? Description { get; }
= "Optimizes GetCompProperties lookups with a fast custom dictionary implementation. This is often more "
+ "than 10x faster than the vanilla method. Replaces Performance Optimizer's error prone and slower "
+ "optimization.";
public override MethodBase TargetMethodBase { get; }
= AccessTools.Method(typeof(ThingDef), nameof(ThingDef.GetCompProperties));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(GetCompProperties<CompProperties>);
public static T? GetCompProperties<T>(ThingDef thingDef) where T : CompProperties
{
if (thingDef.comps != null)
{
ref var cache = ref Cache.ByInt<ThingDef, CacheValue<T>>.GetOrAddReference(thingDef.shortHash);
return !cache.IsDirty(thingDef.comps) ? cache.Comp : UpdateCache(thingDef, ref cache);
}
return null;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static T? UpdateCache<T>(ThingDef thingDef, ref CacheValue<T> cache)
where T : CompProperties
{
for (var i = 0; i < thingDef.comps.Count; i++)
{
if (thingDef.comps[i] is not T comp)
continue;
cache.Update(thingDef, comp);
return comp;
}
cache.Update(thingDef, null);
return null;
}
public record struct CacheValue<T> where T : CompProperties
{
private int _listVersion = -1;
public T? Comp;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty(List<CompProperties> comps) => comps._version != _listVersion;
[MethodImpl(MethodImplOptions.NoInlining)]
public void Update(ThingDef thingDef, T? comp)
{
if (thingDef.shortHash == default)
return;
_listVersion = thingDef.comps._version;
Comp = comp;
}
public CacheValue() {}
}
}
public sealed class ThingDefHasCompPatch : FishPrepatch
{
public override string? Description { get; }
= "Optimizes HasComp lookups with a fast custom dictionary implementation. This is often more than 10x "
+ "faster than the vanilla method. Replaces Performance Optimizer's error prone and slower optimization.";
public override MethodBase TargetMethodBase { get; }
= AccessTools.Method(typeof(ThingDef), nameof(ThingDef.HasComp));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(HasComp);
public static bool HasComp(ThingDef thingDef, Type compType)
{
if (thingDef.comps != null)
{
ref var cache
= ref Cache.ByReference<ushort, RuntimeTypeHandle, CacheValue>.GetOrAddReference(thingDef.shortHash,
compType.TypeHandle);
return !cache.IsDirty(thingDef.comps) ? cache.HasComp : UpdateCache(thingDef, compType, ref cache);
}
return false;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool UpdateCache(ThingDef thingDef, Type compType, ref CacheValue cache)
{
for (var i = 0; i < thingDef.comps.Count; i++)
{
if (thingDef.comps[i].compClass != compType)
continue;
cache.Update(thingDef, true);
return true;
}
cache.Update(thingDef, false);
return false;
}
public record struct CacheValue
{
private int _listVersion = -1;
public bool HasComp;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty(List<CompProperties> comps) => comps._version != _listVersion;
[MethodImpl(MethodImplOptions.NoInlining)]
public void Update(ThingDef thingDef, bool hasComp)
{
if (thingDef.shortHash == default)
return;
_listVersion = thingDef.comps._version;
HasComp = hasComp;
}
public CacheValue() {}
}
}
public sealed class HediffCompPatch : FishPrepatch
{
public override string? Description { get; }
= "Optimizes GetComp lookups with a fast custom dictionary implementation. This is often more than 10x "
@@ -134,7 +259,7 @@ public class GetCompCaching : ClassWithFishPrepatches
}
}
public class HediffCompPropsPatch : FishPrepatch
public sealed class HediffCompPropsPatch : FishPrepatch
{
public override string? Description { get; }
= "Optimizes GetComp lookups with a fast custom dictionary implementation. This is often more than 10x "
@@ -195,7 +320,7 @@ public class GetCompCaching : ClassWithFishPrepatches
}
}
public class AbilityCompPatch : FishPrepatch
public sealed class AbilityCompPatch : FishPrepatch
{
public override string? Description { get; }
= "Optimizes GetComp lookups with a fast custom dictionary implementation. This is often more than 10x "
@@ -256,7 +381,7 @@ public class GetCompCaching : ClassWithFishPrepatches
}
}
public class WorldObjectCompPatch : FishPrepatch
public sealed class WorldObjectCompPatch : FishPrepatch
{
public override string? Description { get; }
= "Optimizes GetComp lookups with a fast custom dictionary implementation. This is often more than 10x "
@@ -312,7 +437,7 @@ public class GetCompCaching : ClassWithFishPrepatches
}
}
public class MapComponentPatch : FishPrepatch
public sealed class MapComponentPatch : FishPrepatch
{
public override string? Description { get; }
= "Optimizes GetComp lookups with a fast custom dictionary implementation. This is often more than 10x "
@@ -368,7 +493,7 @@ public class GetCompCaching : ClassWithFishPrepatches
}
}
public class WorldComponentPatch : FishPrepatch
public sealed class WorldComponentPatch : FishPrepatch
{
public override string? Description { get; }
= "Optimizes GetComp lookups with a fast custom dictionary implementation. This is often more than 10x "
@@ -419,7 +544,7 @@ public class GetCompCaching : ClassWithFishPrepatches
}
}
public class GameComponentPatch : FishPrepatch
public sealed class GameComponentPatch : FishPrepatch
{
public override string? Description { get; }
= "Optimizes GetComp lookups with a fast custom dictionary implementation. This is often more than 10x "
+58
View File
@@ -0,0 +1,58 @@
// Copyright (c) 2023 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 JetBrains.Annotations;
namespace PerformanceFish;
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public record struct GroupThingPair
{
private uint _data;
public ThingRequestGroup ThingRequestGroup
{
get => (ThingRequestGroup)(byte)(_data >> 24); // fewer hash collisions this way than in rightmost bit
set => _data = (_data & 0x00_FF_FF_FF) | ((uint)value << 24);
}
public int ThingIDNumber
{
get => (int)(_data & 0x00_FF_FF_FF); // 24 bit, 16.8m MaxValue
set => _data = (_data & 0xFF_00_00_00) | (uint)(value & 0x00_FF_FF_FF);
}
public GroupThingPair(ThingRequestGroup thingRequestGroup, int thingIDNumber)
=> _data = ((uint)thingRequestGroup << 24) | (0x00_FF_FF_FF & (uint)thingIDNumber);
public bool Equals(GroupThingPair other) => _data == other._data;
public override int GetHashCode() => unchecked((int)_data);
}
// [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
// public record struct GroupThingPair
// {
// private uint _data;
//
// public ThingRequestGroup ThingRequestGroup
// {
// get => (ThingRequestGroup)(byte)_data;
// set => _data = (_data & 0xFF_FF_FF_00) | (byte)value;
// }
//
// public int ThingIDNumber
// {
// get => (int)(_data >> 8); // 24 bit, 16.8m MaxValue
// set => _data = (_data & 0xFF) | (uint)(value << 8);
// }
//
// public GroupThingPair(ThingRequestGroup thingRequestGroup, int thingIDNumber)
// => _data = (uint)thingRequestGroup | ((uint)thingIDNumber << 8);
//
// public bool Equals(GroupThingPair other) => _data == other._data;
//
// public override int GetHashCode() => Unsafe.As<uint, int>(ref _data);
// }
@@ -0,0 +1,62 @@
// Copyright (c) 2023 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 Mono.Cecil;
using Mono.Cecil.Cil;
using PerformanceFish.Events;
using PerformanceFish.Prepatching;
namespace PerformanceFish.Hauling;
public sealed class GridsUtilityPatches : ClassWithFishPrepatches
{
public sealed class GetItemCountPatch : FishPrepatch
{
public override int Priority => HarmonyLib.Priority.First;
public override string? Description { get; }
= "Caches item counts in a grid, to not have to repeat counting on every request.";
public override MethodBase TargetMethodBase { get; } = methodof(GridsUtility.GetItemCount);
protected internal override void BeforeHarmonyPatching() => ThingEvents.Initialized += TryRegister;
public static void TryRegister(Thing thing)
{
if (!thing.IsItem())
return;
var thingEvents = thing.Events();
thingEvents.RegisteredAtThingGrid += Increment;
thingEvents.DeregisteredAtThingGrid += Decrement;
}
public static void Increment(Thing thing, Map map, in IntVec3 cell)
=> map.ItemCountGrid()[cell]++;
public static void Decrement(Thing thing, Map map, in IntVec3 cell)
{
ref var itemCount = ref map.ItemCountGrid()[cell];
itemCount--;
if (itemCount < 0)
{
itemCount = 0;
ErrorForInvalidCount(thing);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ErrorForInvalidCount(Thing thing)
=> Log.Error($"Tried decrementing item count at cell {thing.Position} for thing '{
thing}', but it was already 0. This should never happen.");
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int ReplacementBody(IntVec3 c, Map map) => map.ItemCountGrid()[c];
}
}
@@ -0,0 +1,31 @@
// Copyright (c) 2023 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/.
namespace PerformanceFish.Hauling;
public record struct HaulDestinationManagerCache
{
public static readonly int StoragePriorityCount = Enum.GetValues(typeof(StoragePriority)).Length;
public event Action<HaulDestinationManager>? PriorityChanged;
public int[] GroupCountByPriority = new int[StoragePriorityCount];
[MethodImpl(MethodImplOptions.NoInlining)]
public void OnPriorityChanged(HaulDestinationManager manager)
{
var slotGroupsByPriority = manager.AllGroupsListInPriorityOrder;
Array.Clear(GroupCountByPriority, 0, GroupCountByPriority.Length);
for (var i = slotGroupsByPriority.Count; i-- > 0;)
GroupCountByPriority[(int)slotGroupsByPriority[i].Settings.Priority]++;
PriorityChanged?.Invoke(manager);
}
public HaulDestinationManagerCache()
{
}
}
@@ -0,0 +1,53 @@
// Copyright (c) 2023 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 PerformanceFish.Prepatching;
namespace PerformanceFish.Hauling;
public sealed class HaulDestinationManagerPatches : ClassWithFishPrepatches
{
public sealed class Notify_HaulDestinationChangedPriorityPatch : FishPrepatch
{
public override bool ShowSettings => false;
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(HaulDestinationManager),
nameof(HaulDestinationManager.Notify_HaulDestinationChangedPriority));
public static void Postfix(HaulDestinationManager __instance)
=> __instance.Cache().OnPriorityChanged(__instance);
}
public sealed class AddHaulDestinationPatch : FishPrepatch
{
public override bool ShowSettings => false;
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(HaulDestinationManager),
nameof(HaulDestinationManager.AddHaulDestination));
public static void Postfix(HaulDestinationManager __instance, IHaulDestination haulDestination)
{
if (haulDestination is ISlotGroupParent)
__instance.Cache().OnPriorityChanged(__instance);
}
}
public sealed class RemoveHaulDestinationPatch : FishPrepatch
{
public override bool ShowSettings => false;
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(HaulDestinationManager),
nameof(HaulDestinationManager.RemoveHaulDestination));
public static void Postfix(HaulDestinationManager __instance, IHaulDestination haulDestination)
{
if (haulDestination is ISlotGroupParent)
__instance.Cache().OnPriorityChanged(__instance);
}
}
}
@@ -0,0 +1,38 @@
// Copyright (c) 2023 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/.
global using LwmCompCache = PerformanceFish.Cache.ByReference<Verse.Thing, PerformanceFish.Hauling.LwmCompCacheValue>;
namespace PerformanceFish.Hauling;
public record struct LwmCompCacheValue
{
public CompProperties? CompProperties;
public static CompProperties? TryGetLwmProps(Thing slotGroupParent)
=> ModCompatibility.Types.LWM.CompProperties != null
? slotGroupParent.def.comps?.Find(static props
=> props.GetType() == ModCompatibility.Types.LWM.CompProperties)
: null;
static LwmCompCacheValue() => EnsureInitialized();
public static void EnsureInitialized()
{
if (Interlocked.Exchange(ref _initialized, 1) == 0)
LwmCompCache.ValueInitializer = static key => new() { CompProperties = TryGetLwmProps(key) };
}
private static volatile int _initialized;
public static AccessTools.FieldRef<CompProperties, int>?
LwmMaxNumberStacks = TryGetLwmField("maxNumberStacks"),
LwmMinNumberStacks = TryGetLwmField("minNumberStacks");
private static AccessTools.FieldRef<CompProperties, int>? TryGetLwmField(string name)
=> ModCompatibility.Types.LWM.CompProperties is { } propsType
? AccessTools.FieldRefAccess<CompProperties, int>(AccessTools.Field(propsType, name))
: null;
}
@@ -0,0 +1,147 @@
// Copyright (c) 2023 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.Diagnostics.CodeAnalysis;
using FisheryLib.Pools;
namespace PerformanceFish.Hauling;
public static class SlotGroupExtensions
{
public const int
MINIMUM_DISTRICT_COUNT = 3,
MINIMUM_DISTRICT_SLOTS = 9,
MINIMUM_DISTRICT_CELLS = 3;
public static bool ShouldHaveDistricts([NotNullWhen(true)] this SlotGroup? slotGroup, out int cellCountIfTrue,
out int totalSlotsIfTrue)
{
if (slotGroup is null || slotGroup.parent is Building { Spawned: false })
{
Unsafe.SkipInit(out cellCountIfTrue);
goto False;
}
cellCountIfTrue = slotGroup.CellsList.Count;
if (cellCountIfTrue >= MINIMUM_DISTRICT_COUNT * MINIMUM_DISTRICT_CELLS)
{
return (totalSlotsIfTrue = slotGroup.GetTotalSlots(cellCountIfTrue))
>= MINIMUM_DISTRICT_COUNT * MINIMUM_DISTRICT_SLOTS;
}
False:
Unsafe.SkipInit(out totalSlotsIfTrue);
return false;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void ResetAndRemakeDistricts(this SlotGroup slotGroup)
{
slotGroup.ResetDistricts();
slotGroup.NotifyCellCountChanged();
}
public static void ResetDistricts(this SlotGroup slotGroup)
{
ref var districts = ref slotGroup.Districts();
for (var i = districts.Length; i-- > 0;)
districts[i].Dispose();
districts = StorageDistrict.GetDefaultArray();
}
public static void NotifyCellCountChanged(this SlotGroup slotGroup)
{
if (slotGroup.ShouldHaveDistricts(out var cellCount, out var totalSlots))
slotGroup.AdjustDistricts(totalSlots, cellCount);
else
slotGroup.ResetDistricts();
}
public static void AdjustDistricts(this SlotGroup slotGroup, int totalSlots, int cellCount,
Span<IntVec3> cellsToAdd = default, Span<IntVec3> cellsToRemove = default)
{
var districtCount = Math.Min(totalSlots / MINIMUM_DISTRICT_SLOTS, cellCount / MINIMUM_DISTRICT_COUNT);
ref var districts = ref slotGroup.Districts();
using var cellBufferPooled = new PooledIList<List<IntVec3>>();
var cellBuffer = cellBufferPooled.List;
var hasDistricts = districts.Length > 1;
if (cellsToRemove.Length > 0)
{
if (hasDistricts)
{
for (var i = cellsToRemove.Length; i-- > 0;)
{
for (var j = districts.Length; j-- > 0;)
{
if (districts[j].TryRemoveCell(cellsToRemove[i]))
break;
}
}
}
else
{
Log.Error($"Tried removing cells from districts in slotGroup of {
slotGroup.parent}, but none exist.");
}
}
if (cellsToAdd.Length > 0 && hasDistricts)
{
for (var i = cellsToAdd.Length; i-- > 0;)
cellBuffer.Add(cellsToAdd[i]);
}
if (hasDistricts)
{
if (districts.Length > districtCount)
{
for (var i = districtCount - 1; i < districts.Length; i++)
cellBuffer.AddRange(districts[i].Cells);
}
}
else
{
cellBuffer.AddRange(slotGroup.CellsList);
}
if (districts.Length != districtCount)
{
if (hasDistricts)
Array.Resize(ref districts, districtCount);
else
districts = new StorageDistrict[districtCount];
for (var i = 0; i < districts.Length; i++)
((StorageDistrict?[])districts)[i] ??= new(slotGroup);
}
var districtSize = cellCount / districtCount;
var finalDistrictSize = cellCount - (districtSize * (districtCount - 1));
for (var i = 0; i < districtCount; i++)
{
var district = districts[i];
var desiredDistrictSize = i != districtCount - 1 ? districtSize : finalDistrictSize;
while (district.Cells.Count > desiredDistrictSize)
cellBuffer.Add(district.PopCell());
}
for (var i = 0; i < districtCount; i++)
{
var district = districts[i];
while (district.Cells.Count < districtSize)
district.AddCell(cellBuffer.Pop());
}
while (cellBuffer.Count > 0)
districts[^1].AddCell(cellBuffer.Pop());
}
}
@@ -0,0 +1,44 @@
// Copyright (c) 2023 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 PerformanceFish.Prepatching;
namespace PerformanceFish.Hauling;
public sealed class SlotGroupPrepatches : ClassWithFishPrepatches
{
public sealed class Notify_AddedCellPatch : FishPrepatch
{
public override bool ShowSettings => false;
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(SlotGroup), nameof(SlotGroup.Notify_AddedCell));
public static void Postfix(SlotGroup __instance, IntVec3 c)
{
if (__instance.ShouldHaveDistricts(out var cellCount, out var totalSlots))
__instance.AdjustDistricts(totalSlots, cellCount, stackalloc IntVec3[] { c });
}
}
public sealed class Notify_LostCellPatch : FishPrepatch
{
public override bool ShowSettings => false;
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(SlotGroup), nameof(SlotGroup.Notify_LostCell));
public static void Postfix(SlotGroup __instance, IntVec3 c)
{
if (__instance.Districts().Length <= 1)
return;
if (__instance.ShouldHaveDistricts(out var cellCount, out var totalSlots))
__instance.AdjustDistricts(totalSlots, cellCount, cellsToRemove: stackalloc IntVec3[] { c });
else
__instance.ResetDistricts();
}
}
}
@@ -0,0 +1,125 @@
// Copyright (c) 2023 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/.
namespace PerformanceFish.Hauling;
public class StorageDistrict
{
public int FreeSlots = 0xFFFFFFF;
private int _storedThingCount;
public int TotalSlots => Parent.GetTotalSlots(Cells.Count);
public List<IntVec3> Cells { get; } = [];
private FishTable<ushort, List<Thing>> _storedThingsByDef = new();
public SlotGroup Parent { get; }
public Map Map { get; } = null!;
public StorageDistrict(SlotGroup? parent)
{
Parent = parent!;
if (parent != null)
Map = parent.Map;
}
public void AddCell(in IntVec3 cell)
{
Cells.Add(cell);
var cellIndex = new CellIndex(cell, Map);
Map.StorageDistrictGrid()[cellIndex] = this;
FreeSlots = TotalSlots - _storedThingCount;
var thingsAtCell = cellIndex.GetThingList(Map);
for (var i = 0; i < thingsAtCell.Count; i++)
{
var thing = thingsAtCell[i];
if (!thing.IsItem())
continue;
AddThing(thing);
}
}
public IntVec3 PopCell()
{
var cell = Cells.Pop();
DeregisterCell(cell);
return cell;
}
public bool TryRemoveCell(in IntVec3 cell)
{
if (!Cells.Remove(cell))
return false;
DeregisterCell(cell);
return true;
}
private void DeregisterCell(in IntVec3 cell)
{
var cellIndex = new CellIndex(cell, Map);
ref var districtGridSlot = ref Map.StorageDistrictGrid()[cellIndex];
if (districtGridSlot == this)
districtGridSlot = null;
var thingsAtCell = cellIndex.GetThingList(Map);
for (var i = 0; i < thingsAtCell.Count; i++)
{
var thing = thingsAtCell[i];
if (!thing.IsItem())
continue;
RemoveThing(thing);
}
}
public void AddThing(Thing thing)
{
StoredThingsOfDef(thing.def).Add(thing);
FreeSlots = TotalSlots - ++_storedThingCount;
}
public void RemoveThing(Thing thing)
{
if (!StoredThingsOfDef(thing.def).Remove(thing))
ThrowHelper.ThrowInvalidOperationException();
FreeSlots = TotalSlots - --_storedThingCount;
}
public List<Thing> StoredThingsOfDef(ThingDef def) => _storedThingsByDef.GetOrAdd(def.shortHash);
public bool CapacityAllows(Thing t) => FreeSlots > 0 || StoredThingsOfDef(t.def).ContainsThingStackableWith(t);
public static StorageDistrict[] GetDefaultArray() => _defaultDistrictArray;
public void Dispose()
{
if (this == _defaultDistrict)
return;
var districtGrid = Map.StorageDistrictGrid();
var cells = Cells;
for (var i = cells.Count; i-- > 0;)
{
ref var district = ref districtGrid[cells[i]];
if (district == this)
district = null;
}
}
private static StorageDistrict _defaultDistrict = new(null);
private static StorageDistrict[] _defaultDistrictArray = [_defaultDistrict];
}
@@ -0,0 +1,32 @@
// Copyright (c) 2023 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/.
namespace PerformanceFish.Hauling;
public static class StorageExtensions
{
public static int TrueItemsPerCell(this Thing slotGroupParent)
=> slotGroupParent.TrueItemsPerCell(slotGroupParent.TryGetLwmCompProperties());
public static int TrueItemsPerCell(this Thing slotGroupParent, CompProperties? lwmProps)
=> lwmProps != null
? Math.Max(LwmCompCacheValue.LwmMinNumberStacks!(lwmProps), LwmCompCacheValue.LwmMaxNumberStacks!(lwmProps))
: slotGroupParent is Building building
? building.MaxItemsInCell
: 1;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static CompProperties? TryGetLwmCompProperties(this Thing thing)
=> LwmCompCache.GetOrAdd(thing).CompProperties;
public static int GetTotalSlots(this SlotGroup slotGroup) => slotGroup.GetTotalSlots(slotGroup.CellsList.Count);
public static int GetTotalSlots(this SlotGroup slotGroup, int cellCount)
=> slotGroup.parent is Building storage
? storage.TrueItemsPerCell() * cellCount
: cellCount;
static StorageExtensions() => LwmCompCacheValue.EnsureInitialized();
}
@@ -0,0 +1,232 @@
// Copyright (c) 2023 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/.
// #define StorageSettings_DEBUG
using System.Diagnostics;
using System.Linq;
using FisheryLib.Pools;
using PerformanceFish.Events;
namespace PerformanceFish.Hauling;
public sealed class StorageSettingsPatches : ClassWithFishPatches
{
public sealed class AllowedToAcceptPatch : FirstPriorityFishPatch
{
public override string Description { get; }
= "StorageSettings and capacity caching. High impact in colonies with multiple full storage buildings.";
public override Expression<Action> TargetMethod { get; }
= static () => default(StorageSettings)!.AllowedToAccept(default(Thing));
protected internal override void OnPatchingCompleted() => ThingEvents.Initialized += TryRegister;
public static void TryRegister(Thing thing)
{
var thingEvents = thing.Events();
if (thing is ISlotGroupParent)
thingEvents.Spawned += InitializeSlotGroupParent;
if (!thing.IsItem())
return;
thingEvents.RegisteredAtThingGrid += TryNotifyReceivedThing;
thingEvents.DeregisteredAtThingGrid += TryNotifyLostThing;
}
public static void TryNotifyReceivedThing(Thing thing, Map map, in IntVec3 cell)
{
if (cell.GetSlotGroupParent(map) is { } parent and Thing)
parent.GetStoreSettings()?.Cache().Notify_ReceivedThing(thing);
}
public static void TryNotifyLostThing(Thing thing, Map map, in IntVec3 cell)
{
if (cell.GetSlotGroupParent(map) is { } parent and Thing)
parent.GetStoreSettings()?.Cache().Notify_LostThing(thing);
}
public static void InitializeSlotGroupParent(Thing thing, Map map)
{
var slotGroupParent = (ISlotGroupParent)thing;
if (slotGroupParent.GetStoreSettings() is not { } storeSettings)
return;
ref var cache = ref storeSettings.Cache();
cache.Initialize(thing);
var slotCellsList = slotGroupParent.AllSlotCellsList();
for (var i = slotCellsList.Count; i-- > 0;)
{
var thingsAtCell = slotCellsList[i].GetThingListUnchecked(map);
for (var j = thingsAtCell.Count; j-- > 0;)
{
var storedThing = thingsAtCell[j];
if (!storedThing.IsItem() || cache.Contains(storedThing))
continue;
cache.Notify_ReceivedThing(storedThing);
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Prefix(StorageSettings __instance, Thing t, ref bool __result, out bool __state)
{
var cachedAllowedToAcceptValue = __instance.Cache().AllowedToAccept.GetOrAdd(t.thingIDNumber);
if (StorageSettingsCache.IsDirty(cachedAllowedToAcceptValue))
return __state = true;
__result = cachedAllowedToAcceptValue.AsBool() && CapacityAllows(__instance, t);
return __state = false;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool CapacityAllows(StorageSettings __instance, Thing t)
{
if (__instance.owner is not Thing ownerThing)
return true;
ref var cache = ref __instance.Cache();
Debug.VerifyItemCount(ownerThing, ref cache);
return cache.FreeSlots > 0
|| (t.TryGetMap() is { } map && t.Position.GetSlotGroupParent(map) == ownerThing)
|| AcceptsForStacking(ref cache, t);
}
public static bool AcceptsForStacking(ref StorageSettingsCache cache, Thing t)
=> cache.StoredThingsOfDef(t.def).ContainsThingStackableWith(t);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Postfix(StorageSettings __instance, Thing t, bool __result, bool __state)
{
if (!__state)
return;
UpdateCache(__instance, t, __result);
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void UpdateCache(StorageSettings __instance, Thing t, bool __result)
=> __instance.Cache().Update(t.thingIDNumber, __result && t is { def: not null, thingIDNumber: not -1 });
}
public sealed class TryNotifyChanged_Patch : FishPatch
{
public static event Action<StorageSettings>? Changed;
public override string Description { get; } = "StorageSettings cache resetting";
public override Expression<Action> TargetMethod { get; }
= static () => default(StorageSettings)!.TryNotifyChanged();
public static void Postfix(StorageSettings __instance)
{
__instance.Cache().OnSettingsChanged(__instance);
Changed?.Invoke(__instance);
}
}
public record struct StorageSettingsCache
{
public FishTable<int, int> AllowedToAccept = new();
private Thing? _parent;
public int FreeSlots = 0xFFFFFFF;
private int _storedThingCount;
private FishTable<ushort, List<Thing>> _storedThingsByDef = new();
private CompProperties? _lwmProps;
public event Action<StorageSettings>? SettingsChanged;
public int TotalSlots => _parent!.TrueItemsPerCell(_lwmProps) * _parent!.SlotGroupCellCount();
public List<Thing> StoredThingsOfDef(ThingDef def) => _storedThingsByDef.GetOrAdd(def.shortHash);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsDirty(int allowedToAcceptValue) => allowedToAcceptValue == int.MaxValue;
public void Update(int thingIDNumber, bool allowedToAccept)
=> AllowedToAccept[thingIDNumber] = allowedToAccept.AsInt();
public void Initialize(Thing slotGroupParent)
{
_parent = slotGroupParent;
_storedThingsByDef.Clear();
AllowedToAccept.Clear();
_lwmProps = slotGroupParent.TryGetLwmCompProperties();
FreeSlots = TotalSlots;
}
public bool Contains(Thing thing) => StoredThingsOfDef(thing.def).Contains(thing);
public void Notify_ReceivedThing(Thing thing)
{
if (_parent is null)
return;
StoredThingsOfDef(thing.def).Add(thing);
FreeSlots = TotalSlots - ++_storedThingCount;
Debug.VerifyItemCount(_parent, ref this);
}
public void Notify_LostThing(Thing thing)
{
if (_parent is null)
return;
StoredThingsOfDef(thing.def).Remove(thing);
FreeSlots = TotalSlots - --_storedThingCount;
Debug.VerifyItemCount(_parent, ref this);
}
public void OnSettingsChanged(StorageSettings storageSettings)
{
AllowedToAccept.Clear();
if (_parent?.IsSpawned() ?? false)
FreeSlots = TotalSlots - _storedThingCount;
SettingsChanged?.Invoke(storageSettings);
}
public StorageSettingsCache() => AllowedToAccept.ValueInitializer = static _ => int.MaxValue;
}
public static class Debug
{
[Conditional("StorageSettings_DEBUG")]
public static void LogMessage(PooledStringHandler stringHandler) => Log.Message(stringHandler);
[Conditional("StorageSettings_DEBUG")]
public static void VerifyItemCount(Thing storageThing, ref StorageSettingsCache cache)
{
if (storageThing.TryGetMap() is not { } ownerMap || storageThing is not ISlotGroupParent slotGroup)
return;
var realTotalSlots = cache.TotalSlots;
var realTotalItemCount = slotGroup.AllSlotCellsList().Sum(cell => cell.GetItemCount(ownerMap));
if (cache.FreeSlots == realTotalSlots - realTotalItemCount)
return;
Log.Error($"StorageSettings cache for '{storageThing}' has incorrect free slots count of '{
cache.FreeSlots}'. Real total item count: '{realTotalItemCount}', real total slots: '{
realTotalSlots}', real free slots: '{realTotalSlots - realTotalItemCount}'");
Current.Game.tickManager.Pause();
}
}
}
@@ -0,0 +1,296 @@
// Copyright (c) 2023 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 Mono.Cecil;
using Mono.Cecil.Cil;
using PerformanceFish.Events;
using PerformanceFish.Listers;
using PerformanceFish.Prepatching;
namespace PerformanceFish.Hauling;
public sealed class StoreUtilityPrepatches : ClassWithFishPrepatches
{
public sealed class NoStorageBlockersInPatch : FishPrepatch
{
public override string? Description { get; }
= "Optimizes this frequently accessed storage check by caching fully blocked cells in a bit array and "
+ "reordering instructions to make better use of caches.";
public override MethodBase TargetMethodBase { get; } = methodof(StoreUtility.NoStorageBlockersIn);
protected internal override void OnPatchingCompleted() => ThingEvents.Initialized += TryRegister;
public static void TryRegister(Thing thing)
{
if (!IsStorageBlocker(thing.def))
return;
var thingEvents = thing.Events();
thingEvents.RegisteredAtThingGrid += SetStorageBlocker;
thingEvents.DeregisteredAtThingGrid += TryUnsetStorageBlocker;
}
public static bool IsStorageBlocker(ThingDef thingDef)
=> (thingDef.entityDefToBuild != null && thingDef.entityDefToBuild.passability != 0)
|| (thingDef.passability != 0
&& thingDef.surfaceType == SurfaceType.None
&& thingDef.category != ThingCategory.Item);
public static void SetStorageBlocker(Thing thing, Map map, in IntVec3 cell)
=> map.StorageBlockerGrid()[cell] = true;
public static void TryUnsetStorageBlocker(Thing thing, Map map, in IntVec3 cell)
{
var thingsList = map.thingGrid.ThingsListAt(cell);
for (var i = thingsList.Count; i-- > 0;)
{
var otherThing = thingsList[i];
if (otherThing != thing && IsStorageBlocker(otherThing.def))
return;
}
map.StorageBlockerGrid()[cell] = false;
}
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static bool ReplacementBody(IntVec3 c, Map map, Thing thing)
{
if (map.StorageBlockerGrid()[c])
return false;
if (c.GetItemCount(map) < c.GetMaxItemsAllowedInCell(map))
return true;
var thingsAtCell = map.thingGrid.ThingsListAt(c);
var thingCount = thingsAtCell.Count;
for (var i = 0; i < thingCount; i++)
{
var thingAtCell = thingsAtCell[i];
if (thingAtCell.stackCount < thingAtCell.def.stackLimit
&& thingAtCell.CanStackWith(thing)
&& thingAtCell.def.EverStorable(false))
{
return true;
}
}
return false;
}
}
public sealed class TryFindBestBetterStoreCellForWorkerPatch : FishPrepatch
{
public override List<Type> LinkedPatches { get; } =
[
typeof(SlotGroupPrepatches.Notify_AddedCellPatch), typeof(SlotGroupPrepatches.Notify_LostCellPatch)
];
public override string? Description { get; }
= "Optimizes hauling by splitting storage areas into smaller regions with cached capacity information. "
+ "Additionally makes lookups more thorough when having the 'Improve hauling accuracy' setting enabled, "
+ "raising the odds of getting closer cells as haul destinations";
public override MethodBase TargetMethodBase { get; }
= methodof(StoreUtility.TryFindBestBetterStoreCellForWorker);
protected internal override void OnPatchingCompleted() => ThingEvents.Initialized += TryRegister;
public static void TryRegister(Thing thing)
{
var thingEvents = thing.Events();
if (thing is ISlotGroupParent)
{
thingEvents.Spawned += NotifySlotGroupCellCountChanged;
thingEvents.DeSpawned += NotifySlotGroupCellCountChanged;
}
if (!thing.IsItem())
return;
thingEvents.RegisteredAtThingGrid += TryNotifyReceivedThing;
thingEvents.DeregisteredAtThingGrid += TryNotifyLostThing;
}
public static void NotifySlotGroupCellCountChanged(Thing slotGroupParent, Map map)
=> NotifySlotGroupCellCountChanged(slotGroupParent);
public static void NotifySlotGroupCellCountChanged(Thing slotGroupParent)
=> slotGroupParent.GetSlotGroup()?.NotifyCellCountChanged();
public static void TryNotifyReceivedThing(Thing thing, Map map, in IntVec3 cell)
=> map.StorageDistrictGrid()[cell]?.AddThing(thing);
public static void TryNotifyLostThing(Thing thing, Map map, in IntVec3 cell)
=> map.StorageDistrictGrid()[cell]?.RemoveThing(thing);
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static void ReplacementBody(Thing t, Pawn carrier, Map map, Faction faction, SlotGroup? slotGroup,
bool needAccurateResult, ref IntVec3 closestSlot, ref float closestDistSquared,
ref StoragePriority foundPriority)
{
if (slotGroup == null || !slotGroup.parent.Accepts(t))
return;
var thingPosition = t.SpawnedOrAnyParentSpawned ? t.PositionHeld : carrier.PositionHeld;
var slotGroupCells = slotGroup.CellsList;
var maxValidCellsToCheck
= !FishSettings.ImproveHaulingAccuracy && needAccurateResult
? Mathf.FloorToInt(slotGroupCells.Count * Rand.Range(0.005f, 0.018f))
: 0;
StartOfDistrictLoop:
var districts = slotGroup.Districts();
for (var districtIndex = 0; districtIndex < districts.Length; districtIndex++)
{
var district = districts[districtIndex];
if (!district.CapacityAllows(t))
continue;
var districtCells = districts.Length > 1 ? district.Cells : slotGroupCells;
for (var i = 0; i < districtCells.Count; i++)
{
var slotGroupCell = districtCells[i];
var cellDistance = (float)(thingPosition - slotGroupCell).LengthHorizontalSquared;
if (cellDistance > closestDistSquared
|| !StoreUtility.IsGoodStoreCell(slotGroupCell, map, t, carrier, faction))
{
continue;
}
if (districts.Length > 1 && slotGroupCell.GetSlotGroup(map) != district.Parent)
{
district.Parent.ResetAndRemakeDistricts(); // TODO: figure out why this happens
goto StartOfDistrictLoop;
}
closestSlot = slotGroupCell;
closestDistSquared = cellDistance;
foundPriority = slotGroup.Settings.Priority;
if (!FishSettings.ImproveHaulingAccuracy && (districtIndex * i) + i >= maxValidCellsToCheck)
return;
}
}
}
}
public sealed class TryFindBestBetterStoreCellForPatch : FishPrepatch
{
public override string? Description { get; }
= "Optimizes a loop in the method to make use of cached storage priorities for fewer comparisons against "
+ "those. Also removes haulables from ListerHaulables if the found best store cell turns out to be the "
+ "current cell";
public override MethodBase TargetMethodBase { get; } = methodof(StoreUtility.TryFindBestBetterStoreCellFor);
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static bool ReplacementBody(Thing t, Pawn carrier, Map map,
StoragePriority currentPriority, Faction faction, out IntVec3 foundCell, bool needAccurateResult = true)
{
var haulDestinationManager = map.haulDestinationManager;
List<SlotGroup> listInPriorityOrder = haulDestinationManager.AllGroupsListInPriorityOrder;
if (listInPriorityOrder.Count == 0)
{
foundCell = IntVec3.Invalid;
return false;
}
var foundPriority = currentPriority;
var closestDistSquared = (float)int.MaxValue;
var closestSlot = IntVec3.Invalid;
var groupCountByPriority = GetCachedGroupCountByPriority(haulDestinationManager);
var i = 0;
for (var priorityGroupIndex = groupCountByPriority.Length; priorityGroupIndex-- > 0;)
{
var groupsOfPriorityCount = groupCountByPriority[priorityGroupIndex];
if ((StoragePriority)priorityGroupIndex < foundPriority) // succeeded finding a higher priority storage
break;
if ((StoragePriority)priorityGroupIndex <= currentPriority) // current storage is highest valid priority
{
TryRemoveFromListerHaulables(t, currentPriority);
break;
}
while (groupsOfPriorityCount-- > 0)
{
StoreUtility.TryFindBestBetterStoreCellForWorker(t, carrier, map, faction, listInPriorityOrder[i++],
needAccurateResult, ref closestSlot, ref closestDistSquared, ref foundPriority);
}
}
if (!closestSlot.IsValid)
{
foundCell = IntVec3.Invalid;
return false;
}
foundCell = closestSlot;
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int[] GetCachedGroupCountByPriority(HaulDestinationManager haulDestinationManager)
=> haulDestinationManager.Cache().GroupCountByPriority;
[MethodImpl(MethodImplOptions.NoInlining)]
public static void TryRemoveFromListerHaulables(Thing t, StoragePriority currentPriority)
{
if (t.IsInAnyStorage() /*t.IsInValidStorage()*/ && t.TryGetMapHeld() is { } map)
map.listerHaulables.TryRemoveDirectly(t);
// remove for any storage instead of only valid storage to prevent further haul attempts until the next
// automatic ListerHaulablesTick cycle
}
}
public sealed class GetSlotGroupPatch : FishPrepatch
{
public override string? Description { get; }
= "Optimization for this method by reducing its amount of instructions without changing results";
public override MethodBase TargetMethodBase { get; }
= methodof((Func<Thing, SlotGroup>)StoreUtility.GetSlotGroup);
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static SlotGroup? ReplacementBody(Thing thing)
=> thing.TryGetMap() is { } map ? thing.Position.GetSlotGroup(map) : null;
}
public sealed class CurrentHaulDestinationOfPatch : FishPrepatch
{
public override string? Description { get; }
= "Optimization for this method by reducing its amount of instructions without changing results";
public override MethodBase TargetMethodBase { get; }
= methodof(StoreUtility.CurrentHaulDestinationOf);
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static IHaulDestination? ReplacementBody(Thing t)
=> t.TryGetMap() is { } map
? map.haulDestinationManager.SlotGroupParentAt(t.Position)
: t.ParentHolder as IHaulDestination;
}
}
@@ -8,7 +8,7 @@ using PerformanceFish.Prepatching;
namespace PerformanceFish.Hediffs;
public class HealthTrackerOptimization : ClassWithFishPrepatches
public sealed class HealthTrackerOptimization : ClassWithFishPrepatches
{
public static void Initialize()
{
@@ -24,7 +24,7 @@ public class HealthTrackerOptimization : ClassWithFishPrepatches
}
}
public class HealthTickPatch : FishPrepatch
public sealed class HealthTickPatch : FishPrepatch
{
public override MethodBase TargetMethodBase { get; }
= AccessTools.Method(typeof(Pawn_HealthTracker), nameof(Pawn_HealthTracker.HealthTick));
@@ -3,14 +3,13 @@
// 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.Linq;
using static System.Reflection.Emit.OpCodes;
namespace PerformanceFish.Hediffs;
public class HediffComp_PsychicHarmonizer_Bugfix : ClassWithFishPatches
public sealed class HediffComp_PsychicHarmonizer_Bugfix : ClassWithFishPatches
{
public class CompPostTick_Patch : FishPatch
public sealed class CompPostTick_Patch : FishPatch
{
public override string Description { get; }
= "Fixes a bug in the psychic harmonizer tick method that was causing it to recalculate results on 149 "
@@ -22,29 +21,19 @@ public class HediffComp_PsychicHarmonizer_Bugfix : ClassWithFishPatches
public static CodeInstructions Transpiler(CodeInstructions codeInstructions)
{
var codes = codeInstructions.ToList();
var codes = codeInstructions.AsOrToList();
var pawn_int_IsHashIntervalTick = FishTranspiler.Call<Func<Thing, int, bool>>(Gen.IsHashIntervalTick);
var success = false;
for (var i = 0; i < codes.Count; i++)
{
if (i + 2 < codes.Count
&& codes[i].opcode == Ldc_I4
&& codes[i + 1] == pawn_int_IsHashIntervalTick
&& codes[i + 2].opcode == Brfalse_S)
if (i - 2 >= 0
&& codes[i - 2].opcode == Ldc_I4
&& codes[i - 1] == pawn_int_IsHashIntervalTick
&& codes[i].opcode == Brfalse_S)
{
//codes[i].operand = 128;
yield return codes[i];
yield return codes[i + 1];
codes[i + 2].opcode = Brtrue_S;
yield return codes[i + 2];
codes[i].opcode = Brtrue_S;
success = true;
i += 2;
}
else
{
yield return codes[i];
}
}
@@ -55,6 +44,8 @@ public class HediffComp_PsychicHarmonizer_Bugfix : ClassWithFishPatches
+ "an actual failure to patch if settings were not toggled however, which would mean some other "
+ "patch or a vanilla change either messes with this or implements the same thing");
}
return codes;
}
}
}
@@ -18,9 +18,9 @@ using NotMissingPartsCache
namespace PerformanceFish.Hediffs;
public class HediffSetPrecaching : ClassWithFishPrepatches
public sealed class HediffSetPrecaching : ClassWithFishPrepatches
{
public class GetFirstHediffOfDef : FishPrepatch
public sealed class GetFirstHediffOfDef : FishPrepatch
{
public override string Description { get; }
= "Caches results of the HediffSet.GetFirstHediffOfDef method. Impact scales with hediff count.";
@@ -94,7 +94,7 @@ public class HediffSetPrecaching : ClassWithFishPrepatches
}
}
public class HasHediff : FishPrepatch
public sealed class HasHediff : FishPrepatch
{
public override string Description { get; }
= "Caches results of the HediffSet.HasHediff method. Impact scales with hediff count.";
@@ -111,7 +111,7 @@ public class HediffSetPrecaching : ClassWithFishPrepatches
=> __instance.GetFirstHediffOfDef(def, mustBeVisible) != null;
}
public class GetNotMissingParts : FishPrepatch
public sealed class GetNotMissingParts : FishPrepatch
{
public override string? Description { get; }
= "Caches results of the HediffSet.GetNotMissingParts method. Impact scales with hediff count.";
@@ -156,9 +156,9 @@ public class HediffSetPrecaching : ClassWithFishPrepatches
}
}
public class HediffSetCaching : ClassWithFishPatches
public sealed class HediffSetCaching : ClassWithFishPatches
{
public class DirtyCache : FishPatch
public sealed class DirtyCache : FishPatch
{
public override string Description { get; }
= "Patched to trigger psylink cache clearing for the psychic entropy optimization.";
@@ -176,9 +176,9 @@ public class HediffSetCaching : ClassWithFishPatches
public record struct NotMissingPartsCacheValue
{
private int _version = -1;
private List<Hediff> _hediffsInSet = new();
private List<Hediff> _hediffsInSet = [];
private int _nextRefreshTick;
public List<BodyPartRecord> Parts = new();
public List<BodyPartRecord> Parts = [];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Update(HediffSet set, ref IEnumerable<BodyPartRecord> result)
@@ -209,7 +209,7 @@ public class HediffSetCaching : ClassWithFishPatches
public record struct HediffCacheValue
{
private int _version = -1;
private List<Hediff> _hediffsInSet = new();
private List<Hediff> _hediffsInSet = [];
private int _nextRefreshTick;
public Hediff? Hediff;
@@ -0,0 +1,99 @@
// Copyright (c) 2023 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/.
#if useless
using Mono.Cecil;
using Mono.Cecil.Cil;
using PerformanceFish.Prepatching;
namespace PerformanceFish.Hediffs;
public sealed class HediffWithCompsOptimization : ClassWithFishPrepatches
{
public sealed class ShouldRemovePatch : FishPrepatch
{
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredPropertyGetter(typeof(HediffWithComps), nameof(HediffWithComps.ShouldRemove));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static unsafe bool ReplacementBody(HediffWithComps instance)
{
if (instance.comps is { } comps)
{
for (var i = 0; i < comps.Count; i++)
{
if (comps[i].CompShouldRemove)
return true;
}
}
return BaseShouldRemove(instance);
}
public static unsafe delegate*<HediffWithComps, bool> BaseShouldRemove
= (delegate*<HediffWithComps, bool>)AccessTools
.DeclaredPropertyGetter(typeof(Hediff), nameof(Hediff.ShouldRemove)).GetFunctionPointer();
}
public sealed class VisiblePatch : FishPrepatch
{
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredPropertyGetter(typeof(HediffWithComps), nameof(HediffWithComps.Visible));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static unsafe bool ReplacementBody(HediffWithComps instance)
{
if (instance.comps is { } comps)
{
for (var i = 0; i < comps.Count; i++)
{
if (comps[i].CompDisallowVisible())
return false;
}
}
return BaseVisible(instance);
}
public static unsafe delegate*<HediffWithComps, bool> BaseVisible
= (delegate*<HediffWithComps, bool>)AccessTools
.DeclaredPropertyGetter(typeof(Hediff), nameof(Hediff.Visible)).GetFunctionPointer();
}
public sealed class PostTickPatch : FishPrepatch
{
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(HediffWithComps), nameof(HediffWithComps.PostTick));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static unsafe void ReplacementBody(HediffWithComps instance)
{
BasePostTick(instance);
if (instance.comps is not { } comps)
return;
var severityAdjustment = 0f;
for (var i = 0; i < comps.Count; i++)
comps[i].CompPostTick(ref severityAdjustment);
if (severityAdjustment != 0f)
instance.Severity += severityAdjustment;
}
public static unsafe delegate*<HediffWithComps, void> BasePostTick
= (delegate*<HediffWithComps, void>)AccessTools.DeclaredMethod(typeof(Hediff), nameof(Hediff.PostTick))
.GetFunctionPointer();
}
}
#endif
@@ -12,9 +12,9 @@ using ImmunityRecordExistsCache
namespace PerformanceFish.Hediffs;
public class ImmunityHandlerCaching : ClassWithFishPatches
public sealed class ImmunityHandlerCaching : ClassWithFishPatches
{
public class NeededImmunitiesNow : FirstPriorityFishPatch
public sealed class NeededImmunitiesNow : FirstPriorityFishPatch
{
public override string Description { get; }
= "Caches results of the ImmunityHandler.NeededImmunitiesNow method. Impact scales with hediff quantity.";
@@ -54,7 +54,7 @@ public class ImmunityHandlerCaching : ClassWithFishPatches
.Update(__instance.pawn.health.hediffSet, __result);
}
public class ImmunityRecordExists : FishPatch
public sealed class ImmunityRecordExists : FishPatch
{
public override string? Description { get; } = "Required by the TryAddImmunityRecord optimization.";
@@ -82,7 +82,7 @@ public class ImmunityHandlerCaching : ClassWithFishPatches
}
}
public class TryAddImmunityRecord : FishPatch
public sealed class TryAddImmunityRecord : FishPatch
{
public override string? Description { get; }
= "Optimization for this specific method to not run more checks than necessary.";
@@ -113,9 +113,9 @@ public class ImmunityHandlerCaching : ClassWithFishPatches
public record struct ImmunityInfoCacheValue
{
private int _version = -1;
private List<Hediff> _hediffsInSet = new();
private List<Hediff> _hediffsInSet = [];
private int _nextRefreshTick;
public List<ImmunityHandler.ImmunityInfo> Infos = new();
public List<ImmunityHandler.ImmunityInfo> Infos = [];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Update(HediffSet set, List<ImmunityHandler.ImmunityInfo> infos)
@@ -5,9 +5,9 @@
namespace PerformanceFish.Hediffs;
public class Pawn_PsychicEntropyTrackerOptimization : ClassWithFishPatches
public sealed class Pawn_PsychicEntropyTrackerOptimization : ClassWithFishPatches
{
public class Psylink_Patch : FirstPriorityFishPatch
public sealed class Psylink_Patch : FirstPriorityFishPatch
{
public override string Description { get; } = "Caches/throttles the psylink method to only update every 32 ticks.";
@@ -0,0 +1,59 @@
// Copyright (c) 2023 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 Mono.Cecil;
using Mono.Cecil.Cil;
using PerformanceFish.Prepatching;
namespace PerformanceFish;
public sealed class IdeoBuildingPresenceDemandOptimization : ClassWithFishPrepatches
{
public sealed class BuildingPresentPatch : FishPrepatch
{
public override string? Description { get; }
= "Optimization to use cached ListerBuildings data instead of scanning through all colonist buildings, "
+ "frequently";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(IdeoBuildingPresenceDemand),
nameof(IdeoBuildingPresenceDemand.BuildingPresent));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static bool ReplacementBody(IdeoBuildingPresenceDemand __instance, Map map)
{
var buildings = map.listerBuildings.AllBuildingsColonistOfDef(__instance.parent.ThingDef);
if (buildings is IndexedFishSet<Building> fishSet)
{
var list = fishSet.ReadOnlyList;
for (var i = list.Count; i-- > 0;)
{
if (list[i].StyleSourcePrecept == __instance.parent)
return true;
}
return false;
}
else
{
return FallbackCheck(__instance, buildings);
}
}
public static bool FallbackCheck(IdeoBuildingPresenceDemand __instance, IEnumerable<Building> buildings)
{
foreach (var building in buildings)
{
if (building.StyleSourcePrecept == __instance.parent)
return true;
}
return false;
}
}
}
+2 -2
View File
@@ -10,9 +10,9 @@ using StyleDominanceCache
namespace PerformanceFish;
public class IdeoUtilityCaching : ClassWithFishPrepatches
public sealed class IdeoUtilityCaching : ClassWithFishPrepatches
{
public class GetStyleDominance_Patch : FishPrepatch
public sealed class GetStyleDominance_Patch : FishPrepatch
{
public override string Description { get; }
= "Caches IdeoUtility.GetStyleDominance to only update about once every 900 ticks or so.";
@@ -3,12 +3,16 @@
// 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 PerformanceFish.ModCompatibility;
namespace PerformanceFish.JobSystem;
public class DisabledWorkTypesOptimization : ClassWithFishPatches
public sealed class DisabledWorkTypesOptimization : ClassWithFishPatches
{
public class PawnGetDisabledWorkTypes : FishPatch
public sealed class PawnGetDisabledWorkTypes : FishPatch
{
public override List<string> IncompatibleModIDs { get; } = [PackageIDs.MULTIPLAYER];
public override string? Description { get; }
= "Essentially fixes a bug in Pawn.GetDisabledWorkTypes that causes the method to discard its cache and "
+ "constantly keep recalculating. With the patch applied it'll return early when a valid cache exists.";
@@ -9,9 +9,9 @@ using Verse.AI;
namespace PerformanceFish.JobSystem;
public class GenClosestPatches : ClassWithFishPatches
public sealed class GenClosestPatches : ClassWithFishPatches
{
public class ClosestThing_Global_Patch : FishPatch
public sealed class ClosestThing_Global_Patch : FishPatch
{
public override string Description { get; } = "Threading for job scanning. Disabled and unstable.";
public override bool DefaultState => false;
@@ -226,29 +226,4 @@ public class GenClosestPatches : ClassWithFishPatches
}
}
#endif
#if threadingEnabled
public class CanReach_Patch : FishPatch
{
protected CanReach_Patch() => FishSettings.ThreadingPatches.Add(this);
public override bool DefaultState => false;
public override string Description { get; }
= "Lock for threading. Only needed with threading enabled and off by default";
public override Expression<Action> TargetMethod { get; }
= static () => default(Reachability)!.CanReach(default, null, default, default);
public static void Prefix()
{
if (FishSettings.ThreadingEnabled)
Monitor.Enter(Lock);
}
public static Exception Finalizer(Exception __exception)
{
if (FishSettings.ThreadingEnabled)
Monitor.Exit(Lock);
return __exception;
}
private static object Lock { get; } = new();
}
#endif
//}
@@ -7,10 +7,12 @@ using PerformanceFish.ModCompatibility;
namespace PerformanceFish.JobSystem;
public class JobGiver_HaulOptimization : ClassWithFishPatches
public sealed class JobGiver_HaulOptimization : ClassWithFishPatches
{
public class PotentialWorkThingsGlobal_Patch : FishPatch
public sealed class PotentialWorkThingsGlobal_Patch : FishPatch
{
public override List<string> IncompatibleModIDs { get; } = [PackageIDs.MULTIPLAYER];
public override string Description { get; }
= "Sorts haulables by distance before running expensive hauling calculations on them to avoid checks on "
+ "far away items";
@@ -20,9 +22,6 @@ public class JobGiver_HaulOptimization : ClassWithFishPatches
public static CodeInstructions? Transpiler(CodeInstructions codes, MethodBase method)
{
if (ActiveMods.Multiplayer) // PUAH has known issues with sorting and multiplayer
return codes;
var pawn_Map_listerHaulables_ThingsPotentiallyNeedingHauling
= FishTranspiler.Call(static () => default(ListerHaulables)!.ThingsPotentiallyNeedingHauling());
@@ -13,9 +13,9 @@ using static PerformanceFish.TranspilerHelpers;
namespace PerformanceFish.JobSystem;
public class JobGiver_WorkPatches : ClassWithFishPatches
public sealed class JobGiver_WorkPatches : ClassWithFishPatches
{
public class TryIssueJobPackage_InnerDelegate_Patch : FishPatch
public sealed class TryIssueJobPackage_InnerDelegate_Patch : FishPatch
{
public override string Description => "Work scanning optimization. Splits the IsForbidden and HasJobOnThing checks into separate loops, then threads the IsForbidden loop, sorts it, caches the result and skips any following duplicate checks";
public override MethodBase TargetMethodInfo => AccessTools.FindIncludingInnerTypes(typeof(JobGiver_Work),
@@ -44,7 +44,7 @@ public class JobGiver_WorkPatches : ClassWithFishPatches
}
}
public class TryIssueJobPackage_Patch : FishPatch
public sealed class TryIssueJobPackage_Patch : FishPatch
{
public override string Description => "Work scanning optimization. Splits the IsForbidden and HasJobOnThing checks into separate loops, then threads the IsForbidden loop, sorts it, caches the result and skips any following duplicate checks";
public override MethodBase TargetMethodInfo => AccessTools.Method(typeof(JobGiver_Work), nameof(JobGiver_Work.TryIssueJobPackage));
@@ -243,7 +243,7 @@ public class JobGiver_WorkPatches : ClassWithFishPatches
public static Dictionary<IEnumerable<Thing>, IEnumerable<Thing>> PrefilteredPotentialWorkThingsGlobal { get; } = new();
}
public class ThingRequest_IsUndefined_Patch : FishPatch
public sealed class ThingRequest_IsUndefined_Patch : FishPatch
{
public override string Description => "Part of the work scanning optimization";
public override MethodBase TargetMethodInfo => AccessTools.PropertyGetter(typeof(ThingRequest), nameof(ThingRequest.IsUndefined));
@@ -259,7 +259,7 @@ public class JobGiver_WorkPatches : ClassWithFishPatches
=> __instance.group == ThingRequestGroup.Undefined && !((FishCache)__instance.singleDef).IsSingleDef;
}
public class ThingRequest_CanBeFoundInRegion_Patch : FirstPriorityFishPatch
public sealed class ThingRequest_CanBeFoundInRegion_Patch : FirstPriorityFishPatch
{
public override string Description => "Part of the work scanning optimization";
public override MethodBase TargetMethodInfo => AccessTools.PropertyGetter(typeof(ThingRequest), nameof(ThingRequest.CanBeFoundInRegion));
@@ -276,7 +276,7 @@ public class JobGiver_WorkPatches : ClassWithFishPatches
|| (__instance.group != ThingRequestGroup.Undefined && (__instance.group == ThingRequestGroup.Nothing || __instance.group.StoreInRegion()));
}
public class ListerThings_ThingsMatching_Patch : FirstPriorityFishPatch
public sealed class ListerThings_ThingsMatching_Patch : FirstPriorityFishPatch
{
public override string Description => "Part of the work scanning optimization";
public override Expression<Action> TargetMethod => () => new ListerThings(default).ThingsMatching(default);
@@ -5,44 +5,55 @@
using System.Linq;
using System.Reflection.Emit;
using Mono.Cecil;
using Mono.Cecil.Cil;
using PerformanceFish.Prepatching;
using Verse.AI;
using IngredientCacheList
= PerformanceFish.Utility.KeyedList<Verse.IngredientCount, (System.Collections.Generic.List<Verse.ThingDef> Defs,
bool Found)>;
using OpCodes = System.Reflection.Emit.OpCodes;
using RecipeIngredientCache
= PerformanceFish.Cache.ByIndex<RimWorld.Bill,
PerformanceFish.JobSystem.WorkGiver_DoBillOptimization.RecipeIngredientCacheValue>;
namespace PerformanceFish.JobSystem;
public class WorkGiver_DoBillOptimization : ClassWithFishPatches
public sealed class WorkGiver_DoBillPrepatches : ClassWithFishPrepatches
{
public static List<Type?> BlackListedModExtensions
=> _blackListedModExtensions ??= new() { Reflection.Type("VFEAncients", "VFEAncients.RecipeExtension_Mend") };
public static List<Type> BlackListedModExtensions
=> _blackListedModExtensions ??= InitializeBlackListedModExtensions();
private static List<Type?>? _blackListedModExtensions;
public class TryFindBestIngredientsHelper_InnerDelegate_Patch : FirstPriorityFishPatch
[MethodImpl(MethodImplOptions.NoInlining)]
private static List<Type> InitializeBlackListedModExtensions()
{
var list = new List<Type>();
if (ModCompatibility.Types.VFEAncients.RecipeExtensionMend is { } VFEAncientsRecipeExtension)
list.Add(VFEAncientsRecipeExtension);
return list;
}
private static List<Type>? _blackListedModExtensions;
public sealed class TryFindBestIngredientsHelper_InnerDelegate_Patch : FishPrepatch
{
public override List<string> IncompatibleModIDs { get; }
= [ModCompatibility.PackageIDs.USE_BEST_MATERIALS];
public override List<Type> LinkedPatches { get; } =
[
typeof(WorkGiver_DoBillOptimization.TryFindBestIngredientsHelper_Patch),
typeof(WorkGiver_DoBillOptimization.TryFindBestIngredientsInSet_NoMixHelper_Patch)
];
public override string Description { get; }
= "Optimizes WorkGiver_DoBill to only check things that get accepted by a bill's filter for its "
+ "ingredient lookup, instead of looping over everything and then checking filters afterwards. "
+ "Also caches these filter acceptances. Large performance impact.";
public override bool Enabled
{
set
{
if (base.Enabled == value)
return;
Get<TryFindBestIngredientsHelper_Patch>().Enabled
= Get<TryFindBestIngredientsInSet_NoMixHelper_Patch>().Enabled
= base.Enabled = value;
}
}
public override MethodBase TargetMethodInfo { get; }
public override MethodBase TargetMethodBase { get; }
= AccessTools.FirstMethod(DelegateInstanceType, static m
=> m.GetParameters() is { Length: 1 } parameterInfos
&& parameterInfos[0].ParameterType == typeof(Region)
@@ -52,12 +63,15 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches
&& instruction.operand is MethodInfo info
&& info == AccessTools.Method(typeof(ListerThings), nameof(ListerThings.ThingsMatching))));
public static CodeInstructions Transpiler(CodeInstructions codeInstructions, MethodBase method)
=> Reflection.MakeReplacementCall(AccessTools.Method(
typeof(TryFindBestIngredientsHelper_InnerDelegate_Patch), nameof(Replacement),
generics: new[] { DelegateInstanceType }));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static bool Replacement<T>(T __instance, Region r) where T : class
// public static CodeInstructions Transpiler(CodeInstructions codeInstructions, MethodBase method)
// => Reflection.MakeReplacementCall(AccessTools.Method(
// typeof(TryFindBestIngredientsHelper_InnerDelegate_Patch), nameof(ReplacementBody),
// generics: new[] { DelegateInstanceType }));
public static bool ReplacementBody(object __instance, Region r)
{
var instance = _delegateInstance.Get(__instance);
var thingValidator = _thingValidatorInstance.Get(instance.ThingValidator.Target);
@@ -75,7 +89,7 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches
#if V1_4
|| bill.billStack?.billGiver is Building_MechGestator
#endif
|| (BlackListedModExtensions.Exists(static b => b != null)
|| (BlackListedModExtensions.Count > 0
&& IsBlackListed(bill))) // <-- Fuck these too
{
list = GetDefaultList(r);
@@ -89,7 +103,7 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches
cache.Update(bill);
}
list = GetList(r.listerThings.listsByDef, cache.thingDefs);
list = GetList(r, cache.thingDefs);
}
}
@@ -110,10 +124,9 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches
instance.FoundAll = true;
return true;
}
private static bool HasNoFilters(Bill bill)
public static bool HasNoFilters(Bill bill)
=> bill.ingredientFilter is null
|| bill.recipe.fixedIngredientFilter is null;
@@ -135,13 +148,13 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches
return false;
}*/
private static bool IsBlackListed(Bill? bill)
public static bool IsBlackListed(Bill? bill)
=> bill is null
|| bill.recipe.modExtensions.ExistsAndNotNull(static e
=> BlackListedModExtensions.Contains(e.GetType()));
private static List<Thing> GetDefaultList(Region r)
=> r.ListerThings.ThingsMatching(ThingRequest.ForGroup(ThingRequestGroup.HaulableEver));
public static List<Thing> GetDefaultList(Region r)
=> r.listerThings.ThingsMatching(ThingRequest.ForGroup(ThingRequestGroup.HaulableEver));
public static void InsertAtCorrectPosition(List<Thing> list, List<Thing> thingsToInsert, Pawn pawn)
{
@@ -159,7 +172,8 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches
}
}
private static readonly ThingPositionComparer _insertAtCorrectPositionComparer = new();
private static readonly WorkGiver_DoBillOptimization.ThingPositionComparer _insertAtCorrectPositionComparer
= new();
public static void ActualLoop(List<Thing> list, Region r, Pawn pawn, Predicate<Thing> baseValidator,
bool billGiverIsPawn)
@@ -182,8 +196,10 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches
}
}
public static List<Thing> GetList(Dictionary<ThingDef, List<Thing>> listsByDef, IngredientCacheList cacheList)
public static List<Thing> GetList(Region region, IngredientCacheList cacheList)
{
var listsByDef = region.listerThings.listsByDef;
var list = _tempListForIngredients;
list.Clear();
var set = _tempSetForIngredientDefs;
@@ -216,7 +232,7 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches
{
var recipe = bill.recipe;
var ingredients = recipe.ingredients;
var listsByDef = r.Map.listerThings.listsByDef;
var listsByDef = r.GetMap().listerThings.listsByDef;
var result = cacheList ?? new(3);
for (var i = 0; i < ingredients.Count;
@@ -229,7 +245,7 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches
}
else
{
result[ingredients[i]] = ingredientList = new(new(), false);
result[ingredients[i]] = ingredientList = new([], false);
}
var filter = ingredients[i].filter;
@@ -291,8 +307,8 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches
(mediumFilter, largestFilter) = (largestFilter, mediumFilter);
}
private static readonly List<Thing> _tempListForIngredients = new();
private static readonly HashSet<ThingDef> _tempSetForIngredientDefs = new();
private static readonly List<Thing> _tempListForIngredients = [];
private static readonly HashSet<ThingDef> _tempSetForIngredientDefs = [];
}
private static Type DelegateInstanceType { get; }
@@ -308,8 +324,8 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches
&& AccessTools.Field(t, "foundAllIngredientsAndChoose") != null
&& AccessTools.Field(t, "foundAll") != null
&& AccessTools.Field(t, "thingValidator") != null)
?? throw new("Performance Fish failed to find the target type for its WorkGiver_DoBill patch. "
+ "This means that specific patch won't apply.");
?? ThrowHelper.ThrowInvalidOperationException<Type>("Performance Fish failed to find the target type "
+ "for its WorkGiver_DoBill patch. This means that specific patch won't apply.");
private static Type ThingValidatorInstanceType { get; }
= AccessTools.FirstInner(typeof(WorkGiver_DoBill), static t
@@ -320,20 +336,20 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches
//&& AccessTools.Field(t, "chosen") != null
//&& AccessTools.Field(t, "billGiver") != null
/*&& AccessTools.Field(t, "pawn") != null*/)
?? throw new("Performance Fish failed to find the thingValidator type for its WorkGiver_DoBill patch. "
+ "This likely flat out breaks the entire patch.");
?? ThrowHelper.ThrowInvalidOperationException<Type>("Performance Fish failed to find the thingValidator "
+ "type for its WorkGiver_DoBill patch. This likely flat out breaks the entire patch.");
// ReSharper disable once InconsistentNaming
private static DelegateInstance _delegateInstance { get; }
public static DelegateInstance _delegateInstance { get; }
= (DelegateInstance)Activator.CreateInstance(
typeof(DelegateInstanceImplementation<>).MakeGenericType(DelegateInstanceType));
// ReSharper disable once InconsistentNaming
private static ThingValidatorInstance _thingValidatorInstance { get; }
public static ThingValidatorInstance _thingValidatorInstance { get; }
= (ThingValidatorInstance)Activator.CreateInstance(
typeof(ThingValidatorInstanceImplementation<>).MakeGenericType(ThingValidatorInstanceType));
private abstract class DelegateInstance
public abstract class DelegateInstance
{
public abstract DelegateInstance Get(object obj);
@@ -347,7 +363,7 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches
public abstract Predicate<Thing> ThingValidator { get; set; }
}
private class DelegateInstanceImplementation<T> : DelegateInstance where T : class
private sealed class DelegateInstanceImplementation<T> : DelegateInstance where T : class
{
#pragma warning disable CS8618
private T _instance;
@@ -430,7 +446,7 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches
= AccessTools.FieldRefAccess<T, Predicate<Thing>>("thingValidator");
}
private abstract class ThingValidatorInstance
public abstract class ThingValidatorInstance
{
public abstract ThingValidatorInstance? Get(object obj);
@@ -438,7 +454,7 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches
//public abstract Thing BillGiver { get; set; }
}
private class ThingValidatorInstanceImplementation<T> : ThingValidatorInstance where T : class
private sealed class ThingValidatorInstanceImplementation<T> : ThingValidatorInstance where T : class
{
#pragma warning disable CS8618
private T _instance;
@@ -467,27 +483,21 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches
private static AccessTools.FieldRef<T, Bill> _bill = AccessTools.FieldRefAccess<T, Bill>("bill");
//private static AccessTools.FieldRef<T, Thing> _billGiver = AccessTools.FieldRefAccess<T, Thing>("billGiver");
}
}
public class TryFindBestIngredientsHelper_Patch : FishPatch
public sealed class WorkGiver_DoBillOptimization : ClassWithFishPatches
{
public sealed class TryFindBestIngredientsHelper_Patch : FishPatch
{
public override bool ShowSettings => false;
public override string Description { get; } = "Part of the DoBill optimization";
public override bool Enabled
{
set
{
if (base.Enabled == value)
return;
Get<TryFindBestIngredientsInSet_NoMixHelper_Patch>().Enabled = base.Enabled = value;
}
}
public override Delegate TargetMethodGroup { get; } = WorkGiver_DoBill.TryFindBestIngredientsHelper;
public static void Postfix(Predicate<Thing> thingValidator)
{
var thingValidatorInstance = _thingValidatorInstance.Get(thingValidator.Target);
var thingValidatorInstance = WorkGiver_DoBillPrepatches._thingValidatorInstance.Get(thingValidator.Target);
if (thingValidatorInstance is null)
return;
@@ -505,8 +515,10 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches
}
}
public class TryFindBestIngredientsInSet_NoMixHelper_Patch : FishPatch
public sealed class TryFindBestIngredientsInSet_NoMixHelper_Patch : FishPatch
{
public override List<Type> LinkedPatches { get; } = [typeof(TryFindBestIngredientsHelper_Patch)];
public override string Description { get; }
= "Part of the DoBill optimization. Bills that need multiple ingredients, like for example steel and "
+ "components for guns, normally scan their surroundings for all those ingredients and count them, until "
@@ -515,17 +527,6 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches
+ "case of steel and components that could mean not having to go over 100s of steel stacks at times. "
+ "Prevents large spikes from these situations, does nothing for all other bills.";
public override bool Enabled
{
set
{
if (base.Enabled == value)
return;
Get<TryFindBestIngredientsHelper_Patch>().Enabled = base.Enabled = value;
}
}
public override Delegate TargetMethodGroup { get; } = WorkGiver_DoBill.TryFindBestIngredientsInSet_NoMixHelper;
public static void Prefix(ref bool alreadySorted, Bill? bill)
@@ -549,16 +550,15 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches
&& codes[i + 1] == FishTranspiler.Constant(1) // i++;
&& codes[i + 2] == FishTranspiler.Add
&& codes[i + 3] == i_variable.Store(),
code
=> new[]
{
code =>
[
new(OpCodes.Br_S, new_i_label), // else
FishTranspiler.FirstLocalVariable(method, typeof(IngredientCount))
.WithLabels(code.ExtractLabels()),
FishTranspiler.FirstArgument(method, typeof(Bill)),
FishTranspiler.Call(MarkIngredientCountAsFound),
code.WithLabels(new_i_label)
});
]);
// if (!flag)
// {
@@ -599,7 +599,7 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches
}
}
public class ThingPositionComparer : IComparer<Thing>
public sealed class ThingPositionComparer : IComparer<Thing>
{
public IntVec3 rootCell;
@@ -7,11 +7,11 @@ using PerformanceFish.ModCompatibility;
namespace PerformanceFish.JobSystem;
public class WorkGiver_HaulOptimization : ClassWithFishPatches
public sealed class WorkGiver_HaulOptimization : ClassWithFishPatches
{
public class PotentialWorkThingsGlobal_Patch : FishPatch
public sealed class PotentialWorkThingsGlobal_Patch : FishPatch
{
public override bool Enabled => base.Enabled && !ActiveMods.Multiplayer;
public override List<string> IncompatibleModIDs { get; } = [PackageIDs.MULTIPLAYER];
public override string Description { get; }
= "Sorts haulables by distance before running expensive hauling calculations on them to avoid checks on "
@@ -7,9 +7,9 @@ using Verse.AI;
namespace PerformanceFish.JobSystem;
public class WorkGiver_MergeOptimization : ClassWithFishPatches
public sealed class WorkGiver_MergeOptimization : ClassWithFishPatches
{
public class JobOnThing_Patch : FishPatch
public sealed class JobOnThing_Patch : FishPatch
{
public override string Description { get; }
= "Removes mergeables from ListerMergeables if they no longer qualify as mergeable when trying to check "
@@ -23,14 +23,16 @@ public class WorkGiver_MergeOptimization : ClassWithFishPatches
if (t.GetSlotGroup() is not null)
return true;
RemoveFromListerMergeables(t);
TryRemoveFromListerMergeables(t);
__result = null;
return false;
}
private static void RemoveFromListerMergeables(Thing t)
private static void TryRemoveFromListerMergeables(Thing t)
{
var map = t.MapHeld;
if (t.TryGetMapHeld() is not { } map)
return;
map.listerMergeables.TryRemove(t);
map.listerHaulables.Check(t);
}
@@ -0,0 +1,26 @@
// Copyright (c) 2023 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 JetBrains.Annotations;
using PerformanceFish.Listers;
namespace PerformanceFish;
[PublicAPI]
public static class ListerThingsExtensions
{
public static void RemoveFromGroup(this ListerThings listerThings, Thing thing, ThingRequestGroup group)
=> ThingsPrepatches.RemoveFromGroupList(listerThings, thing, group);
public static void AddToGroup(this ListerThings listerThings, Thing thing, ThingRequestGroup group)
=> ThingsPrepatches.AddToGroupList(listerThings, thing, group);
public static void RemoveFromDefList(this ListerThings listerThings, Thing thing)
=> ThingsPrepatches.RemoveFromDefList(listerThings, thing);
public static void AddToDefList(this ListerThings listerThings, Thing thing)
=> ThingsPrepatches.AddToDefList(listerThings, thing);
}
+195 -249
View File
@@ -3,300 +3,246 @@
// 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.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
using PerformanceFish.Prepatching;
namespace PerformanceFish.Listers;
public class Buildings : ClassWithFishPatches
public sealed class Buildings : ClassWithFishPrepatches
{
/// <summary>
/// Get Things from the ListerThings dictionary with a rather quick lookup instead of looping over every single element in the lists of listerBuildings
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static List<Thing> ThingsOfDefFast(ListerThings listerThings, ThingDef def)
=> listerThings.listsByDef.TryGetValue(def) ?? ListerThings.EmptyList;
public class ColonistsHaveBuilding_Patch : FishPatch
public sealed class AllBuildingsColonistOfClassPatch : FishPrepatch
{
public override string Description { get; }
= "ListerBuildings optimization. Should be relatively riskfree, but doesn't impact performance much "
+ "outside of a few prevented spikes here and there either";
= "Optimizes building lookups for generic types on ListerBuildings to directly return the result of cached "
+ "data instead of iterating over all buildings on the map trying to find all matches";
public override Expression<Action> TargetMethod { get; }
= static () => default(ListerBuildings)!.ColonistsHaveBuilding(default(ThingDef));
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ListerBuildings), nameof(ListerBuildings.AllBuildingsColonistOfClass));
public static bool Prefix(ListerBuildings __instance, ref bool __result, ThingDef def)
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody<Building>);
public static IEnumerable<T> ReplacementBody<T>(ListerBuildings __instance) where T : Building
=> (IEnumerable<T>)__instance.Cache().ColonistBuildingsByType.GetOrAdd(typeof(T));
}
public sealed class ColonistsHaveResearchBenchPatch : FishPrepatch
{
var lister = GetListerThings(__instance);
if (lister is null)
return true;
public override string Description { get; }
= "Optimizes research bench lookups on ListerBuildings to directly return the result of cached data "
+ "instead of iterating over all buildings on the map trying to find the bench";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ListerBuildings), nameof(ListerBuildings.ColonistsHaveResearchBench));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static bool ReplacementBody(ListerBuildings __instance)
=> __instance.Cache().ColonistResearchBenches.Count > 0;
}
public sealed class ColonistsHaveBuildingPatch : FishPrepatch
{
public override string Description { get; }
= "Optimizes building lookups by def on ListerBuildings to directly return the result of cached "
+ "data instead of iterating over all buildings on the map trying to find any matches";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ListerBuildings), nameof(ListerBuildings.ColonistsHaveBuilding),
[typeof(ThingDef)]);
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static bool ReplacementBody(ListerBuildings __instance, ThingDef def)
=> __instance.Cache().ColonistBuildingsByDef.GetOrAdd(def).Count > 0;
}
public sealed class AllBuildingsColonistOfDef_Patch : FishPrepatch
{
public override string Description { get; }
= "Optimizes building lookups by def on ListerBuildings to directly return the result of cached "
+ "data instead of iterating over all buildings on the map trying to find all matches. Relatively large "
+ "performance impact when having water power plants on the map";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ListerBuildings), nameof(ListerBuildings.AllBuildingsColonistOfDef));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static IEnumerable<Building> ReplacementBody(ListerBuildings __instance, ThingDef def)
=> __instance.Cache().ColonistBuildingsByDef.GetOrAdd(def);
}
public sealed class AllBuildingsNonColonistOfDef_Patch : FishPrepatch
{
public override string Description { get; }
= "Optimizes building lookups by def on ListerBuildings to directly return the result of cached "
+ "data instead of iterating over all buildings on the map trying to find all matches";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ListerBuildings), nameof(ListerBuildings.AllBuildingsNonColonistOfDef));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static IEnumerable<Building> ReplacementBody(ListerBuildings __instance, ThingDef def)
=> __instance.Cache().NonColonistBuildingsByDef.GetOrAdd(def);
}
public sealed class ColonistsHaveBuildingWithPowerOnPatch : FishPrepatch
{
public override string Description { get; }
= "Optimizes lookups for powered buildings of a given def on ListerBuildings to directly return the result "
+ "of cached data instead of iterating over all buildings on the map trying to find any matches";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ListerBuildings),
nameof(ListerBuildings.ColonistsHaveBuildingWithPowerOn));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static bool ReplacementBody(ListerBuildings __instance, ThingDef def)
{
var colonistBuildingsOfDef = __instance.Cache().ColonistBuildingsByDef.GetOrAdd(def);
for (var i = 0; i < colonistBuildingsOfDef.Count; i++)
{
if (colonistBuildingsOfDef[i].TryGetComp<CompPowerTrader>() is not { PowerOn: false })
return true;
}
__result = ContainsDefForColonists(lister, def);
return false;
}
}
public class ColonistsHaveResearchBench_Patch : FishPatch
public sealed class AddPatch : FishPrepatch
{
public override string Description { get; }
= "ListerBuildings optimization. Should be relatively riskfree, but doesn't impact performance much "
+ "outside of a few prevented spikes here and there either";
public override List<Type> LinkedPatches { get; } =
[
typeof(AllBuildingsColonistOfClassPatch), typeof(ColonistsHaveResearchBenchPatch),
typeof(ColonistsHaveBuildingPatch), typeof(AllBuildingsColonistOfDef_Patch),
typeof(ColonistsHaveBuildingWithPowerOnPatch),
typeof(DeepResourceGridOptimization.AnyActiveDeepScannersOnMapPatch)
];
public override Expression<Action> TargetMethod { get; }
= static () => default(ListerBuildings)!.ColonistsHaveResearchBench();
public override string? Description { get; }
= "Required for all other ListerBuildings patches. This assigns buildings to the cache when they're "
+ "spawned";
public static bool Prefix(ListerBuildings __instance, ref bool __result)
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ListerBuildings), nameof(ListerBuildings.Add));
public static void Postfix(ListerBuildings __instance, Building b)
{
var lister = GetListerThings(__instance);
if (lister is null)
return true;
var def = b.def;
var researchBenches = LazyInit.ResearchBenches;
for (var i = 0; i < researchBenches.Length; i++)
if (def.building is { isNaturalRock: true })
return;
var cache = __instance.Cache();
if (b.Faction == Faction.OfPlayer)
{
if (!ContainsDefForColonists(lister, researchBenches[i]))
continue;
cache.ColonistBuildingsByDef.GetOrAdd(def).Add(b);
if (b is Building_ResearchBench researchBench)
cache.ColonistResearchBenches.Add(researchBench);
__result = true;
if (b.TryGetComp<CompDeepScanner>() != null)
cache.ColonistDeepScanners.Add(b);
var buildingType = b.GetType();
do
{
if (buildingType == null)
break;
cache.ColonistBuildingsByType.GetOrAdd(buildingType).Add(b);
}
return false;
}
public static class LazyInit
{
public static ThingDef[] ResearchBenches = DefDatabase<ThingDef>.AllDefsListForReading
.Where(static def => def.thingClass == typeof(Building_ResearchBench)).ToArray();
static LazyInit()
{
// beforefieldinit
}
}
}
public class ColonistsHaveBuildingWithPowerOn_Patch : FishPatch
{
public override string Description { get; }
= "ListerBuildings optimization. Should be relatively riskfree, but doesn't impact performance much "
+ "outside of a few prevented spikes here and there either";
public override Expression<Action> TargetMethod { get; }
= static () => default(ListerBuildings)!.ColonistsHaveBuildingWithPowerOn(null);
public static bool Prefix(ListerBuildings __instance, ref bool __result, ThingDef def)
{
var lister = GetListerThings(__instance);
if (lister is null)
return true;
var things = ThingsOfDefFast(lister, def);
var playerFaction = Faction.OfPlayerSilentFail;
for (var i = 0; i < things.Count; i++)
{
if (things[i].Faction != playerFaction
|| things[i].TryGetComp<CompPowerTrader>() is { PowerOn: false })
{
continue;
}
__result = true;
break;
}
return false;
}
}
public class AllBuildingsColonistOfDef_Patch : FishPatch
{
public override string Description { get; }
= "ListerBuildings optimization. Relatively large performance impact when having water power plants on the "
+ "map.";
public override Expression<Action> TargetMethod { get; }
= static () => default(ListerBuildings)!.AllBuildingsColonistOfDef(null);
public static bool Prefix(ListerBuildings __instance, ref IEnumerable<Building> __result, ThingDef def)
{
var lister = GetListerThings(__instance);
if (lister is null)
return true;
__result = AllBuildingsColonistOfDefReplacement(lister, def);
return false;
}
public static IEnumerable<Building> AllBuildingsColonistOfDefReplacement(ListerThings lister, ThingDef def)
{
var things = ThingsOfDefFast(lister, def);
var playerFaction = Faction.OfPlayerSilentFail;
for (var i = 0; i < things.Count; i++)
{
if (things[i] is Building building)
{
if (building.Faction == playerFaction)
yield return building;
while ((buildingType = buildingType.BaseType) != typeof(Building));
}
else
{
ThrowWarning(def, things[i]);
yield break;
}
cache.NonColonistBuildingsByDef.GetOrAdd(def).Add(b);
}
}
}
public record struct AllBuildingsColonistOfDefCacheValue
public sealed class RemovePatch : FishPrepatch
{
public List<Building> Buildings;
public override List<Type> LinkedPatches { get; } =
[
typeof(AllBuildingsColonistOfClassPatch), typeof(ColonistsHaveResearchBenchPatch),
typeof(ColonistsHaveBuildingPatch), typeof(AllBuildingsColonistOfDef_Patch),
typeof(ColonistsHaveBuildingWithPowerOnPatch),
typeof(DeepResourceGridOptimization.AnyActiveDeepScannersOnMapPatch)
];
public override string? Description { get; }
= "Required for all other ListerBuildings patches. This removes buildings from the map when they're "
+ "despawned";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ListerBuildings), nameof(ListerBuildings.Remove));
public static void Postfix(ListerBuildings __instance, Building b)
{
var def = b.def;
if (def.building is { isNaturalRock: true })
return;
var cache = __instance.Cache();
if (b.Faction == Faction.OfPlayer)
{
cache.ColonistBuildingsByDef.GetOrAdd(def).Remove(b);
if (b is Building_ResearchBench researchBench)
cache.ColonistResearchBenches.Remove(researchBench);
cache.ColonistDeepScanners.Remove(b);
var buildingType = b.GetType();
do
{
if (buildingType == null)
break;
cache.ColonistBuildingsByType.GetOrAdd(buildingType).Remove(b);
}
public class AllBuildingsNonColonistOfDef_Patch : FishPatch
{
public override string Description { get; }
= "ListerBuildings optimization. Should be relatively riskfree, but doesn't impact performance much "
+ "outside of a few prevented spikes here and there either";
public override Expression<Action> TargetMethod { get; }
= static () => default(ListerBuildings)!.AllBuildingsNonColonistOfDef(null);
public static bool Prefix(ListerBuildings __instance, ref IEnumerable<Building> __result, ThingDef def)
{
var lister = GetListerThings(__instance);
if (lister is null)
return true;
__result = AllBuildingsNonColonistOfDefReplacement(lister, def);
return false;
}
public static IEnumerable<Building> AllBuildingsNonColonistOfDefReplacement(ListerThings lister, ThingDef def)
{
var things = ThingsOfDefFast(lister, def);
var playerFaction = Faction.OfPlayerSilentFail;
for (var i = 0; i < things.Count; i++)
{
if (things[i] is Building building)
{
if (building.Faction != playerFaction)
yield return building;
while ((buildingType = buildingType.BaseType) != typeof(Building));
}
else
{
ThrowWarning(def, things[i]);
yield break;
}
cache.NonColonistBuildingsByDef.GetOrAdd(def).Remove(b);
}
}
}
#if harmonyIsFailingOnThisOne
public class AllBuildingsColonistOfClass_Patch : FishPatch
public readonly record struct Cache
{
public override string Description { get; }
= "ListerBuildings optimization. Should be relatively riskfree, but doesn't impact performance much "
+ "outside of a few prevented spikes here and there either";
public readonly FishTable<ThingDef, IndexedFishSet<Building>>
ColonistBuildingsByDef = new(),
NonColonistBuildingsByDef = new();
public override Expression<Action> TargetMethod { get; }
= static () => default(ListerBuildings)!.AllBuildingsColonistOfClass<Building>();
public readonly IndexedFishSet<Building_ResearchBench> ColonistResearchBenches = [];
public static bool Prefix(ListerBuildings __instance, MethodBase __originalMethod, ref IEnumerable __result)
public readonly IndexedFishSet<Building> ColonistDeepScanners = [];
public readonly FishTable<Type, IList> ColonistBuildingsByType = new()
{
var lister = GetListerThings(__instance);
if (lister is null)
return true;
ValueInitializer = static type
=> (IList)Activator.CreateInstance(typeof(IndexedFishSet<>).MakeGenericType(type))
};
var type = __originalMethod.GetGenericArguments()[0]; //<-- This always returns Building instead of the
// type it's actually meant to look for
DebugLog.Message($"AllBuildingsColonistOfClass running for type {type.Name}");
if (!Replacements.TryGetValue(type, out var replacement))
public Cache()
{
Replacements.Add(type,
replacement = (GenericReplacementBase)Activator.CreateInstance(
typeof(GenericReplacement<>).MakeGenericType(type)));
}
__result = replacement.Replacement(lister);
return false;
}
public static Dictionary<Type, GenericReplacementBase> Replacements { get; } = new();
public abstract class GenericReplacementBase
{
public abstract IEnumerable Replacement(ListerThings lister);
}
public class GenericReplacement<T> : GenericReplacementBase where T : Building
{
public override IEnumerable Replacement(ListerThings lister)
{
buildingsOfClass ??= DefDatabase<ThingDef>.AllDefsListForReading
.Where(static def => def.thingClass == typeof(T)).ToArray();
foreach (var def in buildingsOfClass)
{
var things = ThingsOfDefFast(lister, def);
for (var i = 0; i < things.Count; i++)
{
if (things[i].Faction == Faction.OfPlayerSilentFail)
yield return things[i];
}
}
}
public ThingDef[]? buildingsOfClass;
}
}
#endif
public class Map_Patch : FishPatch
{
public override string Description { get; }
= "Part of the ListerBuildings optimizations. Without this patch mods like Colony Manager throw errors "
+ "during loading";
public override MethodBase TargetMethodInfo { get; } = AccessTools.Constructor(typeof(Map));
public static void Postfix(Map __instance) => _tempMap = __instance;
}
private static Map? _tempMap;
// mapComps get constructed before maps are added to Game.Maps, so this makes the patches work there too
public static void ThrowWarning(ThingDef def, Thing thing)
=> Log.Warning($"Tried to get building of def {def}, but its type is {thing.GetType()} instead of Building");
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ListerThings? GetListerThings(ListerBuildings listerBuildings)
{
var maps = Current.gameInt.Maps;
for (var i = 0; i < maps.Count; i++)
{
if (maps[i].listerBuildings == listerBuildings)
return maps[i].listerThings;
}
return GetListerThingsUsingTempMapFallback(listerBuildings);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static ListerThings? GetListerThingsUsingTempMapFallback(ListerBuildings listerBuildings)
=> _tempMap!.listerBuildings == listerBuildings ? _tempMap.listerThings : null;
public static bool ContainsDefForColonists(ListerThings listerThings, ThingDef def)
{
if (!listerThings.listsByDef.TryGetValue(def, out var things))
return false;
var playerFaction = Faction.OfPlayerSilentFail;
for (var i = 0; i < things.Count; i++)
{
if (things[i].Faction == playerFaction)
return true;
}
return false;
}
}
+125 -323
View File
@@ -3,375 +3,177 @@
// 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/.
global using HaulablesCache
= PerformanceFish.Cache.ByReference<Verse.Thing, RimWorld.ListerHaulables,
PerformanceFish.Listers.Haulables.HaulablesCacheValue>;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Reflection.Emit;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using PerformanceFish.Cache;
using PerformanceFish.ModCompatibility;
using Mono.Cecil;
using Mono.Cecil.Cil;
using MonoMod.Utils;
using PerformanceFish.Prepatching;
namespace PerformanceFish.Listers;
public class Haulables : ClassWithFishPatches
public sealed class Haulables : ClassWithFishPrepatches
{
public class MapPreTick_Patch : FishPatch
public sealed class CheckPatch : FishPrepatch
{
public override bool Enabled => base.Enabled && !ActiveMods.Multiplayer;
public override string? Description { get; }
= "Optimizes the ListerHaulables by making frequent checks that happen every tick to only check for adding "
+ "new haulables, skipping things that are already inside the lister. This generally results in them "
+ "getting removed when actively checked by pawns or otherwise getting destroyed instead, which happens "
+ "far less frequently. Hauling behaviour visible to the player remains identical";
public override string Description { get; }
= "Throttling of the ListerHaulablesTick. This marks whether items should be moved from one stockpile to "
+ "another. When throttled these checks get spread out more evenly, helping against spikes that can "
+ "happen with large deep storage containers.";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ListerHaulables), nameof(ListerHaulables.Check));
public override Expression<Action> TargetMethod { get; } = static () => default(Map)!.MapPreTick();
public static CodeInstructions Transpiler(CodeInstructions codes)
=> codes.MethodReplacer(new ListerHaulables(null).ListerHaulablesTick,
ThreadedOrThrottled_ListerHaulablesTick);
public static void ThreadedOrThrottled_ListerHaulablesTick(ListerHaulables instance)
{
MainThread = Environment.CurrentManagedThreadId;
if (false && FishSettings.ThreadingEnabled)
Threaded_ListerHaulablesTick(instance);
else
Throttled_ListerHaulablesTick(instance);
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(CheckAddPatch.ReplacementBody);
}
//disabled
public static void Threaded_ListerHaulablesTick(ListerHaulables instance)
public sealed class CheckAddPatch : FishPrepatch
{
DequeueQueuedThings(instance);
public override string? Description { get; }
= "Caches haulables in a fast custom hash set to not have to look them up in a large list";
var currentCellCheckCount = GetCellCheckCount(instance);
if (currentCellCheckCount is null)
public override List<Type> LinkedPatches { get; } = [typeof(TryRemovePatch)];
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ListerHaulables), nameof(ListerHaulables.CheckAdd));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static void ReplacementBody(ListerHaulables __instance, Thing t)
{
if (!t.IsItem())
return;
if (_currentTask is { IsCompleted: false })
{
if (currentCellCheckCount.value > 1)
currentCellCheckCount.value--;
if (Volatile.Read(ref _queue) == 0)
{
Interlocked.Increment(ref _queue);
_currentTask = _currentTask.ContinueWith(t =>
{
if (t.Exception is { } taskException)
Log.Error(taskException.ToString());
var haulables = __instance.haulables;
lock (Lock)
{
_skippedTicks = 0;
Interlocked.Decrement(ref _queue);
ListerHaulablesTick_Patch.CurrentCellCheckCount = currentCellCheckCount.value;
instance.ListerHaulablesTick();
}
});
}
else
{
if (_skippedTicks >= 9)
{
Log.Warning("Skipped 10 or more ListerHaulables ticks. This often means there's some "
+ "kind of hauling error happening");
_skippedTicks = 0;
}
else
{
_skippedTicks++;
}
}
ref var cache = ref __instance.Cache();
cache.CheckUpdate(haulables);
return;
}
else
{
if (currentCellCheckCount.value < ListerHaulables.CellsPerTick)
currentCellCheckCount.value++;
}
if (_currentTask?.Exception is { } exception)
Log.Error(exception.ToString());
_currentTask = Task.Run(() =>
{
lock (Lock)
{
_skippedTicks = 0;
ListerHaulablesTick_Patch.CurrentCellCheckCount = currentCellCheckCount.value;
instance.ListerHaulablesTick();
}
});
}
public static void DequeueQueuedThings(ListerHaulables instance)
{
while (ThingsToRemove.TryDequeue(out var thing))
TryRemove(instance, thing);
while (ThingsToAdd.TryDequeue(out var thing))
TryAdd(instance, thing);
}
public static void Throttled_ListerHaulablesTick(ListerHaulables instance)
{
var cellCheckCount = GetCellCheckCount(instance);
if (cellCheckCount is null)
var haulablesInCache = cache.ContainedThings;
var thingKey = t.GetKey();
if (haulablesInCache.Contains(thingKey))
return;
ListerHaulablesTick_Patch.CurrentCellCheckCount = cellCheckCount.value;
Stopwatch.Start();
try
{
instance.ListerHaulablesTick();
}
catch (Exception ex)
{
Log.Error(ex.ToString());
}
Stopwatch.Stop();
if (Stopwatch.ElapsedTicks > 4000L)
{
if (cellCheckCount.value > 0)
cellCheckCount.value--;
}
else
{
if (cellCheckCount.value < 4)
cellCheckCount.value++;
}
Stopwatch.Reset();
}
private static ListerHaulablesTick_Patch.CellCheckCount? GetCellCheckCount(ListerHaulables instance)
{
var groupCycleIndex = ListerHaulables.groupCycleIndex + 1;
if (groupCycleIndex >= int.MaxValue)
groupCycleIndex = 0;
if (groupCycleIndex == _lastGroupCycleIndex)
{
groupCycleIndex++;
if (groupCycleIndex >= int.MaxValue)
groupCycleIndex = 0;
}
_lastGroupCycleIndex = groupCycleIndex;
var allGroupsListForReading = instance.map.haulDestinationManager.AllGroupsListForReading;
if (allGroupsListForReading.Count == 0)
return null;
var num = groupCycleIndex % allGroupsListForReading.Count;
var slotGroup = allGroupsListForReading[num];
return slotGroup.CellsList.Count == 0
? null
: ListerHaulablesTick_Patch.CellCheckCountsBySlotGroup.GetOrCreateValue(slotGroup);
}
public static int MainThread { get; private set; }
public static ConcurrentQueue<Thing> ThingsToRemove { get; } = new();
public static ConcurrentQueue<Thing> ThingsToAdd { get; } = new();
private static Stopwatch Stopwatch { get; } = new();
private static int _lastGroupCycleIndex;
private static int _skippedTicks;
private static int _queue;
private static Task? _currentTask;
private static object Lock { get; } = new();
}
public class ListerHaulablesTick_Patch : FishPatch
{
public override bool Enabled => base.Enabled && !ActiveMods.Multiplayer;
public override string Description { get; } = "Part of the ListerHaulables throttling optimization";
public override Expression<Action> TargetMethod { get; }
= static () => default(ListerHaulables)!.ListerHaulablesTick();
public static CodeInstructions Transpiler(CodeInstructions codes)
=> codes //.MethodReplacer(GridsUtility.GetThingList, GetCopyOfThingList)
.Manipulator(static c => c.LoadsConstant(4), static c =>
{
c.opcode = OpCodes.Call;
c.operand = typeof(ListerHaulablesTick_Patch).GetProperty(nameof(CurrentCellCheckCount))!.GetMethod;
});
/*public static List<Thing> GetCopyOfThingList(IntVec3 c, Map map)
{
if (FishSettings.ThreadingEnabled)
{
PrivateThingList.ReplaceContentsWith(map.thingGrid.ThingsListAt(c));
return PrivateThingList;
}
else
{
return map.thingGrid.ThingsListAt(c);
}
}*/
public static int CurrentCellCheckCount { get; set; } = ListerHaulables.CellsPerTick;
public static ConditionalWeakTable<SlotGroup, CellCheckCount> CellCheckCountsBySlotGroup { get; } = new();
public class CellCheckCount
{
public int value = ListerHaulables.CellsPerTick;
}
//private static List<Thing> PrivateThingList { get; } = new();
}
public static void TryAdd(ListerHaulables instance, Thing t)
{
ref var cache = ref HaulablesCache.GetAndCheck<HaulablesCacheValue>(t, instance);
if (cache.Contains)
if (!__instance.ShouldBeHaulable(t))
return;
if (!instance.haulables.Contains(t))
instance.haulables.Add(t);
cache.SetValue(true, t);
}
public static void ThreadSafeTryAdd(ListerHaulables instance, Thing t)
{
if (Environment.CurrentManagedThreadId == MapPreTick_Patch.MainThread || !FishSettings.ThreadingEnabled)
{
TryAdd(instance, t);
}
else
{
if (!HaulablesCache.GetAndCheck<HaulablesCacheValue>(t, instance).Contains)
MapPreTick_Patch.ThingsToAdd.Enqueue(t);
haulables.Add(t);
haulablesInCache.Add(thingKey);
}
}
public static void TryRemove(ListerHaulables instance, Thing t)
public sealed class TryRemovePatch : FishPrepatch
{
ref var cache = ref HaulablesCache.GetAndCheck<HaulablesCacheValue>(t, instance);
if (!cache.Contains)
public override string? Description { get; }
= "Caches haulables in a fast custom hash set to not have to look them up in a large list";
public override List<Type> LinkedPatches { get; } = [typeof(CheckAddPatch)];
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ListerHaulables), nameof(ListerHaulables.TryRemove));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static void ReplacementBody(ListerHaulables __instance, Thing t)
{
if (t.def.category != ThingCategory.Item)
return;
instance.haulables.Remove(t);
cache.SetValue(false, t);
}
var haulables = __instance.haulables;
public static void ThreadSafeTryRemove(ListerHaulables instance, Thing t)
{
if (Environment.CurrentManagedThreadId == MapPreTick_Patch.MainThread || !FishSettings.ThreadingEnabled)
{
TryRemove(instance, t);
}
else
{
if (HaulablesCache.GetAndCheck<HaulablesCacheValue>(t, instance).Contains)
MapPreTick_Patch.ThingsToRemove.Enqueue(t);
ref var cache = ref __instance.Cache();
cache.CheckUpdate(haulables);
if (cache.ContainedThings.Remove(t.GetKey()))
haulables.Remove(t);
}
}
public class Check_Patch : FishPatch
public sealed class TickPatch : FishPrepatch
{
public override string Description { get; } = "Optimization for haulables checking";
public override Expression<Action> TargetMethod { get; } = static () => default(ListerHaulables)!.Check(null);
public override string? Description { get; }
= "Essentially fixes a bug that was causing the method to tick cells multiple times for storages with less "
+ "than 4 cells. Not ticking multiple times leads to a performance improvement";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ListerHaulables), nameof(ListerHaulables.ListerHaulablesTick));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
{
var instructions = ilProcessor.instructions;
var fourInstructionIndex = instructions.FirstIndexOf(static code
=> code.OpCode == OpCodes.Ldc_I4_4 || (code.OpCode == OpCodes.Ldc_I4 && code.Operand is 4));
ilProcessor.InsertRange(fourInstructionIndex + 1,
(OpCodes.Ldloc,
ilProcessor.Body.Variables.First(static local => local.VariableType.Is(typeof(SlotGroup)))),
(OpCodes.Call, methodof(GetCellCountToTick)));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Check(ListerHaulables instance, Thing t)
{
// So the situation here is this: Vanilla RimWorld normally checks 4 storage cells per tick for valid haul
// destinations, usually with the intent of reaching higher priority storage. This check itself can get
// quite expensive, but 4 of them aren't at all enough to be worth bothering with. Now problems arise when
// LWM's deep storage comes into play: You put 5 stacks of items in one cell and suddenly 4 checks turn
// into 20. 20 hauling checks in turn is a number large enough to cause performance issues in colonies with
// high wealth.
if (!t.def.alwaysHaulable && !t.def.designateHaulable)
return;
if (instance.ShouldBeHaulable(t))
ThreadSafeTryAdd(instance, t);
else
ThreadSafeTryRemove(instance, t);
public static int GetCellCountToTick(int original, SlotGroup slotGroup)
=> Math.Min(original, slotGroup.CellsList.Count);
}
public static CodeInstructions Transpiler(CodeInstructions codeInstructions, ILGenerator generator)
=> Reflection.GetCodeInstructions(Check, generator);
}
public class TryRemove_Patch : FishPatch
public record struct Cache
{
public override string Description { get; } = "Part of the ListerHaulables optimization";
public override Expression<Action> TargetMethod { get; }
= static () => default(ListerHaulables)!.TryRemove(null);
public readonly FishSet<int> ContainedThings = [];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void TryRemove(ListerHaulables instance, Thing t)
public void CheckUpdate(List<Thing> haulables)
{
if (t.def.category == ThingCategory.Item)
ThreadSafeTryRemove(instance, t);
if (IsDirty(haulables))
Update(haulables);
}
public static CodeInstructions Transpiler(CodeInstructions codeInstructions, ILGenerator generator)
=> Reflection.GetCodeInstructions(TryRemove, generator);
}
public class CheckAdd_Patch : FishPatch
{
public override string Description { get; } = "Part of the ListerHaulables optimization";
public override Expression<Action> TargetMethod { get; }
= static () => default(ListerHaulables)!.CheckAdd(null);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void CheckAdd(ListerHaulables instance, Thing t)
{
if (!t.def.alwaysHaulable && !t.def.designateHaulable)
return;
public bool IsDirty(List<Thing> haulables) => haulables.Count != ContainedThings.Count;
if (HaulablesCache.GetAndCheck<HaulablesCacheValue>(t, instance).Contains
|| !instance.ShouldBeHaulable(t))
[MethodImpl(MethodImplOptions.NoInlining)]
public void Update(List<Thing> haulables)
{
return;
ContainedThings.Clear();
for (var i = haulables.Count; i-- > 0;)
ContainedThings.Add(haulables[i].GetKey());
}
ThreadSafeTryAdd(instance, t);
}
public static CodeInstructions Transpiler(CodeInstructions codeInstructions, ILGenerator generator)
=> Reflection.GetCodeInstructions(CheckAdd, generator);
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public record struct HaulablesCacheValue : ICacheable<HaulablesCache>
public Cache()
{
private int _nextRefreshTick;
public bool Contains;
public void Update(ref HaulablesCache key)
{
Contains = key.Second.haulables.Contains(key.First);
SetDirty(false, key.First.thingIDNumber);
}
public void SetValue(bool contains, Thing thing)
{
Contains = contains;
SetDirty(false, thing.thingIDNumber);
}
public void SetDirty(bool value, int offset)
=> _nextRefreshTick = value ? 0 : TickHelper.Add(3072, offset, 2048);
public bool Dirty
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => TickHelper.Past(_nextRefreshTick);
}
}
}
public static class ListerHaulablesExtensions
{
public static void TryAddDirectly(this ListerHaulables lister, Thing t)
{
if (!t.IsItem())
return;
var haulables = lister.haulables;
ref var cache = ref lister.Cache();
cache.CheckUpdate(haulables);
if (cache.ContainedThings.Add(t.GetKey()))
haulables.Add(t);
}
public static void TryRemoveDirectly(this ListerHaulables lister, Thing t)
{
if (!t.IsItem())
return;
var haulables = lister.haulables;
ref var cache = ref lister.Cache();
cache.CheckUpdate(haulables);
if (cache.ContainedThings.Remove(t.GetKey()))
haulables.Remove(t);
}
}
+21 -7
View File
@@ -3,6 +3,8 @@
// 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/.
#if bad
global using MergeablesCache
= PerformanceFish.Cache.ByReference<Verse.Thing, RimWorld.ListerMergeables,
PerformanceFish.Listers.Mergeables.MergeablesCacheValue>;
@@ -10,10 +12,21 @@ using PerformanceFish.Cache;
namespace PerformanceFish.Listers;
public class Mergeables : ClassWithFishPatches
public sealed class Mergeables : ClassWithFishPatches
{
public class Check_Patch : FishPatch
public sealed class Check_Patch : FishPatch
{
protected internal override void OnPatchingCompleted()
{
Hauling.StorageSettingsPatches.TryNotifyChanged_Patch.Changed += _ => MergeablesCache.Get.Clear();
Events.ThingEvents.Initialized += thing =>
{
if (thing is IHaulDestination)
thing.Events().Destroying += _ => MergeablesCache.Get.Clear();
};
}
public override string Description { get; } = "Optimization for mergeables checking";
public override Expression<Action> TargetMethod { get; } = static () => default(ListerMergeables)!.Check(null);
@@ -46,7 +59,7 @@ public class Mergeables : ClassWithFishPatches
=> Reflection.MakeReplacementCall(Check);
}
public class CheckAdd_Patch : FishPatch
public sealed class CheckAdd_Patch : FishPatch
{
public override string Description { get; } = "Part of the ListerMergeables optimization";
@@ -70,7 +83,7 @@ public class Mergeables : ClassWithFishPatches
=> Reflection.MakeReplacementCall(CheckAdd);
}
public class TryRemove_Patch : FishPatch
public sealed class TryRemove_Patch : FishPatch
{
public override string Description { get; } = "Part of the ListerMergeables optimization";
@@ -79,8 +92,9 @@ public class Mergeables : ClassWithFishPatches
public static void TryRemove(ListerMergeables instance, Thing t)
{
if (t.def.category == ThingCategory.Item)
{
if (!t.IsItem())
return;
ref var cache = ref MergeablesCache.GetAndCheck<MergeablesCacheValue>(t, instance);
if (!cache.Contains)
@@ -89,7 +103,6 @@ public class Mergeables : ClassWithFishPatches
instance.mergeables.Remove(t);
cache.SetValue(false, t);
}
}
public static CodeInstructions Transpiler(CodeInstructions codeInstructions)
=> Reflection.MakeReplacementCall(TryRemove);
@@ -122,3 +135,4 @@ public class Mergeables : ClassWithFishPatches
}
}
}
#endif
-27
View File
@@ -1,27 +0,0 @@
// Copyright (c) 2023 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/.
namespace PerformanceFish.Listers;
public class Misc : ClassWithFishPatches
{
public class Building_Destroy_Patch : FishPatch
{
public override string Description { get; }
= "Used for haulables caching. Items don't become haulable after deconstructing stockpiles without this "
+ "patch";
public override Expression<Action> TargetMethod { get; } = static () => default(Building)!.Destroy(default);
public static void Prefix(Building __instance)
{
if (__instance is not IHaulDestination)
return;
HaulablesCache.Get.Clear();
MergeablesCache.Get.Clear();
}
}
}
@@ -1,76 +0,0 @@
// Copyright (c) 2023 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.Runtime.InteropServices;
using PerformanceFish.Cache;
using StorageSettingsCache
= PerformanceFish.Cache.ByReference<Verse.Thing, RimWorld.StorageSettings,
PerformanceFish.Listers.StorageSettingsPatches.StorageSettingsCacheValue>;
namespace PerformanceFish.Listers;
public class StorageSettingsPatches : ClassWithFishPatches
{
public class AllowedToAccept_Patch : FirstPriorityFishPatch
{
public override string Description { get; } = "StorageSettings caching";
public override Expression<Action> TargetMethod { get; }
= static () => default(StorageSettings)!.AllowedToAccept(default(Thing));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Prefix(StorageSettings __instance, Thing t, ref bool __result, out bool __state)
{
ref var cache = ref StorageSettingsCache.GetOrAddReference(new(t, __instance));
if (cache.Dirty)
return __state = true;
__result = cache.AllowedToAccept;
return __state = false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Postfix(StorageSettings __instance, Thing t, bool __result, bool __state)
{
if (!__state)
return;
StorageSettingsCache.Update<StorageSettingsCacheValue, bool>(t, __instance, __result);
}
}
public class TryNotifyChanged_Patch : FishPatch
{
public override string Description { get; } = "StorageSettings cache resetting";
public override Expression<Action> TargetMethod { get; } = static () => default(StorageSettings)!.TryNotifyChanged();
public static void Postfix(StorageSettings __instance)
{
StorageSettingsCache.Get.Clear();
HaulablesCache.Get.Clear();
MergeablesCache.Get.Clear();
}
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public record struct StorageSettingsCacheValue : ICacheable<StorageSettingsCache, bool>
{
private int _nextRefreshTick;
public bool AllowedToAccept;
public bool Dirty
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => TickHelper.Past(_nextRefreshTick);
}
public void Update(ref StorageSettingsCache key, bool allowedToAccept)
{
AllowedToAccept = allowedToAccept;
_nextRefreshTick = TickHelper.Add(3072, key.First.thingIDNumber, 2048);
}
}
}
@@ -0,0 +1,148 @@
// Copyright (c) 2023 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 Mono.Cecil;
using Mono.Cecil.Cil;
using PerformanceFish.Prepatching;
namespace PerformanceFish.Listers;
public sealed class ThingOwnerOptimization : ClassWithFishPrepatches
{
public sealed class ExposeDataPatch : FishPrepatch
{
public override string? Description { get; }
= "Required to keep the ThingOwner cache synced";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ThingOwner<Thing>), nameof(ThingOwner<Thing>.ExposeData));
public static void Postfix<T>(ThingOwner<T> __instance) where T : Thing
{
if (Scribe.mode != LoadSaveMode.PostLoadInit)
return;
var indexMap = __instance.IndexMap();
var innerList = __instance.innerList;
innerList.RemoveAll(static thing => thing is null);
indexMap.Clear();
for (var i = innerList.Count; i-- > 0;)
indexMap[innerList[i].GetKey()] = i;
}
}
public sealed class TryAddPatch : FishPrepatch
{
public override string? Description { get; }
= "Required to keep the ThingOwner cache synced";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ThingOwner<Thing>), nameof(ThingOwner<Thing>.TryAdd),
[typeof(Thing), typeof(bool)]);
public static void Postfix<T>(ThingOwner<T> __instance, Thing? item, bool canMergeWithExistingStacks,
bool __result) where T : Thing
{
if (!__result
|| item is null
|| (canMergeWithExistingStacks && (item.Destroyed || item.stackCount == 0)))
{
return;
}
__instance.IndexMap()[item.GetKey()] = __instance.innerList.Count - 1;
}
}
/*public sealed class IndexOfPatch : FishPrepatch // not really used enough to be worth patching
{
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ThingOwner<Thing>), nameof(ThingOwner<Thing>.IndexOf));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody<Thing>);
public static int ReplacementBody<T>(ThingOwner<T> instance, Thing? item) where T : Thing
=> item is not T itemOfT || item.holdingOwner != instance
? -1
: instance.innerList is var innerList
&& instance.IndexMap().TryGetValue(GetKey(itemOfT), out var knownIndex)
&& knownIndex < innerList.Count
&& innerList[knownIndex] == item
? knownIndex
: innerList.IndexOf(itemOfT);
}*/
public sealed class RemovePatch : FishPrepatch
{
public override string? Description { get; }
= "Optimizes ThingOwner to keep a cache of thing indices, to greatly speed up removal and in turn improve "
+ "performance when despawning happens. Late game colonies and maps in biomes with large amounts of plants "
+ "benefit most from this";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ThingOwner<Thing>), nameof(ThingOwner<Thing>.Remove));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
{
var codes = ilProcessor.instructions;
var lastIndexOfIndex = codes.FirstIndexOf(static code
=> code.Operand is MethodReference { Name: "LastIndexOf" });
for (var i = lastIndexOfIndex; i-- > 0;)
{
if (codes[i].OpCode != OpCodes.Ldfld || !codes[i - 1].LoadsArgument(0))
continue;
codes.RemoveAt(i);
lastIndexOfIndex--;
break;
}
var removeAtIndex = codes.FirstIndexOf(static code
=> code.Operand is MethodReference { Name: "RemoveAt" });
for (var i = removeAtIndex + 1; i-- > 0;)
{
if (i == lastIndexOfIndex)
break;
codes.RemoveAt(i);
}
codes[lastIndexOfIndex].OpCode = OpCodes.Call;
codes[lastIndexOfIndex].Operand
= module.ImportMethodWithGenericArguments(methodof(Remove<Thing>).GetGenericMethodDefinition(),
ilProcessor.Body.Method.DeclaringType.GenericParameters);
var variables = ilProcessor.Body.Variables;
variables.RemoveAt(variables.FirstIndexOf(variable => variable.VariableType == module.TypeSystem.Int32));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Remove<T>(ThingOwner<T> instance, Thing item) where T : Thing
{
var indexMap = instance.IndexMap();
var innerList = instance.innerList;
var itemKey = item.GetKey();
var index = indexMap.TryGetValue(itemKey, out var knownIndex)
&& knownIndex < innerList.Count
&& innerList[knownIndex] == item
? knownIndex
: innerList.LastIndexOf((T)item);
if (index >= 0)
innerList.RemoveAtFastUnordered(index);
indexMap.Remove(itemKey);
if (index < innerList.Count && index >= 0)
indexMap[innerList[index].GetKey()] = index;
}
}
}
+188 -19
View File
@@ -3,27 +3,196 @@
// 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 Mono.Cecil;
using Mono.Cecil.Cil;
using PerformanceFish.Prepatching;
namespace PerformanceFish.Listers;
public class Things : ClassWithFishPatches
public sealed class ThingsPrepatches : ClassWithFishPrepatches
{
public class ThingsMatching_Patch : FirstPriorityFishPatch
public sealed class AddPatch : FishPrepatch
{
public override string? Description { get; }
= "Required to keep the ListerThings cache synced";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ListerThings), nameof(ListerThings.Add));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static void ReplacementBody(ListerThings __instance, Thing t)
{
if (!ListerThings.EverListable(t.def, __instance.use))
return;
AddToDefList(__instance, t);
var allGroups = ThingListGroupHelper.AllGroups;
foreach (var thingRequestGroup in allGroups)
{
if ((__instance.use == ListerThingsUse.Region && !thingRequestGroup.StoreInRegion())
|| !thingRequestGroup.Includes(t.def))
{
continue;
}
AddToGroupList(__instance, t, thingRequestGroup);
}
}
}
public sealed class RemovePatch : FishPrepatch
{
public override string? Description { get; }
= "Optimizes ListerThings to keep a cache of thing indices within the lister, to greatly speed up removal "
+ "and in turn improve performance when despawning happens. Late game colonies and maps in biomes with "
+ "large amounts of plants benefit most from this";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ListerThings), nameof(ListerThings.Remove));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static void ReplacementBody(ListerThings __instance, Thing t)
{
if (!ListerThings.EverListable(t.def, __instance.use))
return;
RemoveFromDefList(__instance, t);
var allGroups = ThingListGroupHelper.AllGroups;
for (var i = 0; i < allGroups.Length; i++)
{
var thingRequestGroup = allGroups[i];
if ((__instance.use == ListerThingsUse.Region && !thingRequestGroup.StoreInRegion())
|| !thingRequestGroup.Includes(t.def))
{
continue;
}
RemoveFromGroupList(__instance, t, thingRequestGroup);
}
}
}
public static void AddToDefList(ListerThings lister, Thing thing)
{
if (!lister.listsByDef.TryGetValue(thing.def, out var value))
{
value = [];
lister.listsByDef.Add(thing.def, value);
}
value.Add(thing);
lister.IndexMapByDef()[thing.GetKey()] = lister.listsByDef[thing.def].Count - 1;
}
public static void AddToGroupList(ListerThings lister, Thing thing, ThingRequestGroup thingRequestGroup)
{
var list = lister.listsByGroup[(uint)thingRequestGroup];
if (list == null)
{
list = [];
lister.listsByGroup[(uint)thingRequestGroup] = list;
lister.stateHashByGroup[(uint)thingRequestGroup] = 0;
}
list.Add(thing);
lister.stateHashByGroup[(uint)thingRequestGroup]++;
lister.IndexMapByGroup()[new(thingRequestGroup, thing.GetKey())]
= lister.listsByGroup[(uint)thingRequestGroup].Count - 1;
}
public static void RemoveFromDefList(ListerThings lister, Thing thing)
{
var thingKey = thing.GetKey();
var indexMapByDef = lister.IndexMapByDef();
var listByDef = lister.listsByDef[thing.def];
var indexByDef = indexMapByDef.TryGetValue(thingKey, out var knownIndexByDef)
&& knownIndexByDef < listByDef.Count
&& listByDef[knownIndexByDef] == thing
? knownIndexByDef
: listByDef.LastIndexOf(thing);
if (indexByDef >= 0)
listByDef.RemoveAtFastUnordered(indexByDef);
indexMapByDef.Remove(thingKey);
if (indexByDef < listByDef.Count && indexByDef >= 0)
indexMapByDef[listByDef[indexByDef].GetKey()] = indexByDef;
}
public static void RemoveFromGroupList(ListerThings lister, Thing thing, ThingRequestGroup thingRequestGroup)
{
var indexMapByGroup = lister.IndexMapByGroup();
var listByGroup = lister.listsByGroup[(int)thingRequestGroup];
var groupMapKey = new GroupThingPair(thingRequestGroup, thing.GetKey());
var indexByGroup
= indexMapByGroup.TryGetValue(groupMapKey, out var knownIndexByGroup)
&& knownIndexByGroup < listByGroup.Count
&& listByGroup[knownIndexByGroup] == thing
? knownIndexByGroup
: listByGroup.LastIndexOf(thing);
if (indexByGroup >= 0)
listByGroup.RemoveAtFastUnordered(indexByGroup);
indexMapByGroup.Remove(groupMapKey);
if (indexByGroup < listByGroup.Count && indexByGroup >= 0)
indexMapByGroup[new(thingRequestGroup, listByGroup[indexByGroup].GetKey())] = indexByGroup;
lister.stateHashByGroup[(uint)thingRequestGroup]++;
}
public sealed class ContainsPatch : FishPrepatch
{
public override string? Description { get; }
= "Optimization utilizing a cache for faster Contains checks";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ListerThings), nameof(ListerThings.Contains));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static bool ReplacementBody(ListerThings __instance, Thing t)
=> __instance.IndexMapByDef().ContainsKey(t.GetKey())
|| (__instance.listsByDef.TryGetValue(t.def)?.Contains(t) ?? false);
}
public sealed class ClearPatch : FishPrepatch
{
public override string? Description { get; }
= "Required to keep the ListerThings cache synced";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ListerThings), nameof(ListerThings.Clear));
public static void Postfix(ListerThings __instance)
{
__instance.IndexMapByDef().Clear();
__instance.IndexMapByGroup().Clear();
}
}
}
public sealed class Things : ClassWithFishPatches
{
public sealed class ThingsMatching_Patch : FirstPriorityFishPatch
{
public override List<Type> LinkedPatches { get; } = [typeof(ThingFilterPatches.BestThingRequest_Patch)];
public override string Description { get; }
= "Part of the ThingFilter.BestThingRequest optimization. Required by that particular patch and gets "
+ "toggled alongside it";
public override bool Enabled
{
set
{
if (base.Enabled == value)
return;
Get<ThingFilterPatches.BestThingRequest_Patch>().Enabled = base.Enabled = value;
}
}
public override Expression<Action> TargetMethod { get; }
= static () => new ListerThings(default).ThingsMatching(default);
@@ -61,7 +230,7 @@ public class Things : ClassWithFishPatches
private static Dictionary<IEnumerable<ThingDef>, List<Thing>>? _thingsOfDefsDictionary;
#if disabled
public class ThingRequest_IsUndefined_Patch : FishPatch
public sealed class ThingRequest_IsUndefined_Patch : FishPatch
{
public override MethodBase TargetMethodInfo { get; }
= AccessTools.PropertyGetter(typeof(ThingRequest), nameof(ThingRequest.IsUndefined));
@@ -79,7 +248,7 @@ public class Things : ClassWithFishPatches
=> __instance.group == ThingRequestGroup.Undefined && !((FishCache)__instance.singleDef).IsSingleDef;
}
public class ThingRequest_CanBeFoundInRegion_Patch : FirstPriorityFishPatch
public sealed class ThingRequest_CanBeFoundInRegion_Patch : FirstPriorityFishPatch
{
public override MethodBase TargetMethodInfo { get; }
= AccessTools.PropertyGetter(typeof(ThingRequest), nameof(ThingRequest.CanBeFoundInRegion));
@@ -99,7 +268,7 @@ public class Things : ClassWithFishPatches
&& (__instance.group == ThingRequestGroup.Nothing || __instance.group.StoreInRegion()));
}
public class Add_Patch : FishPatch
public sealed class Add_Patch : FishPatch
{
public override Expression<Action> TargetMethod { get; } = static () => new ListerThings(default).Add(null);
@@ -112,7 +281,7 @@ public class Things : ClassWithFishPatches
}
}
public class Remove_Patch : FishPatch
public sealed class Remove_Patch : FishPatch
{
public override Expression<Action> TargetMethod { get; } = static () => new ListerThings(default).Remove(null);
@@ -125,7 +294,7 @@ public class Things : ClassWithFishPatches
}
}
public class Clear_Patch : FishPatch
public sealed class Clear_Patch : FishPatch
{
public override Expression<Action> TargetMethod { get; } = static () => new ListerThings(default).Clear();
@@ -138,7 +307,7 @@ public class Things : ClassWithFishPatches
}
}
public class ThingsMatching_Patch : FirstPriorityFishPatch
public sealed class ThingsMatching_Patch : FirstPriorityFishPatch
{
public override string Description { get; } = "Part of the work scanning optimization";
+10 -9
View File
@@ -3,11 +3,11 @@
// 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.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.InteropServices;
using JetBrains.Annotations;
using PerformanceFish.Pools;
using FisheryLib.Pools;
using PerformanceFish.Prepatching;
namespace PerformanceFish;
@@ -93,6 +93,7 @@ public static class Log
public static UnhandledMessageQueue? UnhandledMessages { get; }
= SetupUnhandledMessageQueue();
[SuppressMessage("Reliability", "CA2000")]
private static UnhandledMessageQueue? SetupUnhandledMessageQueue()
{
if (Ready)
@@ -105,8 +106,7 @@ public static class Log
return stashedMessages;
}
stash.UnhandledMessages = stashedMessages = new();
stash.MessageQueueGCHandle = GCHandle.Alloc(stashedMessages, GCHandleType.Pinned);
stash.MessageQueueGCHandle = GCHandle.Alloc(new UnhandledMessageQueue(), GCHandleType.Pinned);
return stash.UnhandledMessages;
}
@@ -114,7 +114,7 @@ public static class Log
}
[StructLayout(LayoutKind.Sequential)]
public struct LogMessageData : IDisposable
public record struct LogMessageData : IDisposable
{
public IntPtr Text,
StackTrace;
@@ -188,18 +188,19 @@ public unsafe class UnhandledMessageQueue : IDisposable
else
{
_arrayLength <<= 1;
_messages = (LogMessageData*)Marshal.ReAllocHGlobal((IntPtr)_messages, (IntPtr)_arrayLength);
_messages = (LogMessageData*)Marshal.ReAllocHGlobal((IntPtr)_messages,
(IntPtr)(_arrayLength * sizeof(LogMessageData)));
}
}
// ReSharper disable once InconsistentlySynchronizedField
private void ReleaseUnmanagedResources() => Marshal.FreeHGlobal((IntPtr)_messages);
protected virtual void Dispose(bool disposing) => Marshal.FreeHGlobal((IntPtr)_messages);
public void Dispose()
{
ReleaseUnmanagedResources();
Dispose(true);
GC.SuppressFinalize(this);
}
~UnhandledMessageQueue() => ReleaseUnmanagedResources();
~UnhandledMessageQueue() => Dispose(false);
}
+6 -4
View File
@@ -12,26 +12,28 @@ using FieldAttributes = Mono.Cecil.FieldAttributes;
namespace PerformanceFish;
public class LogPatches : ClassWithFishPrepatches
public sealed class LogPatches : ClassWithFishPrepatches
{
public const string PERFORMANCE_FISH_WELCOME_MESSAGE = "Performance Fish!!!";
public const string PERFORMANCE_FISH_WELCOME_STACKTRACE = "UwU";
// This breaks my FishStash somehow // TODO: FIX
// public class LogVersionNumberPatch : FishPrepatch
// public sealed class LogVersionNumberPatch : FishPrepatch
// {
// public override MethodBase MethodBase { get; } = methodof(Root.CheckGlobalInit);
//
// public static void Prefix() => Log.Message("Performance Fish???");
// }
public class ClassPatch : FishClassPrepatch
public sealed class ClassPatch : FishClassPrepatch
{
public override string? Description { get; }
= "Important logging patch";
public override bool Enabled => true;
public override bool ShowSettings => false;
public override Type Type => typeof(Verse.Log);
public override void FreePatch(TypeDefinition typeDefinition)
@@ -49,7 +51,7 @@ public class LogPatches : ClassWithFishPrepatches
staticConstructorBody.GetILProcessor().InsertRange(staticConstructorBody.Instructions.Count - 2,
(OpCodes.Ldc_I8, FishStash.StaticHandle),
(OpCodes.Newobj,
module.ImportReference(typeof(FishStashHandle).GetConstructor(new[] { typeof(long) }))),
module.ImportReference(typeof(FishStashHandle).GetConstructor([typeof(long)]))),
(OpCodes.Stsfld, fishStashHandleField),
(OpCodes.Call, module.ImportReference(InsertEarlyMessages)));
}
+123
View File
@@ -0,0 +1,123 @@
// Copyright (c) 2023 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 PerformanceFish.Prepatching;
namespace PerformanceFish;
public sealed class MapPrepatches : ClassWithFishPrepatches
{
public sealed class FinalizeLoadingPatch : FishPrepatch
{
public override string? Description { get; }
= "An attempt at logging and fixing possible causes for the common FMOD error spam. None of "
+ "Performance Fish's other patches interact with this in any way.";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(Map), nameof(Map.FinalizeLoading));
public static void Postfix(Map __instance)
{
var listerThings = __instance.listerThings;
var musicalInstruments = listerThings.ThingsInGroup(ThingRequestGroup.MusicalInstrument);
for (var i = musicalInstruments.Count; i-- > 0;)
{
var thing = musicalInstruments[i];
if (thing is Building_MusicalInstrument)
continue;
Log.Error($"Map '{__instance}' contains thing that is not of type 'Building_MusicalInstrument' "
+ $"in ThingsInGroup for ThingRequestGroup.MusicalInstrument: '{
thing}' at cell '{thing.Position}'. Removing now to prevent further issues.");
if (!RefundOrRemoveIfUnassignable(__instance, thing))
listerThings.RemoveFromGroup(thing, ThingRequestGroup.MusicalInstrument);
}
var musicSources = listerThings.ThingsInGroup(ThingRequestGroup.MusicSource);
for (var i = musicSources.Count; i-- > 0;)
{
var thing = musicSources[i];
if (thing.TryGetComp<CompPlaysMusic>() != null)
continue;
Log.Error($"Map '{__instance}' contains thing without comp of type 'CompPlaysMusic' in "
+ $"ThingsInGroup for ThingRequestGroup.MusicSource: '{
thing}' at cell '{thing.Position}'. Removing now to prevent further issues.");
if (!RefundOrRemoveIfUnassignable(__instance, thing))
listerThings.RemoveFromGroup(thing, ThingRequestGroup.MusicSource);
}
var allThings = listerThings.AllThings;
for (var i = allThings.Count; i-- > 0;)
{
var thing = allThings[i];
var thingDef = thing.def;
if (thingDef is null)
{
Log.Error($"Map '{__instance}' contains thing '{thing}' with null def at cell '{
thing.Position}'. This will likely cause issues. Removing now.");
listerThings.Remove(thing);
// continue;
}
// else if (thing.GetType().IsAssignableTo(thingDef.thingClass))
// {
// Log.Error($"Map '{__instance}' contains thing '{thing}' of incorrect type '{
// thing.GetType()}' at cell '{thing.Position}'. This should be '{
// thingDef.thingClass}' instead and will likely cause issues. Destroying and refunding now.");
// }
// else
// {
// continue;
// }
//
// if (!TryRefundOrRemove(__instance, thing))
// listerThings.Remove(thing);
}
}
private static bool RefundOrRemoveIfUnassignable(Map map, Thing thing)
{
if (thing.def != null && thing.GetType().IsAssignableTo(thing.def.thingClass))
return false;
return TryRefundOrRemove(map, thing);
}
private static bool TryRefundOrRemove(Map map, Thing thing)
{
var previousAllowDestroy = Thing.allowDestroyNonDestroyable;
Thing.allowDestroyNonDestroyable = true;
try
{
GenSpawn.Refund(thing, map, CellRect.Empty);
}
catch (Exception refundException)
{
Log.Error($"Failed to refund thing '{thing}'. Destroying.\n{refundException}");
try
{
thing.Destroy();
}
catch (Exception destroyException)
{
Log.Error($"Failed to destroy thing '{thing}'.\n{destroyException}");
return false;
}
}
finally
{
Thing.allowDestroyNonDestroyable = previousAllowDestroy;
}
return true;
}
}
}
+3 -3
View File
@@ -9,9 +9,9 @@ using InventoryMassCache
namespace PerformanceFish;
public class MassUtilityCaching : ClassWithFishPatches
public sealed class MassUtilityCaching : ClassWithFishPatches
{
public class GearMass_Patch : FirstPriorityFishPatch
public sealed class GearMass_Patch : FirstPriorityFishPatch
{
public override string Description { get; } = "Caches pawn gear mass. Hauling jobs check this quite a lot";
public override Delegate TargetMethodGroup { get; } = MassUtility.GearMass;
@@ -41,7 +41,7 @@ public class MassUtilityCaching : ClassWithFishPatches
=> GearMassCache.GetExistingReference(p).Update(__result, p);
}
public class InventoryMass_Patch : FirstPriorityFishPatch
public sealed class InventoryMass_Patch : FirstPriorityFishPatch
{
public override string Description { get; } = "Caches pawn inventory mass. Hauling jobs check this quite a lot";
public override Delegate TargetMethodGroup { get; } = MassUtility.InventoryMass;
@@ -10,9 +10,9 @@ using PsyfocusGainCache
namespace PerformanceFish;
// Suggested by https://github.com/notfood
public class MeditationUtilityCaching : ClassWithFishPatches
public sealed class MeditationUtilityCaching : ClassWithFishPatches
{
public class PsyfocusGainPerTick : FirstPriorityFishPatch
public sealed class PsyfocusGainPerTick : FirstPriorityFishPatch
{
public override string? Description { get; } = "Caches psyfocus gain per tick while the target remains "
+ "constant. Impactful in psycasting focused playthroughs";
+30 -14
View File
@@ -12,9 +12,9 @@ using static System.Reflection.Emit.OpCodes;
namespace PerformanceFish;
public class MiscPrepatchedOptimizations : ClassWithFishPrepatches
public sealed class MiscPrepatchedOptimizations : ClassWithFishPrepatches
{
public class GenList_ListFullCopy : FishPrepatch
public sealed class GenList_ListFullCopy : FishPrepatch
{
public override string Description { get; }
= "Optimization for a basic and frequently used list copying method. Replaces a for loop with an "
@@ -32,7 +32,7 @@ public class MiscPrepatchedOptimizations : ClassWithFishPrepatches
public static List<T> ListFullCopy<T>(List<T> source) => source.Copy();
}
public class GenCollection_FirstOrDefault : FishPrepatch
public sealed class GenCollection_FirstOrDefault : FishPrepatch
{
public override string? Description { get; }
= "Minor optimization for a RimWorld method used for list lookups, by looping in a more efficient manner "
@@ -46,11 +46,28 @@ public class MiscPrepatchedOptimizations : ClassWithFishPrepatches
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T? FirstOrDefault<T>(List<T> list, Predicate<T> predicate) => list.FirstOrDefaultFast(predicate);
}
public sealed class GenGrid_InBoundsPatch : FishPrepatch
{
public override string? Description { get; } = "Tiny optimization by fixing an incorrect cast";
public override MethodBase TargetMethodBase => methodof((Func<IntVec3, Map, bool>)GenGrid.InBounds);
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool ReplacementBody(IntVec3 c, Map map)
{
var size = map.Size;
return (uint)c.x < (uint)size.x && (uint)c.z < (uint)size.z;
}
}
}
public class MiscOptimizations : ClassWithFishPatches
public sealed class MiscOptimizations : ClassWithFishPatches
{
public class WindManager_WindManagerTick : FishPatch
public sealed class WindManager_WindManagerTick : FishPatch
{
public override string Description { get; }
= "Throttles plant sway to update at most once per frame instead of every tick. Also fixes the plant sway "
@@ -69,11 +86,10 @@ public class MiscOptimizations : ClassWithFishPatches
&& codes[i - 3] == find_CurrentMap
&& codes[i - 1] == this_map
&& codes[i].operand is Label, // goto return;
static code
=> new[]
{
static code =>
[
code, FishTranspiler.Call(ShouldSway), FishTranspiler.IfFalse_Short((Label)code.operand)
});
]);
}
public static bool ShouldSway()
@@ -95,7 +111,7 @@ public class MiscOptimizations : ClassWithFishPatches
private static bool _lastPrefValue;
}
public class CompRottable : FishPatch
public sealed class CompRottable : FishPatch
{
public override string? Description { get; }
= "Throttles rotting on misconfigured defs to only recalculate every 256 ticks, instead of constantly.";
@@ -114,7 +130,7 @@ public class MiscOptimizations : ClassWithFishPatches
}
}
public class RitualObligationTrigger_Date : FirstPriorityFishPatch
public sealed class RitualObligationTrigger_Date : FirstPriorityFishPatch
{
public override string? Description { get; }
= "Literally just reorders instructions in this method. They were nonsensical, discarding 90% of the "
@@ -141,7 +157,7 @@ public class MiscOptimizations : ClassWithFishPatches
}
}
public class ReportProbablyMissingAttributesFix : FishPatch
public sealed class ReportProbablyMissingAttributesFix : FishPatch
{
public override string? Description { get; }
= """Fixes the "probably needs a StaticConstructorOnStartup attribute" log warning to display full """
@@ -159,7 +175,7 @@ public class MiscOptimizations : ClassWithFishPatches
methodof(Reflection.FullDescription));
}
public class DrawBatch_Flush : FishPatch
public sealed class DrawBatch_Flush : FishPatch
{
public override string Description { get; }
= "Optimization of a fleck rendering method. Low - medium performance impact";
@@ -168,7 +184,7 @@ public class MiscOptimizations : ClassWithFishPatches
public static CodeInstructions? Transpiler(CodeInstructions codeInstructions, ILGenerator generator)
{
var codes = codeInstructions.ToList();
var codes = codeInstructions.AsOrToList();
var instance_tmpPropertyBlocks
= FishTranspiler.Field(typeof(DrawBatch), nameof(DrawBatch.tmpPropertyBlocks));
@@ -12,14 +12,38 @@ public static class ActiveMods
// GradientHair = Contains(PackageIDs.GRADIENT_HAIR),
GraphicsSetter = Contains(PackageIDs.GRAPHICS_SETTINGS),
Harmony = Contains(PackageIDs.HARMONY),
HumanResources = Contains(PackageIDs.HUMAN_RESOURCES),
Multiplayer = Contains(PackageIDs.MULTIPLAYER),
PerformanceOptimizer = Contains(PackageIDs.PERFORMANCE_OPTIMIZER),
Prepatcher = Contains(PackageIDs.PREPATCHER),
UseBestMaterials = Contains(PackageIDs.USE_BEST_MATERIALS),
VanillaExpandedFramework = Contains(PackageIDs.VANILLA_EXPANDED_FRAMEWORK);
// ModsConfig.IsActive does not ignore _steam postfixes, causing it to fail when local copies were made
public static bool Contains(string packageID) => TryGetModMetaData(packageID) != null;
public static bool ContainsAnyOf<T>(T packageIDs) where T : IEnumerable<string>
{
foreach (var id in packageIDs)
{
if (Contains(id))
return true;
}
return false;
}
public static bool ContainsAllOf<T>(T packageIDs) where T : IEnumerable<string>
{
foreach (var id in packageIDs)
{
if (!Contains(id))
return false;
}
return true;
}
public static ModMetaData? TryGetModMetaData(string packageID)
=> ModLister.GetActiveModWithIdentifier(packageID, true);
@@ -55,7 +79,7 @@ public static class ActiveMods
ThrowForMissingModDependency(nameof(Fishery));
if (!Harmony)
ThrowForMissingModDependency(nameof(Harmony));
WarnForMissingModDependency(nameof(Harmony));
VerifyLoadOrder(nameof(Fishery), PackageIDs.FISHERY, PackageIDs.PERFORMANCE_FISH, PackageIDs.BETTER_LOG);
VerifyLoadOrder(nameof(Harmony), PackageIDs.HARMONY, PackageIDs.PERFORMANCE_FISH);
@@ -70,14 +94,14 @@ public static class ActiveMods
if (activeModsInLoadOrder.FindIndex(mod => mod.PackageId == dependencyID)
> activeModsInLoadOrder.FindIndex(mod => dependingModIDs.Contains(mod.PackageId)))
{
ThrowForIncorrectLoadOrder(dependencyName);
ErrorForIncorrectLoadOrder(dependencyName);
}
}
private static void CheckFisheryVersion()
{
if (FisheryLib.FisheryLib.VERSION > FisheryLib.FisheryLib.CurrentlyLoadedVersion)
ThrowForOutdatedModDependency(nameof(Fishery));
ErrorForOutdatedModDependency(nameof(Fishery));
}
private static void CheckHarmonyVersion()
@@ -86,23 +110,27 @@ public static class ActiveMods
private static void CheckDependencyVersion(string dependencyName, Type dependencyType,
string dependencyAssemblyName)
{
if (typeof(PerformanceFishMod).Assembly.GetReferencedAssemblies().First(assembly
=> assembly.FullName.StartsWith($"{dependencyAssemblyName}, Version", StringComparison.Ordinal)).Version
> dependencyType.Assembly.GetName().Version)
if (typeof(PerformanceFishMod).Assembly.GetReferencedAssemblyVersion(dependencyAssemblyName)
> dependencyType.Assembly.GetLoadedVersion())
{
ThrowForOutdatedModDependency(dependencyName);
ErrorForOutdatedModDependency(dependencyName);
}
}
private static void ThrowForIncorrectLoadOrder(string dependencyName)
=> ThrowHelper.ThrowOperationCanceledException($"Incorrect load order detected. {
dependencyName} must be loaded before any mod depending on it.");
private static void ErrorForIncorrectLoadOrder(string dependencyName)
=> Log.Error($"Incorrect load order detected. {dependencyName} must be loaded before any mod depending on it.");
private static void ThrowForOutdatedModDependency(string modName)
=> ThrowHelper.ThrowOperationCanceledException($"Outdated {modName} mod detected. {
private static void ErrorForOutdatedModDependency(string modName)
=> Log.Error($"Outdated {modName} mod detected. {
modName} must be updated for Performance Fish to work correctly");
private static void ThrowForMissingModDependency(string modName)
=> ThrowHelper.ThrowOperationCanceledException(
$"{modName} mod is missing. Performance Fish requires this mod in order to function.");
{
Log.Error($"{modName} mod is missing. Performance Fish requires this mod in order to function.");
ThrowHelper.ThrowOperationCanceledException();
}
private static void WarnForMissingModDependency(string modName)
=> Log.Warning(
$"{modName} mod is missing. Performance Fish may not work correctly without this dependency.");
}
@@ -13,9 +13,11 @@ public static class PackageIDs
// GRADIENT_HAIR = "automatic.gradienthair",
GRAPHICS_SETTINGS = "Telefonmast.GraphicsSettings",
HARMONY = "brrainz.harmony",
HUMAN_RESOURCES = "JPT.HumanResources",
MULTIPLAYER = "rwmt.Multiplayer",
PERFORMANCE_FISH = "bs.performance",
PERFORMANCE_OPTIMIZER = "Taranchuk.PerformanceOptimizer",
PREPATCHER = "zetrith.prepatcher",
USE_BEST_MATERIALS = "kathanon.UseBestMaterials",
VANILLA_EXPANDED_FRAMEWORK = "OskarPotocki.VanillaFactionsExpanded.Core";
}
@@ -20,10 +20,10 @@ public static class PerformanceOptimizer
public static string[] BlockedPatchNames { get; }
=
{
[
"PerformanceOptimizer.Optimization_FasterGetCompReplacement",
"PerformanceOptimizer.Optimization_IdeoUtility_GetStyleDominance"
};
];
static PerformanceOptimizer()
{
@@ -0,0 +1,32 @@
// Copyright (c) 2023 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/.
namespace PerformanceFish.ModCompatibility;
public static class Types
{
public static class RealRuins
{
public static Type? POIWorldObject = Reflection.Type("RealRuins", "RealRuins.RealRuinsPOIWorldObject");
}
public static class VFEAncients
{
public static Type? RecipeExtensionMend = Reflection.Type("VFEAncients", "VFEAncients.RecipeExtension_Mend");
}
public static class PerformanceAnalyzer
{
public static Type?
ModBase = Reflection.Type("PerformanceAnalyzer", "Analyzer.Modbase"),
ProfilingEntry = Reflection.Type("PerformanceAnalyzer", "Analyzer.Profiling.Entry"),
Window_Analyzer = Reflection.Type("PerformanceAnalyzer", "Analyzer.Window_Analyzer");
}
public static class LWM
{
public static Type? CompProperties = Reflection.Type("LWM.DeepStorage.Properties");
}
}
@@ -3,18 +3,20 @@
// 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/.
#if fixed // with commit from Jul 3, 2023
namespace PerformanceFish.ModCompatibility;
public class VanillaExpandedFixes : ClassWithFishPatches
public sealed class VanillaExpandedFixes : ClassWithFishPatches
{
public class ExpandableGraphicData : FishPatch
public sealed class ExpandableGraphicData : FishPatch
{
public override string? Description { get; }
= "Fixes the LoadAllFiles method in Vanilla Expanded Framework's ExpandableGraphicData to not scan through "
+ "all texture folders of all mods for every graphic and instead just use cached files the game already "
+ "loaded. Improves load times and prevents dds related errors.";
public override bool Enabled => ActiveMods.VanillaExpandedFramework;
public override bool Enabled => ActiveMods.VanillaExpandedFramework && base.Enabled;
public override MethodBase TargetMethodInfo { get; }
= AccessTools.Method("VFECore.ExpandableGraphicData:LoadAllFiles");
@@ -28,15 +30,13 @@ public class VanillaExpandedFixes : ClassWithFishPatches
var mods = LoadedModManager.RunningModsListForReading;
var count = mods.Count;
for (var i = 0; i < count; i++)
{
var contentListTrie = mods[i].GetContentHolder<Texture2D>().contentListTrie;
var prefix = !folderPath.NullOrEmpty() && folderPath![^1] == '/'
? folderPath
: folderPath + '/';
foreach (var path in contentListTrie.GetByPrefix(prefix))
for (var i = 0; i < count; i++)
{
foreach (var path in mods[i].GetContentHolder<Texture2D>().contentListTrie.GetByPrefix(prefix))
paths.Add(path);
}
@@ -44,3 +44,5 @@ public class VanillaExpandedFixes : ClassWithFishPatches
}
}
}
#endif
@@ -0,0 +1,63 @@
// Copyright (c) 2023 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 Mono.Cecil;
using Mono.Cecil.Cil;
using PerformanceFish.Prepatching;
namespace PerformanceFish;
public sealed class ModListerPatches : ClassWithFishPrepatches
{
public sealed class AnyFromListActivePatch : FishPrepatch
{
public override string? Description { get; }
= "Fixes a LoadFolders bug causing it to normally not recognize steam versions of mods if another local "
+ "copy exists.";
public override MethodBase TargetMethodBase { get; } = methodof(ModLister.AnyFromListActive);
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> GetActiveModWithIdentifierFix(ilProcessor, module);
}
public sealed class ModIncompatibilityIsSatisfiedPatch : FishPrepatch
{
public override string? Description { get; }
= "Fixes a mod manager bug causing it to normally not recognize steam versions of mods for incompatibility "
+ "checks if another local copy exists.";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredPropertyGetter(typeof(ModIncompatibility), nameof(ModIncompatibility.IsSatisfied));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> GetActiveModWithIdentifierFix(ilProcessor, module);
}
private static void GetActiveModWithIdentifierFix(ILProcessor ilProcessor, ModuleDefinition module)
{
var instructions = ilProcessor.instructions;
var success = false;
for (var i = 0; i + 1 < instructions.Count; i++)
{
if (instructions[i].OpCode == OpCodes.Ldc_I4_0
&& instructions[i + 1].Operand is MethodReference
{
Name: nameof(ModLister.GetActiveModWithIdentifier)
})
{
instructions[i].OpCode = OpCodes.Ldc_I4_1;
success = true;
}
}
if (!success)
{
Log.Error($"Performance Fish failed to apply its patch on '{
ilProcessor.GetMethod().FullName}'. This should be harmless as it's meant to be just a bugfix.");
}
}
}
@@ -0,0 +1,62 @@
// Copyright (c) 2023 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 Mono.Cecil;
using Mono.Cecil.Cil;
using PerformanceFish.Prepatching;
namespace PerformanceFish;
public sealed class ModsConfigPatches : ClassWithFishPrepatches
{
public sealed class AreAllActivePatch : FishPrepatch
{
public override string? Description { get; }
= "Fixes a MayRequire bug causing it to normally not recognize steam versions of mods if another local "
+ "copy exists.";
public override MethodBase TargetMethodBase { get; } = methodof(ModsConfig.AreAllActive);
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> IsActiveFix(ilProcessor, module);
}
public sealed class IsAnyActiveOrEmptyPatch : FishPrepatch
{
public override string? Description { get; }
= "Fixes a MayRequireAnyOf bug causing it to normally not recognize steam versions of mods if another "
+ "local copy exists, as well as its inability to strip whitespace.";
public override MethodBase TargetMethodBase { get; } = methodof(ModsConfig.IsAnyActiveOrEmpty);
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> IsActiveFix(ilProcessor, module);
public static void Prefix(out bool trimNames) => trimNames = true;
}
private static void IsActiveFix(ILProcessor ilProcessor, ModuleDefinition module)
{
var instructions = ilProcessor.instructions;
var success = false;
for (var i = 0; i + 1 < instructions.Count; i++)
{
if (instructions[i].Operand is MethodReference { Name: nameof(ModsConfig.IsActive) })
{
instructions[i].Operand = module.ImportReference(IsActiveReplacement);
success = true;
}
}
if (!success)
{
Log.Error($"Performance Fish failed to apply its patch on '{
ilProcessor.GetMethod().FullName}'. This should be harmless as it's meant to be just a bugfix.");
}
}
public static bool IsActiveReplacement(string id) => ModLister.GetActiveModWithIdentifier(id, true) != null;
}
@@ -33,6 +33,8 @@ public static class OnAssembliesLoaded
_ = FishSettings.Instance;
PerformanceFishMod.AllPrepatchClasses.BeforeHarmonyPatching();
Parallel.ForEach(PerformanceFishMod.AllPatchClasses, static patchClass =>
{
try
@@ -46,6 +48,10 @@ public static class OnAssembliesLoaded
patchClass.GetType().Name}:\n{ex}");
}
});
PerformanceFishMod.AllPrepatchClasses.OnPatchingCompleted();
Cache.Utility.Initialize();
}
private static void TryPatchGraphicsSetter()
+308
View File
@@ -0,0 +1,308 @@
// Copyright (c) 2023 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.Diagnostics.CodeAnalysis;
using FisheryLib.Pools;
using JetBrains.Annotations;
using Mono.Cecil;
namespace PerformanceFish;
public static class ParallelNoAlloc
{
private static FishTable<object, FishSet<Worker>> _groupWorkers = new();
public static void Invoke(Action action) => FishPool<Worker.Single>.Get().Invoke(action);
public static void Invoke(Action[] actions)
{
for (var i = 0; i < actions.Length; i++)
Invoke(actions[i]);
}
public static void Invoke(Span<Action> actions)
{
for (var i = 0; i < actions.Length; i++)
Invoke(actions[i]);
}
public static object RegisterBackgroundWaitingWorkers(Action[] actions)
{
Guard.IsGreaterThanOrEqualTo(actions.Length, 0);
var monitorObject = new MonitorObject.Group(new Worker[actions.Length]);
var workerGroup = new FishSet<Worker>();
for (var i = 0; i < actions.Length; i++)
workerGroup.Add(monitorObject.Subscribers[i] = new Worker.Continuous(monitorObject, actions[i]));
lock (_groupWorkers)
_groupWorkers.Add(monitorObject, workerGroup);
return monitorObject;
}
public static void RegisterBackgroundWaitingWorkers(object monitorObject, Action[] actions)
{
Guard.IsGreaterThanOrEqualTo(actions.Length, 0);
if (monitorObject is not MonitorObject.Group typedMonitorObject)
{
ThrowHelper.ThrowArgumentException();
return;
}
lock (_groupWorkers)
{
var workerGroup = _groupWorkers.GetOrAdd(typedMonitorObject);
for (var i = 0; i < actions.Length; i++)
{
var worker = new Worker.Continuous(typedMonitorObject, actions[i]);
typedMonitorObject.Subscribe(worker);
workerGroup.Add(worker);
}
}
}
public static void RegisterBackgroundWaitingWorker(object monitorObject, Action action)
{
Guard.IsNotNull(action);
if (monitorObject is not MonitorObject.Group typedMonitorObject)
{
ThrowHelper.ThrowArgumentException();
return;
}
var worker = new Worker.Continuous(typedMonitorObject, action);
lock (_groupWorkers)
{
typedMonitorObject.Subscribe(worker);
_groupWorkers.GetOrAdd(typedMonitorObject).Add(worker);
}
}
public static object RegisterBackgroundWaitingWorker(Action action)
{
Guard.IsNotNull(action);
var monitorObject = new MonitorObject.Group(Array.Empty<Worker>());
var worker = new Worker.Continuous(monitorObject, action);
monitorObject.Subscribe(worker);
lock (_groupWorkers)
_groupWorkers.GetOrAdd(monitorObject).Add(worker);
return monitorObject;
}
public static void DeregisterBackgroundWaitingWorker(object monitorObject, Action action)
{
Guard.IsNotNull(action);
if (monitorObject is not MonitorObject.Group typedMonitorObject)
{
ThrowHelper.ThrowArgumentException();
return;
}
lock (_groupWorkers)
{
var workerSet = _groupWorkers[typedMonitorObject]!;
foreach (var worker in workerSet)
{
if (worker.Action != action)
continue;
workerSet.Remove(worker);
typedMonitorObject.Unsubscribe(worker);
return;
}
}
ThrowHelper.ThrowInvalidOperationException($"No worker found to deregister from for object {
monitorObject} and action {action.Method.FullDescription()}");
}
public static void PulseRegisteredBackgroundWorkers(object monitorObject)
{
if (monitorObject is not MonitorObject.Group typedMonitorObject)
{
ThrowHelper.ThrowArgumentException();
return;
}
typedMonitorObject.Pulse();
}
private abstract class Worker : IFishPoolable
{
public Thread Thread { get; private set; }
public MonitorObject MonitorObject { get; private set; }
public bool Pulsed
{
get => _pulsed;
set => _pulsed = value;
}
public Action? Action { get; private set; }
private bool _cancelled;
private volatile bool _pulsed;
protected Worker() => Initialize(new MonitorObject.Single(this));
protected Worker(MonitorObject monitorObject) => Initialize(monitorObject);
[MemberNotNull(nameof(Thread)), MemberNotNull(nameof(MonitorObject))]
private void Initialize(MonitorObject monitorObject)
{
Guard.IsNotNull(monitorObject);
MonitorObject = monitorObject;
Thread = new(Callback, 131072);
Thread.Start();
}
protected Worker(MonitorObject monitorObject, Action action) : this(monitorObject)
=> Action = action;
public void Cancel() => _cancelled = true;
private void Callback()
{
while (true)
{
lock (MonitorObject)
{
while (!Pulsed)
Monitor.Wait(MonitorObject);
}
InvokeAction();
Pulsed = false;
Thread.MemoryBarrier();
if (_cancelled)
return;
Return();
}
}
void IFishPoolable.Reset()
{
}
protected abstract void InvokeAction();
protected abstract void Return();
[UsedImplicitly]
public sealed class Single : Worker
{
protected override void InvokeAction()
{
Action?.Invoke();
Action = null;
}
protected override void Return() => FishPool<Worker>.Return(this);
public void Invoke(Action action)
{
Guard.IsNotAssignableToType<MonitorObject.Group>(MonitorObject);
if (Pulsed)
Spin();
Action = action;
MonitorObject.Pulse();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void Spin()
{
var spinWait = new SpinWait();
while (Pulsed)
{
Thread.MemoryBarrier();
spinWait.SpinOnce();
}
}
}
public sealed class Continuous : Worker
{
public Continuous(MonitorObject monitorObject, Action action) : base(monitorObject, action)
=> Guard.IsNotNull(action);
protected override void InvokeAction() => Action!.Invoke();
protected override void Return()
{
}
}
}
private abstract class MonitorObject
{
public abstract void Pulse();
public sealed class Single(Worker worker) : MonitorObject
{
public Worker Worker { get; } = worker;
public override void Pulse()
{
lock (this)
{
Worker.Pulsed = true;
Thread.MemoryBarrier();
Monitor.Pulse(this);
}
}
}
public sealed class Group(Worker[] subscribers) : MonitorObject
{
public Worker[] Subscribers => subscribers;
public override void Pulse()
{
lock (this)
{
for (var i = subscribers.Length; i-- > 0;)
subscribers[i].Pulsed = true;
Thread.MemoryBarrier();
Monitor.PulseAll(this);
}
}
public void Subscribe(Worker worker) => subscribers = subscribers.Add(worker);
public void Unsubscribe(Worker worker)
{
Guard.IsGreaterThan(Subscribers.Length, 0);
var index = Array.IndexOf(Subscribers, worker);
Guard.IsGreaterThanOrEqualTo(index, 0);
var subscribersLength = subscribers.Length;
if (index != subscribersLength - 1)
Array.Copy(subscribers, index + 1, subscribers, index, subscribersLength - index - 1);
Array.Resize(ref subscribers, subscribersLength - 1);
}
}
}
}
@@ -15,5 +15,19 @@ public abstract class ClassWithFishPatches : SingletonFactory<ClassWithFishPatch
public virtual FishPatchHolder Patches { get; }
public virtual bool RequiresLoadedGameForPatching => false;
public virtual bool ShowSettings
{
get
{
foreach (var patch in Patches)
{
if (patch.ShowSettings)
return true;
}
return false;
}
}
protected ClassWithFishPatches() => Patches = new(GetType());
}
+58 -10
View File
@@ -8,6 +8,8 @@ using System.Diagnostics;
using System.Linq;
using nuget::JetBrains.Annotations;
using PerformanceFish.Cache;
using PerformanceFish.ModCompatibility;
using PerformanceFish.Prepatching;
namespace PerformanceFish.Patching;
@@ -15,11 +17,36 @@ namespace PerformanceFish.Patching;
[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors | ImplicitUseTargetFlags.WithMembers)]
public abstract class FishPatch : SingletonFactory<FishPatch>, IExposable, IHasDescription
{
private bool _enabled;
private string? _descriptionWithNotes;
private FishPrepatchBase[]? _resolvedLinkedPrepatches;
private FishPatch[]? _resolvedLinkedHarmonyPatches;
public virtual List<string> IncompatibleModIDs { get; } = [];
public virtual List<Type> LinkedPatches { get; } = [];
private FishPrepatchBase[] ResolvedLinkedPrepatches
=> _resolvedLinkedPrepatches ??= LinkedPatches
.Where(static type => type.IsAssignableTo(typeof(FishPrepatchBase)))
.Select(static type => FishPrepatchBase.Get(type))
.ToArray();
private FishPatch[] ResolvedLinkedHarmonyPatches
=> _resolvedLinkedHarmonyPatches ??= LinkedPatches
.Where(static type => type.IsAssignableTo(typeof(FishPatch))).Select(static type => Get(type))
.ToArray();
public bool DisabledForModCompatibility => ActiveMods.ContainsAnyOf(IncompatibleModIDs);
public virtual bool ShouldBenchmark => false;
public virtual bool Enabled
{
get => _enabled;
get => _enabled && !DisabledForModCompatibility;
set
{
if (value == _enabled)
@@ -37,30 +64,45 @@ public abstract class FishPatch : SingletonFactory<FishPatch>, IExposable, IHasD
TryUnpatch();
ShouldBePatched = shouldBePatched;
}
}
}
private bool _enabled;
for (var i = 0; i < ResolvedLinkedPrepatches.Length; i++)
ResolvedLinkedPrepatches[i].Enabled = value;
for (var i = 0; i < ResolvedLinkedHarmonyPatches.Length; i++)
ResolvedLinkedHarmonyPatches[i].Enabled = value;
}
}
public virtual bool DefaultState
{
[Pure] get => true;
}
public virtual bool ShowSettings => true;
public virtual int PrefixMethodPriority => TryGetPriority(PrefixMethodInfo);
public virtual int PostfixMethodPriority => TryGetPriority(PostfixMethodInfo);
public virtual int TranspilerMethodPriority => TryGetPriority(TranspilerMethodInfo);
public virtual int FinalizerMethodPriority => TryGetPriority(FinalizerMethodInfo);
public MethodInfo? HarmonyMethodInfo => HarmonyMethodInfos.FirstOrDefault();
public List<MethodInfo> HarmonyMethodInfos { get; } = new();
public List<MethodInfo> HarmonyMethodInfos { get; } = [];
public MethodInfo? BenchmarkHarmonyMethodInfo => BenchmarkHarmonyMethodInfos.FirstOrDefault();
public List<MethodInfo> BenchmarkHarmonyMethodInfos { get; } = new();
public List<MethodInfo> BenchmarkHarmonyMethodInfos { get; } = [];
private bool Patched { get; set; }
private bool ShouldBePatched { get; set; }
public virtual string? Name => null;
public virtual string? Description => null;
public string? DescriptionWithNotes
=> _descriptionWithNotes ??= !DisabledForModCompatibility
? Description
: Description
+ $"{(Description?.EndsWith(".") ?? true ? "" : ".")} Disabled for compatibility with {
IncompatibleModIDs.Select(static id => ActiveMods.TryGetModMetaData(id)?.Name)
.Where(static id => id != null).ToCommaList(true)}";
public virtual Delegate? TargetMethodGroup => null;
public virtual IEnumerable<Delegate>? TargetMethodGroups => null;
public virtual Expression<Action>? TargetMethod => null;
@@ -76,7 +118,7 @@ public abstract class FishPatch : SingletonFactory<FishPatch>, IExposable, IHasD
??= (TargetMethods?.Select(SymbolExtensions.GetMethodInfo)
?? TargetMethodGroups?.Select(static m => m.Method))?.Cast<MethodBase>().ToArray()
?? (TargetMethodInfo != null!
? new[] { TargetMethodInfo }
? [TargetMethodInfo]
: Array.Empty<MethodBase>());
private MethodBase[]? _targetMethodInfos;
@@ -102,7 +144,9 @@ public abstract class FishPatch : SingletonFactory<FishPatch>, IExposable, IHasD
public virtual MethodInfo? FinalizerMethodInfo
=> TryGetMethod("FINALIZER", static m => m.HasAttribute<HarmonyFinalizer>());
internal Type ParentType => GetType().DeclaringType;
internal Type ParentType
=> GetType().DeclaringType
?? ThrowHelper.ThrowArgumentNullException<Type>($"{this} is missing parent class. This is not supported");
internal IHasFishPatch? ParentClass
=> PerformanceFishMod.AllPatchClasses?.FirstOrDefault(patchClass => patchClass.GetType() == ParentType);
@@ -250,9 +294,13 @@ public abstract class FishPatch : SingletonFactory<FishPatch>, IExposable, IHasD
private static int TryGetPriority(MethodInfo? info)
=> info?.TryGetAttribute<HarmonyPriority>()?.info.priority ?? Priority.Normal;
protected internal virtual void OnPatchingCompleted()
{
}
public virtual void ExposeData() => Scribe_Values.Look(ref _enabled, "enabled", DefaultState);
public override string ToString() => GetType().FullName;
public override string ToString() => GetType().FullName ?? GetType().Name;
}
internal static class Benchmarking
@@ -317,7 +365,7 @@ internal static class Benchmarking
}
}
public class Results
public sealed class Results
{
public int Count;
public Stopwatch? Stopwatch;
@@ -1,21 +0,0 @@
// Copyright (c) 2023 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/.
namespace PerformanceFish.Patching;
public static class FishPatchExtensions
{
public static void PatchAll(this IEnumerable<FishPatch> patches)
{
foreach (var patch in patches)
patch.TryPatch();
}
public static void UnpatchAll(this IEnumerable<FishPatch> patches)
{
foreach (var patch in patches)
patch.TryUnpatch();
}
}

Some files were not shown because too many files have changed in this diff Show More