Decompiled source of Prefab Hash Probe v0.2.0

plugins/PrefabHashProbe/PrefabHashProbe.dll

Decompiled 3 weeks ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using HarmonyLib;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[BepInPlugin("local.diagnostics.prefabhashprobe", "Prefab Hash Probe", "0.2.0")]
public sealed class PrefabHashProbe : BaseUnityPlugin
{
	private sealed class Missing
	{
		public long Attempts;

		public readonly List<string> Examples = new List<string>();

		public readonly HashSet<object> SampledZdos = new HashSet<object>();
	}

	private static PrefabHashProbe probe;

	private Harmony harmony;

	private ConfigEntry<bool> suppress;

	private ConfigEntry<bool> diagnostics;

	private string folder;

	private Type sceneType;

	private Type zdoType;

	private Type managerType;

	private Type netType;

	private FieldInfo registryField;

	private MethodInfo getHash;

	private MethodInfo getPosition;

	private object observedScene;

	private float sceneSince;

	private float nextWrite;

	private int phase;

	private int sceneNumber;

	private readonly Dictionary<int, Missing> missing = new Dictionary<int, Missing>();

	private readonly WarningGate warningGate = new WarningGate(10000);

	private readonly Dictionary<int, string> knownNames = new Dictionary<int, string>();

	private bool diagnosticFailureReported;

