Skip to content

Instantly share code, notes, and snippets.

@pardeike
Last active September 26, 2020 16:46
Show Gist options
  • Save pardeike/fc0f044437d137d101b2992637e4455b to your computer and use it in GitHub Desktop.
Save pardeike/fc0f044437d137d101b2992637e4455b to your computer and use it in GitHub Desktop.
using HarmonyLib;
using RimWorld;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Verse;
using Verse.Sound;
namespace Brrainz
{
[HarmonyPatch]
static class ExceptionHandler
{
static readonly string RimworldAssemblyName = typeof(Pawn).Assembly.GetName().Name;
static readonly MethodInfo FinalizerMethod = SymbolExtensions.GetMethodInfo(() => Finalizer(default));
static readonly Dictionary<Assembly, ModMetaData> MetaDataCache = new Dictionary<Assembly, ModMetaData>();
static bool Prepare(MethodBase original)
{
if (original == null) return true;
var info = Harmony.GetPatchInfo(original);
var ok = info == null || info.Finalizers.All(finalizer => finalizer.PatchMethod != FinalizerMethod);
if (ok) Log.Warning($"Brrainz: Patching {original.DeclaringType.FullName}.{original.Name} to report exceptions in mods");
return ok;
}
static IEnumerable<MethodBase> TargetMethods()
{
yield return AccessTools.Method(typeof(Root_Play), nameof(Root_Play.Start));
yield return AccessTools.Method(typeof(Root_Play), nameof(Root_Play.Update));
yield return AccessTools.Method(typeof(SoundRoot), nameof(SoundRoot.Update));
yield return AccessTools.Method(typeof(UIRoot_Entry), nameof(UIRoot_Entry.Init));
yield return AccessTools.Method(typeof(UIRoot_Entry), nameof(UIRoot_Entry.UIRootOnGUI));
yield return AccessTools.Method(typeof(UIRoot_Entry), nameof(UIRoot_Entry.UIRootUpdate));
yield return AccessTools.Method(typeof(UIRoot_Play), nameof(UIRoot_Play.Init));
yield return AccessTools.Method(typeof(UIRoot_Play), nameof(UIRoot_Play.UIRootOnGUI));
yield return AccessTools.Method(typeof(UIRoot_Play), nameof(UIRoot_Play.UIRootUpdate));
}
static bool IsModMethod(MethodBase method)
{
if (method == FinalizerMethod) return false;
var references = method.DeclaringType.Assembly.GetReferencedAssemblies();
return references.Any(assemblyName => assemblyName.Name == RimworldAssemblyName);
}
static ModMetaData GetModMetaData(Assembly assembly)
{
if (MetaDataCache.TryGetValue(assembly, out var metaData) == false)
{
var contentPack = LoadedModManager.RunningMods
.FirstOrDefault(m => m.assemblies.loadedAssemblies.Contains(assembly));
if (contentPack != null)
metaData = new ModMetaData(contentPack.RootDir);
MetaDataCache.Add(assembly, metaData);
}
return metaData;
}
[HarmonyPriority(int.MinValue)]
static void Finalizer(Exception __exception)
{
var n = 0;
var exception = __exception;
MethodBase topMethod = null;
var seenAssemblies = new HashSet<Assembly>();
var lines = new List<string>();
while (exception != null)
{
var st = new StackTrace(exception);
st.GetFrames().Select(frame => frame.GetMethod())
.DoIf(method => method != null, method =>
{
topMethod = topMethod ?? method;
var assembly = method.DeclaringType.Assembly;
if (IsModMethod(method) && seenAssemblies.Add(assembly))
{
var metaData = GetModMetaData(assembly);
if (metaData != null && metaData.IsCoreMod == false)
{
if (n == 0)
{
lines.Add($"### Exception: {__exception.Message.Trim()}");
lines.Add($"### Where: {topMethod.FullDescription()}");
lines.Add("### Involved mods in order of most likely cause:");
lines.Add("### [MOD_NAME_AND_AUTHOR] [STEAM_ID] [URL] [LAST_METHOD_EXECUTED]");
}
lines.Add($"### {++n}) [{metaData.Name} by {metaData.Author}] [{metaData.SteamAppId}] [{metaData.Url}] [{method.DeclaringType.FullName}::{method.Name}]");
}
}
});
exception = exception.InnerException;
}
lines.Do(line => Log.Warning(line));
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment