Created
November 1, 2018 13:47
-
-
Save notfood/893b31b1a32b2043d8def1dbbc177a91 to your computer and use it in GitHub Desktop.
Caches the prepatched.xml to boost load times
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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