Skip to content

Instantly share code, notes, and snippets.

@notfood
Created November 1, 2018 13:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save notfood/893b31b1a32b2043d8def1dbbc177a91 to your computer and use it in GitHub Desktop.
Save notfood/893b31b1a32b2043d8def1dbbc177a91 to your computer and use it in GitHub Desktop.
Caches the prepatched.xml to boost load times
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using System.Linq;
using Harmony;
using RimWorld;
using Verse;
namespace BoostReLoad
{
public class BoostReLoadMod : Mod
{
const string CACHE_FILE = "Boost.xml";
static bool invalid;
static FileInfo cacheFile;
static XmlDocument xmlCache;
static readonly List<FileInfo> files = new List<FileInfo>();
internal static IEnumerable<FileInfo> Files => files;
internal static FileInfo CacheFile => cacheFile ?? (cacheFile = new FileInfo(Path.Combine(GenFilePaths.SaveDataFolderPath, CACHE_FILE)));
public static bool Invalid => invalid;
public BoostReLoadMod(ModContentPack content) : base(content)
{
var harmony = HarmonyInstance.Create("boostreload");
harmony.PatchAll(System.Reflection.Assembly.GetExecutingAssembly());
LongEventHandler.SetCurrentEventText("Validating Cache");
invalid = IsCacheInvalid();
LongEventHandler.SetCurrentEventText("");
Log.Message("BoostReLoad :: Cache is " + (invalid ? "invalid" : "valid") + "\n\t" + CacheFile.FullName);
}
/*
* We want to find if our previously made cache is still valid
* Compare the date first, later we will compare if any file went missing
* It has special support for DefsOnDemand mod.
*/
/// Checks if cache should be rebuilt
bool IsCacheInvalid() {
var cache = CacheFile;
if (!cache.Exists) {
return true;
}
FindAllXmlFiles();
if (IsAnyFileNewer(files, cache.LastWriteTime)) {
return true;
}
xmlCache = new XmlDocument();
xmlCache.Load(CacheFile.OpenRead());
// TODO:
// check modlist + filelist here
return false;
}
/*
* Vanilla loads the files into memory, we don't want that yet.
* Instead we build the list of files to check. It doesn't matter if there
* are extra files as we will compare the cache list later.
*/
/// Collects all the xml files without accessing them
void FindAllXmlFiles() {
foreach (var mod in LoadedModManager.RunningMods)
{
var rootDir = mod.RootDir;
files.AddRange(GetXmlFiles(rootDir, "Defs"));
files.AddRange(GetXmlFiles(rootDir, "Defs_OnDemand"));
files.AddRange(GetXmlFiles(rootDir, "Patches"));
}
}
/// All xml files in the path
static FileInfo[] GetXmlFiles(string rootdir, string folder)
{
var fullPath = new DirectoryInfo(Path.Combine(rootdir, folder));
if (fullPath.Exists) {
return fullPath.GetFiles("*.xml", SearchOption.AllDirectories);
}
return new FileInfo[0];
}
/// Compares the given date against the modification time of a list of files.
static bool IsAnyFileNewer(IEnumerable<FileInfo> list, DateTime date)
{
return list.Any(f => f.LastWriteTime > date);
}
/// Adds the xml document to be packed into the cache
internal static void ProcessIntoCache(XmlDocument xml, Dictionary<XmlNode, LoadableXmlAsset> assetlookup)
{
if (xmlCache == null) {
xmlCache = new XmlDocument();
xmlCache.AppendChild(xmlCache.CreateElement(string.Empty, "Defs", string.Empty));
}
foreach (XmlNode node in xml.FirstChild)
{
if (node.Attributes == null)
{
continue;
}
var newNode = xmlCache.ImportNode(node, true);
if (assetlookup.TryGetValue(node, out LoadableXmlAsset modAsset))
{
XmlAttribute attr;
attr = xmlCache.CreateAttribute("Mod");
attr.Value = modAsset.mod.Identifier;
newNode.Attributes.Append(attr);
}
xmlCache.FirstChild.AppendChild(newNode);
}
}
/// Writes xmlCache to disk
internal static void SaveCache() {
var xmlWriterSettings = new XmlWriterSettings
{
Indent = false,
OmitXmlDeclaration = true,
NewLineHandling = NewLineHandling.Replace
};
try
{
using (var writer = XmlWriter.Create(CacheFile.OpenWrite(), xmlWriterSettings))
{
xmlCache.WriteTo(writer);
}
}
catch (Exception e)
{
Log.Warning("BoostReLoad :: Can't save cache\n\t" + e);
}
finally
{
ClearXmlCache();
LongEventHandler.SetCurrentEventText("");
}
}
/// clears xmlCache from memory, must be called after we are done
internal static void ClearXmlCache()
{
xmlCache = null;
}
/// We have to feed the assetlookup back, from the saved metadata
/// Maybe should remove the attribute but it doesn't seem to do anything
/// bad
internal static XmlDocument LoadCache(Dictionary<XmlNode, LoadableXmlAsset> assetlookup)
{
var core = LoadedModManager.RunningMods.First(m => m.IsCoreMod);
var modList = new Dictionary<string, LoadableXmlAsset>();
foreach(XmlNode node in xmlCache.FirstChild) {
if (node.Attributes == null) {
continue;
}
var modID = node.Attributes["Mod"]?.Value;
if (modID != null) {
LoadableXmlAsset modAsset;
if (!modList.TryGetValue(modID, out modAsset)) {
var mod = LoadedModManager.RunningMods.FirstOrFallback(m => m.Identifier == modID, core);
var defPackage = new DefPackage(CACHE_FILE, string.Empty);
mod.AddDefPackage(defPackage);
modAsset = new LoadableXmlAsset(modID, Path.Combine(GenFilePaths.CoreModsFolderPath, modID), "<Empty />")
{
mod = mod,
defPackage = defPackage
};
modList.Add(modID, modAsset);
}
assetlookup.Add(node, modAsset);
}
}
return xmlCache;
}
}
/*
* If our cache is valid, we want to skip this.
*/
[HarmonyPatch(typeof(LoadedModManager))]
[HarmonyPatch("ApplyPatches")]
class Patch_LoadedModManager_ApplyPatches
{
static bool Prefix()
{
return BoostReLoadMod.Invalid;
}
}
/*
* Here is where we create the cache if it's invalid
* The cache file should exist, we checked earlier
*/
[HarmonyPatch(typeof(LoadedModManager))]
[HarmonyPatch("CombineIntoUnifiedXML")]
class Patch_LoadedModManager_CombineIntoUnifiedXML
{
static bool Prefix(Dictionary<XmlNode, LoadableXmlAsset> assetlookup, ref XmlDocument __result)
{
if (BoostReLoadMod.Invalid)
{
LongEventHandler.SetCurrentEventText("Rebuilding Cache");
return true;
}
__result = BoostReLoadMod.LoadCache(assetlookup);
// Used cached XML, don't load anymore
return false;
}
/*
* LoadedModManager.CombineIntoUnifiedXML can be called many times
* thanks to PatchOperationLoadInDemand.
* Let's just save the individual results and combine afterwards...
*/
static void Postfix(Dictionary<XmlNode, LoadableXmlAsset> assetlookup, ref XmlDocument __result)
{
// Our cache is invalid, let's snatch it.
if (BoostReLoadMod.Invalid) {
BoostReLoadMod.ProcessIntoCache(__result, assetlookup);
}
}
}
/*
* Why here? It's the last relevant method run by
* LoadedModManager.LoadAllActiveMods, here we will cache the collected
* files
*/
[HarmonyPatch(typeof(LoadedModManager))]
[HarmonyPatch("ClearCachedPatches")]
class Patch_LoadedModManager_ClearCachedPatches
{
static bool Prefix() {
return BoostReLoadMod.Invalid;
}
static void Postfix()
{
if (BoostReLoadMod.Invalid)
{
BoostReLoadMod.SaveCache();
} else {
BoostReLoadMod.ClearXmlCache();
}
}
}
/*
* Just in case...
*/
[HarmonyPatch(typeof(ModContentPack))]
[HarmonyPatch("LoadPatches")]
class Patch_ModContentPack_LoadPatches
{
static bool Prefix()
{
if(!BoostReLoadMod.Invalid) {
Log.Warning("BoostReLoad :: This should not be happening!\n\t" + Environment.StackTrace);
return false;
}
return true;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment