// 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 _skippableCompTypes = null!; private static FishTable _skippableWorldObjectTypes = null!; private static readonly List _tmpThingHolders = []; private static readonly List _skippedWorldObjects = []; private static List? _cachedWorldObjectsReference; private static List? _cachedMapsReference; private readonly record struct SkippedWorldObject(WorldObject WorldObject, List 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(); } } } }