	private void Awake()
	{
		//IL_016b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0175: Expected O, but got Unknown
		//IL_0209: Unknown result type (might be due to invalid IL or missing references)
		//IL_0217: Expected O, but got Unknown
		//IL_01ba: Unknown result type (might be due to invalid IL or missing references)
		//IL_01c8: Expected O, but got Unknown
		probe = this;
		suppress = ((BaseUnityPlugin)this).Config.Bind<bool>("Logging", "SuppressRepeatedMissingWarnings", true, "Keep the first identical Missing prefab hash warning; count every failed attempt in CSV. Does not skip object loading or change saves.");
		diagnostics = ((BaseUnityPlugin)this).Config.Bind<bool>("Diagnostics", "EnableDiagnostics", false, "Opt-in detailed exports: prefab names, object IDs/positions, plugin paths and environment. Restart required. No uploads.");
		if (diagnostics.Value)
		{
			folder = Path.Combine(Paths.BepInExRootPath, "diagnostics", "PrefabHashProbe", DateTime.UtcNow.ToString("yyyyMMdd-HHmmss") + "-" + Guid.NewGuid().ToString("N").Substring(0, 6));
			Directory.CreateDirectory(folder);
		}
		sceneType = AccessTools.TypeByName("ZNetScene");
		zdoType = AccessTools.TypeByName("ZDO");
		managerType = AccessTools.TypeByName("ZDOMan");
		netType = AccessTools.TypeByName("ZNet");
		if (sceneType == null || zdoType == null)
		{
			throw new InvalidOperationException("Game types unavailable");
		}
		registryField = AccessTools.Field(sceneType, "m_namedPrefabs");
		getHash = AccessTools.Method(zdoType, "GetPrefab", Type.EmptyTypes, (Type[])null);
		getPosition = AccessTools.Method(zdoType, "GetPosition", Type.EmptyTypes, (Type[])null);
		harmony = new Harmony("local.diagnostics.prefabhashprobe");
		if (diagnostics.Value)
		{
			harmony.Patch((MethodBase)AccessTools.Method(sceneType, "CreateObject", new Type[1] { zdoType }, (Type[])null), new HarmonyMethod(typeof(PrefabHashProbe), "BeforeCreate", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
		}
		harmony.Patch((MethodBase)AccessTools.Method(AccessTools.TypeByName("ZLog"), "LogWarning", new Type[1] { typeof(object) }, (Type[])null), new HarmonyMethod(typeof(PrefabHashProbe), "BeforeWarning", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
		((BaseUnityPlugin)this).Logger.LogInfo((object)(diagnostics.Value ? ("Diagnostic output: " + folder) : "Detailed diagnostics disabled; no diagnostic files written."));
		((BaseUnityPlugin)this).Logger.LogInfo((object)("Repeated missing-prefab warning suppression: " + suppress.Value));
	}

	private static void BeforeCreate(object __instance, object __0)
	{
		//IL_012e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0133: Unknown result type (might be due to invalid IL or missing references)
		if ((Object)(object)probe == (Object)null || __0 == null)
		{
			return;
		}
		try
		{
			int num = (int)probe.getHash.Invoke(__0, null);
			if (num == 0)
			{
				return;
			}
			IDictionary dictionary = (IDictionary)probe.registryField.GetValue(__instance);
			object? obj = dictionary[num];
			Object val = (Object)((obj is Object) ? obj : null);
			if (val != (Object)null)
			{
				return;
			}
			if (!probe.missing.TryGetValue(num, out var value))
			{
				if (probe.missing.Count >= 10000)
				{
					return;
				}
				value = new Missing();
				probe.missing.Add(num, value);
				((BaseUnityPlugin)probe).Logger.LogInfo((object)("First missing hash " + num + "; capturing ZDO example."));
			}
			value.Attempts++;
			if (value.Examples.Count < 8 && value.SampledZdos.Add(__0))
			{
				object value2 = AccessTools.Field(probe.zdoType, "m_uid").GetValue(__0);
				Vector3 val2 = (Vector3)probe.getPosition.Invoke(__0, null);
				value.Examples.Add(string.Concat(value2, " @ ", val2.x.ToString("R", CultureInfo.InvariantCulture), ",", val2.y.ToString("R", CultureInfo.InvariantCulture), ",", val2.z.ToString("R", CultureInfo.InvariantCulture)));
			}
		}
		catch (Exception e)
		{
			probe.ReportFailure(e);
		}
	}

	private static bool BeforeWarning(object __0)
	{
		if (!((Object)(object)probe == (Object)null))
		{
			return probe.warningGate.Allow(__0, probe.suppress.Value);
		}
		return true;
	}

	private static object Instance(Type t)
	{
		if (t == null)
		{
			return null;
		}
		PropertyInfo propertyInfo = AccessTools.Property(t, "instance");
		if (propertyInfo != null)
		{
			return propertyInfo.GetValue(null, null);
		}
		FieldInfo fieldInfo = AccessTools.Field(t, "s_instance") ?? AccessTools.Field(t, "m_instance");
		if (!(fieldInfo == null))
		{
			return fieldInfo.GetValue(null);
		}
		return null;
	}

	private void Update()
	{
		//IL_008e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0099: Expected O, but got Unknown
		try
		{
			object obj = Instance(sceneType);
			if (!object.ReferenceEquals(obj, observedScene))
			{
				if (observedScene != null)
				{
					WriteMissing();
				}
				missing.Clear();
				knownNames.Clear();
				warningGate.Reset();
				observedScene = obj;
				sceneSince = Time.realtimeSinceStartup;
				phase = 0;
				sceneNumber++;
			}
			if (diagnostics.Value && obj != null && (!(obj is Object) || !((Object)obj == (Object)null)))
			{
				float num = Time.realtimeSinceStartup - sceneSince;
				if (phase == 0 && num > 5f)
				{
					phase = 1;
					Snapshot(obj, "initial");
				}
				else if (phase == 1 && num > 25f)
				{
					phase = 2;
					Snapshot(obj, "settled");
				}
				if (Time.realtimeSinceStartup > nextWrite)
				{
					nextWrite = Time.realtimeSinceStartup + 5f;
					WriteMissing();
				}
			}
		}
		catch (Exception e)
		{
			ReportFailure(e);
		}
	}

	private void Snapshot(object scene, string phaseName)
	{
		string text = "scene-" + sceneNumber + "-" + phaseName;
		IDictionary dictionary = (IDictionary)registryField.GetValue(scene);
		Dictionary<string, string> sources = GetSources();
		using (StreamWriter streamWriter = Writer(text + "-registry.csv"))
		{
			streamWriter.WriteLine("hash,name,source,calculated_hash");
			foreach (DictionaryEntry item in dictionary)
			{
				object? value = item.Value;
				GameObject val = (GameObject)((value is GameObject) ? value : null);
				if (!((Object)(object)val == (Object)null))
				{
					sources.TryGetValue(((Object)val).name, out var value2);
					int num = (int)item.Key;
					knownNames[num] = ((Object)val).name;
					streamWriter.WriteLine(num + "," + Csv(((Object)val).name) + "," + Csv(value2 ?? "unattributed") + "," + StableHash(((Object)val).name));
				}
			}
		}
		HashSet<string> hashSet = new HashSet<string>();
		using (StreamWriter streamWriter2 = Writer(text + "-assets.csv"))
		{
			streamWriter2.WriteLine("hash,name,source,calculated_hash");
			GameObject[] array = Resources.FindObjectsOfTypeAll<GameObject>();
			foreach (GameObject val2 in array)
			{
				if (!((Object)(object)val2 == (Object)null) && hashSet.Add(((Object)val2).name))
				{
					sources.TryGetValue(((Object)val2).name, out var value3);
					int num2 = StableHash(((Object)val2).name);
					if (!knownNames.ContainsKey(num2))
					{
						knownNames[num2] = ((Object)val2).name;
					}
					streamWriter2.WriteLine(num2 + "," + Csv(((Object)val2).name) + "," + Csv(value3 ?? "unattributed") + "," + num2);
				}
			}
		}
		DumpWorldHashes(text, dictionary);
		DumpEnvironment(text);
		WriteMissing();
		((BaseUnityPlugin)this).Logger.LogInfo((object)("Captured " + text + ": registry=" + dictionary.Count + ", loaded names=" + hashSet.Count + ", missing hashes=" + missing.Count));
	}

	private Dictionary<string, string> GetSources()
	{
		Dictionary<string, string> dictionary = new Dictionary<string, string>();
		Type type = AccessTools.TypeByName("Jotunn.Managers.PrefabManager");
		if (type == null)
		{
			return dictionary;
		}
		object value = AccessTools.Property(type, "Instance").GetValue(null, null);
		if (!(AccessTools.Property(type, "Prefabs").GetValue(value, null) is IDictionary dictionary2))
		{
			return dictionary;
		}
		foreach (DictionaryEntry item in dictionary2)
		{
			object value2 = item.Value;
			PropertyInfo propertyInfo = AccessTools.Property(value2.GetType(), "SourceMod");
			BepInPlugin val = (BepInPlugin)((propertyInfo == null) ? null : /*isinst with value type is only supported in some contexts*/);
			if (val != null)
			{
				dictionary[Convert.ToString(item.Key)] = val.GUID + " / " + val.Name + " " + val.Version;
			}
		}
		return dictionary;
	}

	private void DumpWorldHashes(string prefix, IDictionary registry)
	{
		object obj = Instance(managerType);
		if (obj == null || !(AccessTools.Field(managerType, "m_objectsByID").GetValue(obj) is IDictionary dictionary))
		{
			return;
		}
		Dictionary<int, int> dictionary2 = new Dictionary<int, int>();
		foreach (DictionaryEntry item in dictionary)
		{
			int key = (int)getHash.Invoke(item.Value, null);
			dictionary2.TryGetValue(key, out var value);
			dictionary2[key] = value + 1;
		}
		using StreamWriter streamWriter = Writer(prefix + "-world-hashes.csv");
		streamWriter.WriteLine("hash,zdo_count,registered,name");
		foreach (KeyValuePair<int, int> item2 in dictionary2)
		{
			object? obj2 = registry[item2.Key];
			GameObject val = (GameObject)((obj2 is GameObject) ? obj2 : null);
			streamWriter.WriteLine(item2.Key + "," + item2.Value + "," + ((Object)(object)val != (Object)null) + "," + Csv(((Object)(object)val == (Object)null) ? "" : ((Object)val).name));
		}
	}

	private void DumpEnvironment(string prefix)
	{
		using StreamWriter streamWriter = Writer(prefix + "-environment.txt");
		streamWriter.WriteLine("UTC=" + DateTime.UtcNow.ToString("o"));
		streamWriter.WriteLine(string.Concat("OS=", Environment.OSVersion, "; CLR=", Environment.Version, "; Unity=", Application.unityVersion));
		object obj = Instance(netType);
		if (obj != null)
		{
			streamWriter.WriteLine("IsServer=" + AccessTools.Method(netType, "IsServer", Type.EmptyTypes, (Type[])null).Invoke(obj, null));
		}
		Type type = sceneType.Assembly.GetType("Version");
		MethodInfo methodInfo = ((type == null) ? null : AccessTools.Method(type, "GetVersionString", new Type[1] { typeof(bool) }, (Type[])null));
		if (methodInfo != null)
		{
			streamWriter.WriteLine("GameVersion=" + methodInfo.Invoke(null, new object[1] { false }));
		}
		foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos)
		{
			PluginInfo value = pluginInfo.Value;
			streamWriter.WriteLine(string.Concat("Plugin=", pluginInfo.Key, " | ", value.Metadata.Name, " | ", value.Metadata.Version, " | ", value.Location, " | SHA256=", FileHash(value.Location)));
		}
		Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
		foreach (Assembly assembly in assemblies)
		{
			string name = assembly.GetName().Name;
			switch (name)
			{
			case "assembly_valheim":
			case "assembly_utils":
			case "Jotunn":
				streamWriter.WriteLine(string.Concat("Assembly=", name, " | MVID=", assembly.ManifestModule.ModuleVersionId, " | SHA256=", FileHash(assembly.Location)));
				break;
			}
		}
		Type type2 = AccessTools.TypeByName("StringExtensionMethods");
		MethodInfo methodInfo2 = AccessTools.Method(type2, "GetStableHashCode", new Type[1] { typeof(string) }, (Type[])null);
		string[] array = new string[5] { "Player", "_ZoneCtrl", "_TerrainCompiler", "wood_wall", "piece_workbench" };
		foreach (string text in array)
		{
			streamWriter.WriteLine(string.Concat("HashCheck=", text, " | game=", methodInfo2.Invoke(null, new object[1] { text }), " | independent=", StableHash(text)));
		}
		foreach (MethodBase allPatchedMethod in Harmony.GetAllPatchedMethods())
		{
			Patches patchInfo = Harmony.GetPatchInfo(allPatchedMethod);
			streamWriter.WriteLine("Patched=" + allPatchedMethod.DeclaringType.FullName + "." + allPatchedMethod.Name + " | " + string.Join(",", new List<string>(patchInfo.Owners).ToArray()));
		}
	}

	private void WriteMissing()
	{
		if (!diagnostics.Value || folder == null || missing.Count == 0)
		{
			return;
		}
		using StreamWriter streamWriter = Writer("scene-" + sceneNumber + "-missing.csv");
		streamWriter.WriteLine("hash,failed_create_attempts,candidate_name,zdo_samples_max8");
		foreach (KeyValuePair<int, Missing> item in missing)
		{
			knownNames.TryGetValue(item.Key, out var value);
			streamWriter.WriteLine(item.Key + "," + item.Value.Attempts + "," + Csv(value ?? "") + "," + Csv(string.Join(" | ", item.Value.Examples.ToArray())));
		}
	}

	private static int StableHash(string text)
	{
		int num = 5381;
		int num2 = num;
		for (int i = 0; i < text.Length && text[i] != 0; i += 2)
		{
			num = ((num << 5) + num) ^ text[i];
			if (i == text.Length - 1 || text[i + 1] == '\0')
			{
				break;
			}
			num2 = ((num2 << 5) + num2) ^ text[i + 1];
		}
		return num + num2 * 1566083941;
	}

	private StreamWriter Writer(string name)
	{
		return new StreamWriter(Path.Combine(folder, name), append: false, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
	}

	private static string Csv(string text)
	{
		return "\"" + text.Replace("\"", "\"\"") + "\"";
	}

	private static string FileHash(string path)
	{
		if (string.IsNullOrEmpty(path) || !File.Exists(path))
		{
			return "unavailable";
		}
		using SHA256 sHA = SHA256.Create();
		using FileStream inputStream = File.OpenRead(path);
		return BitConverter.ToString(sHA.ComputeHash(inputStream)).Replace("-", "").ToLowerInvariant();
	}

	private void ReportFailure(Exception e)
	{
		if (!diagnosticFailureReported)
		{
			diagnosticFailureReported = true;
			((BaseUnityPlugin)this).Logger.LogWarning((object)("Probe diagnostics error (reported once): " + e));
		}
	}

	private void OnDestroy()
	{
		try
		{
			WriteMissing();
		}
		catch (Exception e)
		{
			ReportFailure(e);
		}
		if (harmony != null)
		{
			harmony.UnpatchSelf();
		}
		if ((Object)(object)probe == (Object)(object)this)
		{
			probe = null;
		}
	}
}
internal sealed class WarningGate
{
	private readonly HashSet<string> seen = new HashSet<string>();

	private readonly int capacity;

	internal WarningGate(int capacity)
	{
		this.capacity = capacity;
	}

	internal bool Allow(object message, bool enabled)
	{
		string text = message as string;
		if (!enabled || text == null || !text.StartsWith("Missing prefab hash: ", StringComparison.Ordinal))
		{
			return true;
		}
		if (seen.Contains(text))
		{
			return false;
		}
		if (seen.Count < capacity)
		{
			seen.Add(text);
		}
		return true;
	}

	internal void Reset()
	{
		seen.Clear();
	}
}