Files
Performance-Fish/Source/PerformanceFish/Planet/WorldObjectsOptimization.cs
T

368 lines
11 KiB
C#

// 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 PerformanceFish.Prepatching;
using RimWorld.Planet;
namespace PerformanceFish.Planet;
public sealed class WorldObjectsOptimization : ClassWithFishPrepatches
{
public sealed class WorldObjectsHolderTickPatch : FishPrepatch
{
public override bool DefaultState => true;
private static FishTable<Type, bool> _skippableCompTypes = null!;
private static FishTable<Type, bool> _skippableWorldObjectTypes = null!;
private static readonly List<IThingHolder> _tmpThingHolders = [];
private static readonly List<SkippedWorldObject> _skippedWorldObjects = [];
private static List<WorldObject>? _cachedWorldObjectsReference;
private static List<Map>? _cachedMapsReference;
private readonly record struct SkippedWorldObject(WorldObject WorldObject,
List<WorldObjectComp> Comps, int CompsVersion, bool HasEnterCooldownComp);
public override string? Description { get; }
= "Caches the world objects that need ticking and skips objects with no active tick behavior. "
+ "Objects with changing contents or components are rechecked before each tick.";
public override MethodBase TargetMethodBase { get; }
= AccessTools.Method(typeof(WorldObjectsHolder), nameof(WorldObjectsHolder.WorldObjectsHolderTick));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(WorldObjectsHolderTick);
public static void WorldObjectsHolderTick(WorldObjectsHolder instance)
{
if (CacheDirty(instance))
UpdateCache(instance);
var worldObjects = WorldObjectsHolder.tmpWorldObjects;
for (var i = 0; i < worldObjects.Count; i++)
#if V1_6
worldObjects[i].DoTick();
#else
worldObjects[i].Tick();
#endif
}
public static void UpdateCache(WorldObjectsHolder instance)
{
var staticWorldObjects = WorldObjectsHolder.tmpWorldObjects;
staticWorldObjects.Clear();
_skippedWorldObjects.Clear();
var instanceWorldObjects = instance.worldObjects;
for (var i = 0; i < instanceWorldObjects.Count; i++)
{
var worldObject = instanceWorldObjects[i];
if (worldObject is not MapParent { HasMap: true }
&& CanSkipWorldObjectTick(worldObject)
&& CanSkipCompTick(worldObject))
{
var comps = worldObject.comps;
var hasEnterCooldownComp = false;
for (var j = 0; j < comps.Count; j++)
hasEnterCooldownComp |= comps[j] is EnterCooldownComp;
_skippedWorldObjects.Add(new(worldObject, comps, comps._version, hasEnterCooldownComp));
continue;
}
staticWorldObjects.Add(worldObject);
}
CachedWorldObjectsVersion = instanceWorldObjects._version;
CachedMapsVersion = Current.gameInt.maps._version;
_cachedWorldObjectsReference = instanceWorldObjects;
_cachedMapsReference = Current.gameInt.maps;
}
private static bool CanSkipWorldObjectTick(WorldObject worldObject)
{
return IsSkippableWorldObjectType(worldObject.GetType()) && !HasTickableContents(worldObject);
}
private static bool HasTickableContents(WorldObject worldObject)
{
if (worldObject is not IThingHolder holder
|| worldObject is IThingHolderTickable { ShouldTickContents: false })
return false;
lock (_tmpThingHolders)
{
_tmpThingHolders.Clear();
try
{
_tmpThingHolders.Add(holder);
holder.GetChildHolders(_tmpThingHolders);
for (var i = 0; i < _tmpThingHolders.Count; i++)
{
var heldThings = _tmpThingHolders[i].GetDirectlyHeldThings();
if (heldThings is not null && heldThings.Owner is not Map and not Caravan)
return true;
}
return false;
}
finally
{
_tmpThingHolders.Clear();
}
}
}
private static bool CanSkipCompTick(WorldObject worldObject)
{
var comps = worldObject.comps;
if (comps.Count == 0)
return true;
for (var i = comps.Count; i-- > 0;)
{
var comp = comps[i];
if (comp is EnterCooldownComp { Active: true })
return false;
if (!IsSkippableCompType(comp.GetType()))
return false;
}
return true;
}
public static int
CachedWorldObjectsVersion = -2,
CachedMapsVersion = -2;
private static Type?[]
_whitelistedTickingCompTypes =
[
typeof(WorldObjectComp)
],
_whitelistedWorldObjectTypes =
[
// Settlement.TickInterval checks trader and defeat state even without a map.
typeof(WorldObject),
typeof(MapParent)
];
public static void AddCompToWhiteList(Type compType)
{
_whitelistedTickingCompTypes = _whitelistedTickingCompTypes.Add(compType);
ResetSkippableCompTypes();
}
public static void AddWorldObjectToWhiteList(Type worldObjectType)
{
_whitelistedWorldObjectTypes = _whitelistedWorldObjectTypes.Add(worldObjectType);
ResetSkippableWorldObjectTypes();
}
private static void ResetSkippableCompTypes()
{
_skippableCompTypes = new()
{
ValueInitializer = static type => ComputeIsSkippableCompType(type)
};
}
private static void ResetSkippableWorldObjectTypes()
{
_skippableWorldObjectTypes = new()
{
ValueInitializer = static type => ComputeIsSkippableWorldObjectType(type)
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsSkippableCompType(Type type) => _skippableCompTypes.GetOrAdd(type);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsSkippableWorldObjectType(Type type) => _skippableWorldObjectTypes.GetOrAdd(type);
private static bool ComputeIsSkippableCompType(Type type)
{
if (AccessTools.Method(type, nameof(WorldObjectComp.CompTick))?.DeclaringType is not { } declaringType
|| !_whitelistedTickingCompTypes.Contains(declaringType))
{
return false;
}
#if V1_6
return AccessTools.Method(type, nameof(WorldObjectComp.CompTickInterval), [typeof(int)])?.DeclaringType
is { } intervalDeclaringType
&& _whitelistedTickingCompTypes.Contains(intervalDeclaringType);
#else
return true;
#endif
}
private static bool ComputeIsSkippableWorldObjectType(Type type)
{
if (AccessTools.Method(type, "Tick")?.DeclaringType is not { } declaringType
|| !_whitelistedWorldObjectTypes.Contains(declaringType))
{
return false;
}
#if V1_6
return AccessTools.Method(type, "TickInterval", [typeof(int)])?.DeclaringType is { } intervalDeclaringType
&& _whitelistedWorldObjectTypes.Contains(intervalDeclaringType);
#else
return true;
#endif
}
public static bool CacheDirty(WorldObjectsHolder instance)
{
if (!ReferenceEquals(_cachedWorldObjectsReference, instance.worldObjects)
|| CachedWorldObjectsVersion != instance.worldObjects._version
|| !ReferenceEquals(_cachedMapsReference, Current.gameInt.maps)
|| CachedMapsVersion != Current.gameInt.maps._version)
return true;
for (var i = 0; i < _skippedWorldObjects.Count; i++)
{
var skipped = _skippedWorldObjects[i];
var worldObject = skipped.WorldObject;
var comps = worldObject.comps;
if (!ReferenceEquals(comps, skipped.Comps) || comps._version != skipped.CompsVersion
|| worldObject is MapParent { HasMap: true }
|| HasTickableContents(worldObject))
return true;
if (skipped.HasEnterCooldownComp)
for (var j = 0; j < comps.Count; j++)
if (comps[j] is EnterCooldownComp { Active: true })
return true;
}
return false;
}
public static void SetDirty() => CachedWorldObjectsVersion = CachedMapsVersion = -2;
static WorldObjectsHolderTickPatch()
{
ResetSkippableCompTypes();
ResetSkippableWorldObjectTypes();
Cache.Utility.Cleared += SetDirty;
}
}
public sealed class ExpandingIconCaching : FishPrepatch
{
public override string? Description { get; }
= "Caches icons that get displayed for world objects like settlements on the planet view and adds various "
+ "safety checks to better catch and log errors in case of missing icons";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredPropertyGetter(typeof(WorldObject), nameof(WorldObject.ExpandingIcon));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static Texture2D ReplacementBody(WorldObject __instance)
=> __instance.ExpandingIconCache() ?? InitializeExpandingIcon(__instance);
[MethodImpl(MethodImplOptions.NoInlining)]
public static Texture2D InitializeExpandingIcon(WorldObject? worldObject)
{
if (worldObject is null)
return BaseContent.BadTex;
var def = worldObject.def;
ref var cache = ref worldObject.ExpandingIconCache();
try
{
cache = def?.ExpandingIconTexture;
if (cache != null)
return cache;
var material = worldObject.Material;
if (material != null)
cache = material.mainTexture as Texture2D;
if (cache == null)
{
cache = BaseContent.BadTex;
Log.Error($"No expanding icon found for '{worldObject.ToStringSafe()}' of def '{
def?.ToStringSafe()}' from mod '{def?.modContentPack?.Name}'. Assigning default.");
}
return cache;
}
catch (Exception ex)
{
Log.Error($"Exception while initializing icon for '{worldObject.ToStringSafe()}' of def '{
def?.ToStringSafe()}' from mod '{def?.modContentPack?.Name}':\n{ex}");
return cache = BaseContent.BadTex;
}
}
}
public sealed class ExpandingIconColorCaching : FishPrepatch
{
public override string? Description { get; }
= "Caches colors of icons that get displayed for world objects like settlements on the planet view and "
+ "adds various safety checks to better catch and log errors in case of missing data";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredPropertyGetter(typeof(WorldObject), nameof(WorldObject.ExpandingIconColor));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static Color ReplacementBody(WorldObject __instance)
=> __instance.ExpandingIconColorCache() ?? InitializeExpandingIconColor(__instance);
[MethodImpl(MethodImplOptions.NoInlining)]
public static Color InitializeExpandingIconColor(WorldObject? worldObject)
{
if (worldObject is null)
return Color.white;
var def = worldObject.def;
ref var cache = ref worldObject.ExpandingIconColorCache();
try
{
cache = def?.expandingIconColor;
if (cache != null)
return cache.GetValueOrDefault();
var material = worldObject.Material;
if (material != null)
cache = material.color;
if (cache == null)
{
cache = Color.white;
Log.Error($"No expanding icon color found for '{worldObject.ToStringSafe()}' of def '{
def?.ToStringSafe()}' from mod '{def?.modContentPack?.Name}'. Assigning default.");
}
return cache.GetValueOrDefault();
}
catch (Exception ex)
{
Log.Error($"Exception while initializing icon color for '{worldObject.ToStringSafe()}' of def '{
def?.ToStringSafe()}' from mod '{def?.modContentPack?.Name}':\n{ex}");
return (cache = Color.white).GetValueOrDefault();
}
}
}
}