Skip to content

Commit

Permalink
Fix static caches in Vanilla Expanded Framework (#449)
Browse files Browse the repository at this point in the history
VEF has a static cache that handles additional pawn data, like pawn body size offset, multiplier to their health or the amount of food they eat.

As it often happens with such caches, they aren't cleared when reloading a game/starting a new game. I've included cache clearing when loading a game/starting a new game.

Another issue is that the data is re-cached (by default) every 180 ticks (which is fine), or every 2 seconds (which will cause issues in MP). I've added a patch to only re-cache the data only using the game ticks, rather than real time. On top of that, if the tick re-caching is disabled it will show a warning and enable it, before disabling real time re-caching (to prevent issues with never re-caching).

The final issue with those is that the caches can be created or re-calculated both during ticking and interface code. I've added a patch that will only allow re-caching during ticking, so in interface code it'll never re-cache and never add the newly created data to the dictionary.

Sadly, the solution here isn't the best I could have done due to `DictCache` being a generic type, making me unable to safely patch it. The last 2 points could have been more easily be done by making a prefix to `DictCache.GetCache` and changing all the bool input arguments to `false`.
  • Loading branch information
SokyranTheDragon authored Jun 9, 2024
1 parent a985d5c commit 1be8ae9
Showing 1 changed file with 84 additions and 0 deletions.
84 changes: 84 additions & 0 deletions Source/Mods/VanillaExpandedFramework.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public VanillaExpandedFramework(ModContentPack mod)
(PatchExtraPregnancyApproaches, "Extra Pregnancy Approaches", false),
(PatchWorkGiverDeliverResources, "Building stuff requiring non-construction skill", false),
(PatchExpandableProjectile, "Expandable projectile", false),
(PatchStaticCaches, "Static caches", false),
];

foreach (var (patchMethod, componentName, latePatch) in patches)
Expand Down Expand Up @@ -1437,5 +1438,88 @@ private static void PostIsConstruction(WorkGiver w, ref bool __result)
}

#endregion

#region Caches

private static FastInvokeHandler[] cacheGetterList;

private static void PatchStaticCaches()
{
cacheGetterList = new[]
{
// Currently the only cache in the mod.
// They also technically access DictCache<Pawn, CachedPawnData> directly, but
// PawnDataCache uses the same generics, and they end up sharing the dictionary.
"VFECore.PawnDataCache",
}
.Select(AccessTools.TypeByName)
.Select(t => AccessTools.PropertyGetter(t, "Cache"))
.Select(m => MethodInvoker.GetHandler(m))
.ToArray();

var patch = AccessTools.DeclaredMethod("VFECore.Pawn_DrawTracker_Patch:Postfix");
MpCompat.harmony.Unpatch(AccessTools.DeclaredPropertyGetter(typeof(Pawn_DrawTracker), nameof(Pawn_DrawTracker.DrawPos)), patch);

// The recaching happens (by default) every 180 ticks or 2 seconds.
// Disable recaching caused by real time passing in MP.
MpCompat.harmony.Patch(AccessTools.DeclaredMethod("VFECore.CacheTimer:TimeOutSeconds"),
prefix: new HarmonyMethod(MpMethodUtil.MethodOf(NeverTimeoutDueToRealTime)));

// Clear the caches
MpCompat.harmony.Patch(AccessTools.DeclaredMethod(typeof(GameComponentUtility), nameof(GameComponentUtility.FinalizeInit)),
postfix: new HarmonyMethod(typeof(VanillaExpandedFramework), nameof(ClearCache)));

// Prevent the timers from being reset in interface.
PatchingUtilities.PatchCancelInInterface(AccessTools.DeclaredMethod("VFECore.CacheTimer:ResetTimers"));
// Prevent the cache from being regenerated in interface.
// List of all types implementing ICacheable to patch, currently only 1 type does it.
var typeNames = new[]
{
"VFECore.CachedPawnData",
};
foreach (var typeName in typeNames)
{
const string regenerateCacheMethodName = "RegenerateCache";
var type = AccessTools.TypeByName(typeName);
// Look for the method with no arguments in case there's an overload with different arguments.
var method = AccessTools.DeclaredMethod(type, regenerateCacheMethodName, []);

if (method == null)
Log.Error($"Could not find {typeName}:{regenerateCacheMethodName}");
else if (method.ReturnType != typeof(bool))
Log.Error($"{typeName}:{regenerateCacheMethodName} has a return type of {method.ReturnType}, we were expecting bool.");
else
PatchingUtilities.PatchCancelInInterface(method);
}
}

private static void ClearCache()
{
foreach (var getter in cacheGetterList)
(getter(null) as IDictionary)?.Clear();
}

private static bool NeverTimeoutDueToRealTime(ref bool __result, ref int ___UpdateIntervalTicks, ref int ___UpdateIntervalSeconds)
{
// Result defaults to false, so if <= -1 just stop original from running
if (___UpdateIntervalSeconds <= -1)
return false;
// Let SP do its own thing
if (!MP.IsInMultiplayer)
return true;

// As a backup, enable tick based recaching (if it isn't enabled already)
if (___UpdateIntervalTicks <= -1)
{
Log.WarningOnce("Real time recaching disabled in MP but tick based recaching is disabled. Enabling tick based recaching (and disabling further warnings).", 652857559);
___UpdateIntervalTicks = ___UpdateIntervalSeconds * 90;
}

// Disable real time recaching
___UpdateIntervalSeconds = -1;
return false;
}

#endregion
}
}

0 comments on commit 1be8ae9

Please sign in to comment.