Constrain reflection fast paths and preserve 1.6 tick behavior
This commit is contained in:
@@ -3,7 +3,6 @@
|
||||
// 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;
|
||||
@@ -17,19 +16,12 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches
|
||||
{
|
||||
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);
|
||||
private static List<WorldObject>? _cachedSnapshotReference;
|
||||
private static int _cachedSnapshotVersion = -2;
|
||||
|
||||
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.";
|
||||
= "Reuses the world-object tick snapshot until the source list changes. Every object still receives DoTick.";
|
||||
|
||||
public override MethodBase TargetMethodBase { get; }
|
||||
= AccessTools.Method(typeof(WorldObjectsHolder), nameof(WorldObjectsHolder.WorldObjectsHolderTick));
|
||||
@@ -53,206 +45,40 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches
|
||||
|
||||
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);
|
||||
}
|
||||
var snapshot = WorldObjectsHolder.tmpWorldObjects;
|
||||
snapshot.Clear();
|
||||
snapshot.AddRange(instanceWorldObjects);
|
||||
|
||||
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;
|
||||
_cachedSnapshotReference = snapshot;
|
||||
_cachedSnapshotVersion = snapshot._version;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// Retained for mods that called the old whitelist API. The snapshot no longer skips any ticks.
|
||||
public static void AddCompToWhiteList(Type compType) => SetDirty();
|
||||
public static void AddWorldObjectToWhiteList(Type worldObjectType) => SetDirty();
|
||||
|
||||
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;
|
||||
var source = instance.worldObjects;
|
||||
var snapshot = WorldObjectsHolder.tmpWorldObjects;
|
||||
return !ReferenceEquals(_cachedWorldObjectsReference, source)
|
||||
|| CachedWorldObjectsVersion != source._version
|
||||
|| !ReferenceEquals(_cachedSnapshotReference, snapshot)
|
||||
|| _cachedSnapshotVersion != snapshot._version
|
||||
|| snapshot.Count != source.Count;
|
||||
}
|
||||
|
||||
public static void SetDirty() => CachedWorldObjectsVersion = CachedMapsVersion = -2;
|
||||
|
||||
static WorldObjectsHolderTickPatch()
|
||||
{
|
||||
ResetSkippableCompTypes();
|
||||
ResetSkippableWorldObjectTypes();
|
||||
Cache.Utility.Cleared += SetDirty;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user