Stabilize RimWorld 1.6 caches and tick optimizations

This commit is contained in:
2026-10-02 04:00:27 -04:00
parent 3199c1a85e
commit 227dcd173f
14 changed files with 209 additions and 76 deletions
@@ -15,16 +15,21 @@ 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; }
= "The world objects holder is responsible for ticking every world object. This includes settlements, "
+ "caravans, outposts and any other object placed in the world, instead of maps. Normally it ticks "
+ "everything equally, including many static objects that cannot possibly affect anything from a tick. "
+ "This patch improves the world objects holder to determine objects that need ticking in advance, cache "
+ "the list of them, and only tick those, skipping all the others.";
= "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));
@@ -38,7 +43,7 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches
UpdateCache(instance);
var worldObjects = WorldObjectsHolder.tmpWorldObjects;
for (var i = worldObjects.Count; i-- > 0;)
for (var i = 0; i < worldObjects.Count; i++)
#if V1_6
worldObjects[i].DoTick();
#else
@@ -50,18 +55,23 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches
{
var staticWorldObjects = WorldObjectsHolder.tmpWorldObjects;
staticWorldObjects.Clear();
_skippedWorldObjects.Clear();
var instanceWorldObjects = instance.worldObjects;
for (var i = instanceWorldObjects.Count; i-- > 0;)
for (var i = 0; i < instanceWorldObjects.Count; i++)
{
var worldObject = instanceWorldObjects[i];
if (worldObject is not MapParent { HasMap: true }
&& CanSkipWorldObjectTick(worldObject)
&& (worldObject is not Settlement settlement || settlement.trader?.stock is null)
&& 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;
}
@@ -70,6 +80,8 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches
CachedWorldObjectsVersion = instanceWorldObjects._version;
CachedMapsVersion = Current.gameInt.maps._version;
_cachedWorldObjectsReference = instanceWorldObjects;
_cachedMapsReference = Current.gameInt.maps;
}
private static bool CanSkipWorldObjectTick(WorldObject worldObject)
@@ -79,31 +91,30 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches
private static bool HasTickableContents(WorldObject worldObject)
{
if (worldObject is not IThingHolder holder)
if (worldObject is not IThingHolder holder
|| worldObject is IThingHolderTickable { ShouldTickContents: false })
return false;
lock (_tmpThingHolders)
{
_tmpThingHolders.Clear();
_tmpThingHolders.Add(holder);
holder.GetChildHolders(_tmpThingHolders);
for (var i = 0; i < _tmpThingHolders.Count; i++)
try
{
var thingHolder = _tmpThingHolders[i];
if (thingHolder is IThingHolderTickable tickable && !tickable.ShouldTickContents)
continue;
var directlyHeldThings = thingHolder.GetDirectlyHeldThings();
if (directlyHeldThings?.Owner is not null and not Map and not Caravan)
_tmpThingHolders.Add(holder);
holder.GetChildHolders(_tmpThingHolders);
for (var i = 0; i < _tmpThingHolders.Count; i++)
{
_tmpThingHolders.Clear();
return true;
var heldThings = _tmpThingHolders[i].GetDirectlyHeldThings();
if (heldThings is not null && heldThings.Owner is not Map and not Caravan)
return true;
}
}
_tmpThingHolders.Clear();
return false;
return false;
}
finally
{
_tmpThingHolders.Clear();
}
}
}
@@ -137,10 +148,9 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches
],
_whitelistedWorldObjectTypes =
[
// Settlement.TickInterval checks trader and defeat state even without a map.
typeof(WorldObject),
typeof(MapParent),
typeof(Settlement),
ModCompatibility.Types.RealRuins.POIWorldObject
typeof(MapParent)
];
public static void AddCompToWhiteList(Type compType)
@@ -211,8 +221,31 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches
}
public static bool CacheDirty(WorldObjectsHolder instance)
=> CachedWorldObjectsVersion != instance.worldObjects._version
|| CachedMapsVersion != Current.gameInt.maps._version;
{
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;