Decompiled source of HavensMirror v1.0.0
HavensMirror.dll
Decompiled 3 days ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using HavensMirror.Config; using HavensMirror.Dialogue; using HavensMirror.Gallery; using HavensMirror.Hotkeys; using HavensMirror.Look; using Microsoft.CodeAnalysis; using SunhavenMods.Shared; using UnityEngine; using UnityEngine.Networking; using UnityEngine.SceneManagement; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyCompany("HavensMirror")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+f51c0a9c6c1e97b11c7ea02b854111ea865a0fb4")] [assembly: AssemblyProduct("HavensMirror")] [assembly: AssemblyTitle("HavensMirror")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace SunhavenMods.Shared { public static class ConfigFileHelper { public static ConfigFile CreateNamedConfig(string pluginGuid, string configFileName, Action<string> logWarning = null) { //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Expected O, but got Unknown string text = Path.Combine(Paths.ConfigPath, configFileName); string text2 = Path.Combine(Paths.ConfigPath, pluginGuid + ".cfg"); try { if (!File.Exists(text) && File.Exists(text2)) { File.Copy(text2, text); } } catch (Exception ex) { logWarning?.Invoke("[Config] Migration to " + configFileName + " failed: " + ex.Message); } return new ConfigFile(text, true); } public static bool ReplacePluginConfig(BaseUnityPlugin plugin, ConfigFile newConfig, Action<string> logWarning = null) { if ((Object)(object)plugin == (Object)null || newConfig == null) { return false; } try { Type typeFromHandle = typeof(BaseUnityPlugin); PropertyInfo property = typeFromHandle.GetProperty("Config", BindingFlags.Instance | BindingFlags.Public); if (property != null && property.CanWrite) { property.SetValue(plugin, newConfig, null); return true; } FieldInfo field = typeFromHandle.GetField("<Config>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic); if (field != null) { field.SetValue(plugin, newConfig); return true; } FieldInfo[] fields = typeFromHandle.GetFields(BindingFlags.Instance | BindingFlags.NonPublic); foreach (FieldInfo fieldInfo in fields) { if (fieldInfo.FieldType == typeof(ConfigFile)) { fieldInfo.SetValue(plugin, newConfig); return true; } } } catch (Exception ex) { logWarning?.Invoke("[Config] ReplacePluginConfig failed: " + ex.Message); } return false; } } public static class SharedCodeRevision { public const string Value = "2026.09.21"; } public sealed class ModHealthReport { public string PluginGuid { get; set; } public string DisplayName { get; set; } public string PluginVersion { get; set; } public string SharedCodeRevision { get; set; } public string IntegrationSummary { get; set; } public string Mode { get; set; } public string CharacterName { get; set; } public bool? DataLoaded { get; set; } public string LastPersistenceOutcome { get; set; } public string LastError { get; set; } public DateTime ReportedUtc { get; set; } public ModHealthReport Clone() { return new ModHealthReport { PluginGuid = PluginGuid, DisplayName = DisplayName, PluginVersion = PluginVersion, SharedCodeRevision = SharedCodeRevision, IntegrationSummary = IntegrationSummary, Mode = Mode, CharacterName = CharacterName, DataLoaded = DataLoaded, LastPersistenceOutcome = LastPersistenceOutcome, LastError = LastError, ReportedUtc = ReportedUtc }; } public void AppendDiagnosticDump(StringBuilder sb) { if (sb != null) { sb.AppendLine("guid: " + (PluginGuid ?? "—")); sb.AppendLine("name: " + (DisplayName ?? "—")); sb.AppendLine("version: " + (PluginVersion ?? "—")); sb.AppendLine("shared: " + (SharedCodeRevision ?? "—")); sb.AppendLine("integrations: " + (IntegrationSummary ?? "—")); sb.AppendLine("mode: " + (Mode ?? "—")); sb.AppendLine("character: " + (string.IsNullOrEmpty(CharacterName) ? "—" : CharacterName)); sb.AppendLine("data loaded: " + (DataLoaded.HasValue ? DataLoaded.Value.ToString() : "—")); sb.AppendLine("last save: " + (LastPersistenceOutcome ?? "—")); sb.AppendLine("last error: " + (LastError ?? "—")); sb.AppendLine("reported: " + ((ReportedUtc == default(DateTime)) ? "—" : ReportedUtc.ToLocalTime().ToString("u"))); } } } public static class ModDiagnostics { private static readonly Dictionary<string, ModHealthReport> ReportsByPluginGuid = new Dictionary<string, ModHealthReport>(StringComparer.OrdinalIgnoreCase); private static readonly object Lock = new object(); public static void ReportStartup(ModHealthReport report) { if (report == null || string.IsNullOrWhiteSpace(report.PluginGuid)) { return; } report.ReportedUtc = DateTime.UtcNow; if (string.IsNullOrEmpty(report.SharedCodeRevision)) { report.SharedCodeRevision = "2026.09.21"; } lock (Lock) { ReportsByPluginGuid[report.PluginGuid] = report.Clone(); } } public static void ReportRuntime(string pluginGuid, Action<ModHealthReport> update) { if (string.IsNullOrWhiteSpace(pluginGuid) || update == null) { return; } lock (Lock) { if (!ReportsByPluginGuid.TryGetValue(pluginGuid, out ModHealthReport value)) { value = new ModHealthReport { PluginGuid = pluginGuid }; ReportsByPluginGuid[pluginGuid] = value; } update(value); value.ReportedUtc = DateTime.UtcNow; } } public static ModHealthReport GetReport(string pluginGuid) { if (string.IsNullOrWhiteSpace(pluginGuid)) { return null; } lock (Lock) { ModHealthReport value; return ReportsByPluginGuid.TryGetValue(pluginGuid, out value) ? value.Clone() : null; } } public static IReadOnlyList<ModHealthReport> GetAllReports() { lock (Lock) { return ReportsByPluginGuid.Values.Select((ModHealthReport r) => r.Clone()).ToList(); } } public static string FormatHealthLogLine(ModHealthReport report) { if (report == null) { return "[Health] (empty report)"; } string text = (string.IsNullOrEmpty(report.DisplayName) ? report.PluginGuid : report.DisplayName); string text2 = (string.IsNullOrEmpty(report.PluginVersion) ? "?" : report.PluginVersion); string text3 = (string.IsNullOrEmpty(report.SharedCodeRevision) ? "?" : report.SharedCodeRevision); string text4 = (string.IsNullOrEmpty(report.IntegrationSummary) ? "—" : report.IntegrationSummary); string text5 = (string.IsNullOrEmpty(report.Mode) ? "—" : report.Mode); string text6 = (string.IsNullOrEmpty(report.CharacterName) ? "—" : report.CharacterName); string text7 = ((!report.DataLoaded.HasValue) ? "—" : (report.DataLoaded.Value ? "loaded" : "none")); string text8 = (string.IsNullOrEmpty(report.LastPersistenceOutcome) ? "—" : report.LastPersistenceOutcome); return "[Health] " + text + " v" + text2 + " | shared " + text3 + " | integrations: " + text4 + " | mode: " + text5 + " | character: " + text6 + " | data: " + text7 + " | save: " + text8; } public static void LogStartupHealth(ManualLogSource log, ModHealthReport report) { ReportStartup(report); if (log != null) { log.LogInfo((object)FormatHealthLogLine(report)); } } public static void LogModStartup(ManualLogSource log, string pluginGuid, string displayName, string pluginVersion, string integrationSummary, string mode = "startup", bool? dataLoaded = false, string lastPersistenceOutcome = "—") { LogStartupHealth(log, new ModHealthReport { PluginGuid = pluginGuid, DisplayName = displayName, PluginVersion = pluginVersion, SharedCodeRevision = "2026.09.21", IntegrationSummary = integrationSummary, Mode = mode, DataLoaded = dataLoaded, LastPersistenceOutcome = lastPersistenceOutcome }); } } public static class SuitePluginGuids { public const string DevTools = "com.azraelgodking.havendevtools"; public const string SenpaisChest = "com.azraelgodking.senpaischest"; public const string BirthdayReminder = "com.azraelgodking.squirrelsbirthdayreminder"; public const string HavensBirthright = "com.azraelgodking.havensbirthright"; public const string Smut = "com.azraelgodking.sunhavenmuseumutilitytracker"; public const string SunhavenTodo = "com.azraelgodking.sunhaventodo"; public const string TheVault = "com.azraelgodking.thevault"; public const string HavensAlmanac = "com.azraelgodking.havensalmanac"; public const string FasterRaces = "com.azraelgodking.fasterraces"; public const string TrinketFortune = "com.azraelgodking.trinketfortune"; public const string CropOptimizer = "com.azraelgodking.cropoptimizer"; public const string HavensRespec = "com.azraelgodking.havensrespec"; public const string GiftingAssistant = "com.azraelgodking.giftingassistant"; public const string HavensMirror = "com.azraelgodking.havensmirror"; } public static class ModHealthIntegrationSummary { public static string Build(params (string label, string pluginGuid)[] integrations) { if (integrations == null || integrations.Length == 0) { return "standalone"; } Dictionary<string, PluginInfo> pluginInfos = Chainloader.PluginInfos; if (pluginInfos == null) { return "standalone"; } List<string> list = new List<string>(integrations.Length); for (int i = 0; i < integrations.Length; i++) { var (text, text2) = integrations[i]; if (!string.IsNullOrEmpty(text) && !string.IsNullOrEmpty(text2) && pluginInfos.ContainsKey(text2)) { list.Add(text); } } if (list.Count != 0) { return string.Join(", ", list); } return "standalone"; } } public static class ReflectionHelper { public static readonly BindingFlags AllBindingFlags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy; public static Type FindType(string typeName, params string[] namespaces) { Assembly[] assemblies; if (namespaces != null && namespaces.Length != 0) { string[] array = namespaces; for (int i = 0; i < array.Length; i++) { Type type = AccessTools.TypeByName(array[i] + "." + typeName); if (type != null) { return type; } } assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { array = namespaces; foreach (string text in array) { try { Type type2 = assembly.GetType(text + "." + typeName, throwOnError: false); if (type2 != null) { return type2; } } catch { } } } assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly2 in assemblies) { try { Type type3 = assembly2.GetTypes().FirstOrDefault((Type t) => namespaces.Any((string ns) => string.Equals(t.FullName, ns + "." + typeName, StringComparison.Ordinal) || (t.Name == typeName && string.Equals(t.Namespace, ns, StringComparison.Ordinal)))); if (type3 != null) { return type3; } } catch (ReflectionTypeLoadException ex) { Type type4 = ex.Types?.FirstOrDefault((Type t) => t != null && namespaces.Any((string ns) => string.Equals(t.FullName, ns + "." + typeName, StringComparison.Ordinal) || (t.Name == typeName && string.Equals(t.Namespace, ns, StringComparison.Ordinal)))); if (type4 != null) { return type4; } } } return null; } Type type5 = AccessTools.TypeByName(typeName); if (type5 != null) { return type5; } assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly3 in assemblies) { try { type5 = assembly3.GetTypes().FirstOrDefault((Type t) => t.Name == typeName || t.FullName == typeName); if (type5 != null) { return type5; } } catch (ReflectionTypeLoadException) { } } return null; } public static Type FindModPlugin(string assemblyName) { if (string.IsNullOrWhiteSpace(assemblyName)) { return null; } Type type = FindType("Plugin", assemblyName); if (type != null && string.Equals(type.Assembly.GetName().Name, assemblyName, StringComparison.OrdinalIgnoreCase)) { return type; } Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { if (!string.Equals(assembly.GetName().Name, assemblyName, StringComparison.OrdinalIgnoreCase)) { continue; } Type type2 = assembly.GetType(assemblyName + ".Plugin", throwOnError: false); if (type2 != null) { return type2; } try { Type type3 = assembly.GetTypes().FirstOrDefault((Type t) => t.IsClass && !t.IsAbstract && t.Name == "Plugin"); if (type3 != null) { return type3; } } catch (ReflectionTypeLoadException ex) { Type type4 = ex.Types?.FirstOrDefault((Type t) => t != null && t.IsClass && !t.IsAbstract && t.Name == "Plugin"); if (type4 != null) { return type4; } } } return null; } public static MethodInfo GetStaticMethod(Type type, string methodName) { return type?.GetMethod(methodName, AllBindingFlags); } public static Type FindWishType(string typeName) { return FindType(typeName, "Wish"); } public static object GetStaticValue(Type type, string memberName) { if (type == null) { return null; } try { PropertyInfo property = type.GetProperty(memberName, AllBindingFlags); if (property != null && property.GetMethod != null && property.GetIndexParameters().Length == 0) { return property.GetValue(null); } } catch (AmbiguousMatchException) { return null; } FieldInfo field = type.GetField(memberName, AllBindingFlags); if (field != null) { return field.GetValue(null); } return null; } public static object GetSingletonInstance(Type type) { if (type == null) { return null; } string[] array = new string[5] { "Instance", "instance", "_instance", "Singleton", "singleton" }; foreach (string memberName in array) { object staticValue = GetStaticValue(type, memberName); if (staticValue != null) { return staticValue; } } return null; } public static object GetInstanceValue(object instance, string memberName) { if (instance == null) { return null; } Type type = instance.GetType(); while (type != null) { PropertyInfo property = type.GetProperty(memberName, AllBindingFlags); if (property != null && property.GetMethod != null) { return property.GetValue(instance); } FieldInfo field = type.GetField(memberName, AllBindingFlags); if (field != null) { return field.GetValue(instance); } type = type.BaseType; } return null; } public static bool SetInstanceValue(object instance, string memberName, object value) { if (instance == null) { return false; } Type type = instance.GetType(); while (type != null) { PropertyInfo property = type.GetProperty(memberName, AllBindingFlags); if (property != null && property.SetMethod != null) { property.SetValue(instance, value); return true; } FieldInfo field = type.GetField(memberName, AllBindingFlags); if (field != null) { field.SetValue(instance, value); return true; } type = type.BaseType; } return false; } public static object InvokeMethod(object instance, string methodName, params object[] args) { if (instance == null) { return null; } Type type = instance.GetType(); Type[] array = args?.Select((object a) => a?.GetType() ?? typeof(object)).ToArray() ?? Type.EmptyTypes; MethodInfo methodInfo = AccessTools.Method(type, methodName, array, (Type[])null); if (methodInfo == null) { methodInfo = type.GetMethod(methodName, AllBindingFlags); } if (methodInfo == null) { return null; } return methodInfo.Invoke(instance, args); } public static object InvokeStaticMethod(Type type, string methodName, params object[] args) { if (type == null) { return null; } Type[] array = args?.Select((object a) => a?.GetType() ?? typeof(object)).ToArray() ?? Type.EmptyTypes; MethodInfo methodInfo = AccessTools.Method(type, methodName, array, (Type[])null); if (methodInfo == null) { methodInfo = type.GetMethod(methodName, AllBindingFlags); } if (methodInfo == null) { return null; } return methodInfo.Invoke(null, args); } public static FieldInfo[] GetAllFields(Type type) { if (type == null) { return Array.Empty<FieldInfo>(); } FieldInfo[] fields = type.GetFields(AllBindingFlags); IEnumerable<FieldInfo> second; if (!(type.BaseType != null) || !(type.BaseType != typeof(object))) { second = Enumerable.Empty<FieldInfo>(); } else { IEnumerable<FieldInfo> allFields = GetAllFields(type.BaseType); second = allFields; } return fields.Concat(second).Distinct().ToArray(); } public static PropertyInfo[] GetAllProperties(Type type) { if (type == null) { return Array.Empty<PropertyInfo>(); } PropertyInfo[] properties = type.GetProperties(AllBindingFlags); IEnumerable<PropertyInfo> second; if (!(type.BaseType != null) || !(type.BaseType != typeof(object))) { second = Enumerable.Empty<PropertyInfo>(); } else { IEnumerable<PropertyInfo> allProperties = GetAllProperties(type.BaseType); second = allProperties; } return (from p in properties.Concat(second) group p by p.Name into g select g.First()).ToArray(); } public static T TryGetValue<T>(object instance, string memberName, T defaultValue = default(T)) { try { object instanceValue = GetInstanceValue(instance, memberName); if (instanceValue is T result) { return result; } if (instanceValue != null && typeof(T).IsAssignableFrom(instanceValue.GetType())) { return (T)instanceValue; } return defaultValue; } catch { return defaultValue; } } } public enum CharacterSaveSource { None, Primary, Backup } public enum CharacterSaveAbsenceReason { NoFiles, FilesPresentButUnusable } public static class CharacterSaveStore { public const string BackupSuffix = ".bak"; public const string VaultBackupSuffix = ".backup"; public const string TempSuffix = ".tmp"; private static readonly char[] InvalidFileNameChars = BuildInvalidFileNameChars(); private static char[] BuildInvalidFileNameChars() { char[] array = new char[41]; for (int i = 0; i < 32; i++) { array[i] = (char)i; } "\"<>|:*?\\/".CopyTo(0, array, 32, 9); return array; } public static string SanitizeFileName(string name, string fallback = "unknown") { if (name == null) { return fallback; } char[] invalidFileNameChars = InvalidFileNameChars; foreach (char oldChar in invalidFileNameChars) { name = name.Replace(oldChar, '_'); } name = name.Trim(); if (!string.IsNullOrEmpty(name)) { return name; } return fallback; } public static string GetFilePath(string directory, string characterName, string fileSuffix, string fallback = "unknown") { EnsureDirectory(directory); return Path.Combine(directory, SanitizeFileName(characterName, fallback) + fileSuffix); } public static void EnsureDirectory(string directory) { if (string.IsNullOrEmpty(directory)) { throw new ArgumentException("Directory is required.", "directory"); } if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } } public static CharacterSaveAbsenceReason GetAbsenceReason(string filePath, string backupSuffix = ".bak") { if (string.IsNullOrEmpty(filePath)) { return CharacterSaveAbsenceReason.NoFiles; } bool num = File.Exists(filePath); bool flag = File.Exists(filePath + backupSuffix); if (!(num || flag)) { return CharacterSaveAbsenceReason.NoFiles; } return CharacterSaveAbsenceReason.FilesPresentButUnusable; } public static bool WriteAtomic(string filePath, string content, string backupSuffix = ".bak", bool deleteTempInFinally = true) { if (content == null) { throw new ArgumentNullException("content"); } return WriteAtomicCore(filePath, delegate(string tempPath) { WriteAllTextDurable(tempPath, content); }, backupSuffix, deleteTempInFinally); } public static bool WriteAtomicBytes(string filePath, byte[] content, string backupSuffix = ".bak", bool deleteTempInFinally = true) { if (content == null) { throw new ArgumentNullException("content"); } return WriteAtomicCore(filePath, delegate(string tempPath) { WriteAllBytesDurable(tempPath, content); }, backupSuffix, deleteTempInFinally); } public static T LoadWithBackup<T>(string filePath, Func<string, T> tryDeserialize, out CharacterSaveSource source, string backupSuffix = ".bak", Action<string, Exception> onReadFailure = null) where T : class { source = CharacterSaveSource.None; if (string.IsNullOrEmpty(filePath) || tryDeserialize == null) { return null; } if (File.Exists(filePath)) { string text = TryReadAllText(filePath, onReadFailure); if (text != null) { try { T val = tryDeserialize(text); if (val != null) { source = CharacterSaveSource.Primary; return val; } } catch (Exception arg) { onReadFailure?.Invoke(filePath, arg); } } } string text2 = filePath + backupSuffix; if (File.Exists(text2)) { string text3 = TryReadAllText(text2, onReadFailure); if (text3 != null) { try { T val2 = tryDeserialize(text3); if (val2 != null) { source = CharacterSaveSource.Backup; return val2; } } catch (Exception arg2) { onReadFailure?.Invoke(text2, arg2); } } } return null; } public static string LoadTextWithBackup(string filePath, out CharacterSaveSource source, string backupSuffix = ".bak", Action<string, Exception> onReadFailure = null) { source = CharacterSaveSource.None; if (string.IsNullOrEmpty(filePath)) { return null; } if (File.Exists(filePath)) { string text = TryReadAllText(filePath, onReadFailure); if (text != null) { source = CharacterSaveSource.Primary; return text; } } string path = filePath + backupSuffix; if (File.Exists(path)) { string text2 = TryReadAllText(path, onReadFailure); if (text2 != null) { source = CharacterSaveSource.Backup; return text2; } } return null; } public static bool LooksLikeJsonObject(string text) { if (!string.IsNullOrWhiteSpace(text)) { return text.TrimStart(Array.Empty<char>()).StartsWith("{", StringComparison.Ordinal); } return false; } private static bool WriteAtomicCore(string filePath, Action<string> writeTemp, string backupSuffix, bool deleteTempInFinally) { if (string.IsNullOrEmpty(filePath) || writeTemp == null) { return false; } string text = filePath + ".tmp"; string text2 = filePath + backupSuffix; bool flag = false; try { writeTemp(text); if (File.Exists(filePath)) { if (File.Exists(text2)) { File.Delete(text2); } File.Move(filePath, text2); flag = true; } File.Move(text, filePath); TryDelete(text); return true; } catch (Exception) { if (flag && !File.Exists(filePath) && File.Exists(text2)) { try { File.Move(text2, filePath); } catch { } } if (deleteTempInFinally && !flag) { TryDelete(text); } return false; } } private static void WriteAllTextDurable(string path, string content) { using FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); using StreamWriter streamWriter = new StreamWriter(fileStream); streamWriter.Write(content); streamWriter.Flush(); fileStream.Flush(flushToDisk: true); } private static void WriteAllBytesDurable(string path, byte[] content) { using FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); fileStream.Write(content, 0, content.Length); fileStream.Flush(flushToDisk: true); } private static string TryReadAllText(string path, Action<string, Exception> onReadFailure) { try { if (!File.Exists(path)) { return null; } return File.ReadAllText(path); } catch (Exception arg) { onReadFailure?.Invoke(path, arg); return null; } } private static void TryDelete(string path) { try { if (File.Exists(path)) { File.Delete(path); } } catch { } } } public static class GameSaveCharacterName { public static string TryGetCurrent(string fallback = null, Action<string> logWarning = null) { try { Type type = AccessTools.TypeByName("Wish.GameSave"); if (type == null) { return fallback; } object obj = AccessTools.Property(type, "CurrentCharacter")?.GetValue(null); if (obj != null) { string text = ReadCharacterName(obj); if (!string.IsNullOrEmpty(text)) { return text; } } object obj2 = AccessTools.Property(type, "Instance")?.GetValue(null); if (obj2 != null) { object obj3 = AccessTools.Property(type, "CurrentSave")?.GetValue(obj2); if (obj3 != null) { object obj4 = AccessTools.Property(obj3.GetType(), "characterData")?.GetValue(obj3); if (obj4 != null) { string text2 = ReadCharacterName(obj4); if (!string.IsNullOrEmpty(text2)) { return text2; } } } } } catch (Exception ex) { logWarning?.Invoke(ex.Message); } return fallback; } private static string ReadCharacterName(object characterOrData) { return AccessTools.Property(characterOrData.GetType(), "characterName")?.GetValue(characterOrData) as string; } } public static class SceneRootSurvivor { private static readonly object Lock = new object(); private static readonly List<string> NoKillSubstrings = new List<string>(); private static Harmony _harmony; public static void TryRegisterPersistentRunnerGameObject(GameObject go) { if (!((Object)(object)go == (Object)null)) { TryAddNoKillListSubstring(((Object)go).name); } } public static void TryAddNoKillListSubstring(string nameSubstring) { if (string.IsNullOrEmpty(nameSubstring)) { return; } lock (Lock) { bool flag = false; for (int i = 0; i < NoKillSubstrings.Count; i++) { if (string.Equals(NoKillSubstrings[i], nameSubstring, StringComparison.OrdinalIgnoreCase)) { flag = true; break; } } if (!flag) { NoKillSubstrings.Add(nameSubstring); } } EnsurePatched(); } private static void EnsurePatched() { //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Expected O, but got Unknown //IL_00a3: Expected O, but got Unknown if (_harmony != null) { return; } lock (Lock) { if (_harmony == null) { MethodInfo methodInfo = AccessTools.Method(typeof(Scene), "GetRootGameObjects", Type.EmptyTypes, (Type[])null); if (!(methodInfo == null)) { string text = typeof(SceneRootSurvivor).Assembly.GetName().Name ?? "Unknown"; Harmony val = new Harmony("SunhavenMods.SceneRootSurvivor." + text); val.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(SceneRootSurvivor), "OnGetRootGameObjectsPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _harmony = val; } } } } private static void OnGetRootGameObjectsPostfix(ref GameObject[] __result) { if (__result == null || __result.Length == 0) { return; } List<string> list; lock (Lock) { if (NoKillSubstrings.Count == 0) { return; } list = new List<string>(NoKillSubstrings); } List<GameObject> list2 = new List<GameObject>(__result); for (int i = 0; i < list.Count; i++) { string noKill = list[i]; list2.RemoveAll((GameObject a) => (Object)(object)a != (Object)null && ((Object)a).name.IndexOf(noKill, StringComparison.OrdinalIgnoreCase) >= 0); } __result = list2.ToArray(); } } public abstract class PersistentRunnerBase : MonoBehaviour { private bool _wasInGame; private float _lastHeartbeat; private string _lastSceneName = ""; private float _lastSceneCheckTime; private const float SceneCheckInterval = 0.5f; protected virtual float HeartbeatInterval => 0f; protected virtual string RunnerName => ((object)this).GetType().Name; protected virtual void OnUpdate() { } protected virtual void OnMenuTransition() { } protected virtual void OnGameTransition() { } protected virtual void Log(string message) { } protected virtual void LogWarning(string message) { } public static T CreateRunner<T>() where T : PersistentRunnerBase { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Expected O, but got Unknown //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Expected O, but got Unknown GameObject val = new GameObject("[" + typeof(T).Name + "]") { hideFlags = (HideFlags)61 }; Object.DontDestroyOnLoad((Object)val); SceneRootSurvivor.TryRegisterPersistentRunnerGameObject(val); return val.AddComponent<T>(); } protected virtual void Awake() { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) Scene activeScene = SceneManager.GetActiveScene(); _lastSceneName = ((Scene)(ref activeScene)).name; _wasInGame = !SceneHelpers.IsMenuScene(_lastSceneName); Log("[" + RunnerName + "] Initialized in scene: " + _lastSceneName); } protected virtual void Update() { //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) if (HeartbeatInterval > 0f) { _lastHeartbeat += Time.deltaTime; if (_lastHeartbeat >= HeartbeatInterval) { _lastHeartbeat = 0f; Log("[" + RunnerName + "] Heartbeat - still alive"); } } float unscaledTime = Time.unscaledTime; if (unscaledTime - _lastSceneCheckTime >= 0.5f) { _lastSceneCheckTime = unscaledTime; Scene activeScene = SceneManager.GetActiveScene(); string name = ((Scene)(ref activeScene)).name; if (name != _lastSceneName) { _lastSceneName = name; HandleSceneChange(name); } } try { OnUpdate(); } catch (Exception ex) { LogWarning("[" + RunnerName + "] Error in OnUpdate: " + ex.Message); } } private void HandleSceneChange(string sceneName) { bool flag = SceneHelpers.IsMenuScene(sceneName); if (_wasInGame && flag) { Log("[" + RunnerName + "] Menu transition detected"); try { OnMenuTransition(); } catch (Exception ex) { LogWarning("[" + RunnerName + "] Error in OnMenuTransition: " + ex.Message); } } else if (!_wasInGame && !flag) { Log("[" + RunnerName + "] Game transition detected"); try { OnGameTransition(); } catch (Exception ex2) { LogWarning("[" + RunnerName + "] Error in OnGameTransition: " + ex2.Message); } } _wasInGame = !flag; } protected virtual void OnDestroy() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) Scene activeScene = SceneManager.GetActiveScene(); string text = (((Scene)(ref activeScene)).name ?? string.Empty).ToLowerInvariant(); if (!Application.isPlaying || text.Contains("menu") || text.Contains("title")) { Log("[" + RunnerName + "] OnDestroy during app quit/menu unload (expected)."); } else { LogWarning("[" + RunnerName + "] OnDestroy outside quit/menu (unexpected)."); } } } public static class SceneHelpers { private static readonly string[] MenuScenePatterns = new string[3] { "menu", "title", "bootstrap" }; private static readonly string[] ExactMenuScenes = new string[2] { "MainMenu", "Bootstrap" }; public static bool IsMenuScene(string sceneName) { if (string.IsNullOrEmpty(sceneName)) { return true; } string[] exactMenuScenes = ExactMenuScenes; foreach (string text in exactMenuScenes) { if (sceneName == text) { return true; } } string text2 = sceneName.ToLowerInvariant(); exactMenuScenes = MenuScenePatterns; foreach (string value in exactMenuScenes) { if (text2.Contains(value)) { return true; } } return false; } public static bool IsCurrentSceneMenu() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) Scene activeScene = SceneManager.GetActiveScene(); return IsMenuScene(((Scene)(ref activeScene)).name); } public static bool IsInGame() { return !IsCurrentSceneMenu(); } public static string GetCurrentSceneName() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) Scene activeScene = SceneManager.GetActiveScene(); return ((Scene)(ref activeScene)).name; } public static bool IsMainMenu() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) Scene activeScene = SceneManager.GetActiveScene(); return ((Scene)(ref activeScene)).name == "MainMenu"; } } public static class VersionChecker { public class VersionCheckResult { public bool Success { get; set; } public bool UpdateAvailable { get; set; } public string CurrentVersion { get; set; } public string LatestVersion { get; set; } public string ModName { get; set; } public string NexusUrl { get; set; } public string Changelog { get; set; } public string ErrorMessage { get; set; } } public class ModHealthSnapshot { public string PluginGuid { get; set; } public DateTime LastCheckUtc { get; set; } public int ExceptionCount { get; set; } public string LastError { get; set; } } private class VersionCheckRunner : MonoBehaviour { private ManualLogSource _pluginLog; public void StartCheck(string pluginGuid, string currentVersion, ManualLogSource pluginLog, Action<VersionCheckResult> onComplete) { _pluginLog = pluginLog; ((MonoBehaviour)this).StartCoroutine(CheckVersionCoroutine(pluginGuid, currentVersion, onComplete)); } private void LogInfo(string message) { ManualLogSource pluginLog = _pluginLog; if (pluginLog != null) { pluginLog.LogInfo((object)("[VersionChecker] " + message)); } } private void LogWarningMsg(string message) { ManualLogSource pluginLog = _pluginLog; if (pluginLog != null) { pluginLog.LogWarning((object)("[VersionChecker] " + message)); } } private void LogErrorMsg(string message) { ManualLogSource pluginLog = _pluginLog; if (pluginLog != null) { pluginLog.LogError((object)("[VersionChecker] " + message)); } } private IEnumerator CheckVersionCoroutine(string pluginGuid, string currentVersion, Action<VersionCheckResult> onComplete) { VersionCheckResult result = new VersionCheckResult { CurrentVersion = currentVersion }; UnityWebRequest www = UnityWebRequest.Get("https://azraelgodking.github.io/SunhavenMod/versions.json"); try { www.timeout = 10; yield return www.SendWebRequest(); if ((int)www.result == 2 || (int)www.result == 3) { result.Success = false; result.ErrorMessage = "Network error: " + www.error; RecordHealthError(pluginGuid, result.ErrorMessage); LogWarningMsg(result.ErrorMessage); onComplete?.Invoke(result); Object.Destroy((Object)(object)((Component)this).gameObject); yield break; } try { string text = www.downloadHandler.text; Match match = GetModPattern(pluginGuid).Match(text); if (!match.Success) { result.Success = false; result.ErrorMessage = "Mod '" + pluginGuid + "' not found in versions.json"; RecordHealthError(pluginGuid, result.ErrorMessage); LogWarningMsg(result.ErrorMessage); onComplete?.Invoke(result); Object.Destroy((Object)(object)((Component)this).gameObject); yield break; } string value = match.Groups[1].Value; result.LatestVersion = ExtractJsonString(value, "version"); result.ModName = ExtractJsonString(value, "name"); result.NexusUrl = ExtractJsonString(value, "nexus"); result.Changelog = ExtractJsonString(value, "changelog"); if (string.IsNullOrEmpty(result.LatestVersion)) { result.Success = false; result.ErrorMessage = "Could not parse version from response"; RecordHealthError(pluginGuid, result.ErrorMessage); LogWarningMsg(result.ErrorMessage); onComplete?.Invoke(result); Object.Destroy((Object)(object)((Component)this).gameObject); yield break; } result.Success = true; result.UpdateAvailable = CompareVersions(currentVersion, result.LatestVersion) < 0; if (result.UpdateAvailable) { LogInfo("Update available for " + result.ModName + ": " + currentVersion + " -> " + result.LatestVersion); } else { LogInfo(result.ModName + " is up to date (v" + currentVersion + ")"); } } catch (Exception ex) { result.Success = false; result.ErrorMessage = "Parse error: " + ex.Message; RecordHealthError(pluginGuid, result.ErrorMessage); LogErrorMsg(result.ErrorMessage); } } finally { ((IDisposable)www)?.Dispose(); } onComplete?.Invoke(result); Object.Destroy((Object)(object)((Component)this).gameObject); } private string ExtractJsonString(string json, string key) { Match match = ExtractFieldRegex.Match(json); while (match.Success) { if (string.Equals(match.Groups["key"].Value, key, StringComparison.Ordinal)) { return match.Groups["value"].Value; } match = match.NextMatch(); } return null; } } private const string VersionsUrl = "https://azraelgodking.github.io/SunhavenMod/versions.json"; private static readonly Dictionary<string, ModHealthSnapshot> HealthByPluginGuid = new Dictionary<string, ModHealthSnapshot>(StringComparer.OrdinalIgnoreCase); private static readonly object HealthLock = new object(); private static readonly Dictionary<string, Regex> ModPatternCache = new Dictionary<string, Regex>(StringComparer.Ordinal); private static readonly object ModPatternCacheLock = new object(); private static readonly Regex ExtractFieldRegex = new Regex("\"(?<key>[^\"]+)\"\\s*:\\s*(?:\"(?<value>[^\"]*)\"|null)", RegexOptions.Compiled); public static void CheckForUpdate(string pluginGuid, string currentVersion, ManualLogSource logger = null, Action<VersionCheckResult> onComplete = null) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) TouchHealth(pluginGuid); VersionCheckRunner versionCheckRunner = new GameObject("VersionChecker").AddComponent<VersionCheckRunner>(); Object.DontDestroyOnLoad((Object)(object)((Component)versionCheckRunner).gameObject); SceneRootSurvivor.TryRegisterPersistentRunnerGameObject(((Component)versionCheckRunner).gameObject); versionCheckRunner.StartCheck(pluginGuid, currentVersion, logger, onComplete); } public static ModHealthSnapshot GetHealthSnapshot(string pluginGuid) { if (string.IsNullOrWhiteSpace(pluginGuid)) { return null; } lock (HealthLock) { if (!HealthByPluginGuid.TryGetValue(pluginGuid, out ModHealthSnapshot value)) { return null; } return new ModHealthSnapshot { PluginGuid = value.PluginGuid, LastCheckUtc = value.LastCheckUtc, ExceptionCount = value.ExceptionCount, LastError = value.LastError }; } } public static int CompareVersions(string v1, string v2) { if (string.IsNullOrEmpty(v1) || string.IsNullOrEmpty(v2)) { return 0; } v1 = v1.TrimStart('v', 'V'); v2 = v2.TrimStart('v', 'V'); int num = v1.IndexOfAny(new char[2] { '-', '+' }); if (num >= 0) { v1 = v1.Substring(0, num); } int num2 = v2.IndexOfAny(new char[2] { '-', '+' }); if (num2 >= 0) { v2 = v2.Substring(0, num2); } string[] array = v1.Split(new char[1] { '.' }); string[] array2 = v2.Split(new char[1] { '.' }); int num3 = Math.Max(array.Length, array2.Length); for (int i = 0; i < num3; i++) { int result; int num4 = ((i < array.Length && int.TryParse(array[i], out result)) ? result : 0); int result2; int num5 = ((i < array2.Length && int.TryParse(array2[i], out result2)) ? result2 : 0); if (num4 < num5) { return -1; } if (num4 > num5) { return 1; } } return 0; } private static void TouchHealth(string pluginGuid) { if (string.IsNullOrWhiteSpace(pluginGuid)) { return; } lock (HealthLock) { if (!HealthByPluginGuid.TryGetValue(pluginGuid, out ModHealthSnapshot value)) { value = new ModHealthSnapshot { PluginGuid = pluginGuid }; HealthByPluginGuid[pluginGuid] = value; } value.LastCheckUtc = DateTime.UtcNow; } } private static void RecordHealthError(string pluginGuid, string errorMessage) { if (string.IsNullOrWhiteSpace(pluginGuid)) { return; } lock (HealthLock) { if (!HealthByPluginGuid.TryGetValue(pluginGuid, out ModHealthSnapshot value)) { value = new ModHealthSnapshot { PluginGuid = pluginGuid }; HealthByPluginGuid[pluginGuid] = value; } value.LastCheckUtc = DateTime.UtcNow; value.ExceptionCount++; value.LastError = errorMessage; } } private static Regex GetModPattern(string pluginGuid) { lock (ModPatternCacheLock) { if (!ModPatternCache.TryGetValue(pluginGuid, out Regex value)) { value = new Regex("\"" + Regex.Escape(pluginGuid) + "\"\\s*:\\s*\\{([^}]+)\\}", RegexOptions.Compiled | RegexOptions.Singleline); ModPatternCache[pluginGuid] = value; } return value; } } } public static class VersionCheckerExtensions { public static void NotifyUpdateAvailable(this VersionChecker.VersionCheckResult result, ManualLogSource logger = null) { if (!result.UpdateAvailable) { return; } string text = result.ModName + " update available: v" + result.LatestVersion; try { Type type = ReflectionHelper.FindWishType("NotificationStack"); if (type != null) { Type type2 = ReflectionHelper.FindType("SingletonBehaviour`1", "Wish"); if (type2 != null) { object obj = type2.MakeGenericType(type).GetProperty("Instance")?.GetValue(null); if (obj != null) { MethodInfo method = type.GetMethod("SendNotification", new Type[5] { typeof(string), typeof(int), typeof(int), typeof(bool), typeof(bool) }); if (method != null) { method.Invoke(obj, new object[5] { text, 0, 1, false, true }); return; } } } } } catch (Exception ex) { if (logger != null) { logger.LogWarning((object)("Failed to send native notification: " + ex.Message)); } } if (logger != null) { logger.LogWarning((object)("[UPDATE AVAILABLE] " + text)); } if (!string.IsNullOrEmpty(result.NexusUrl) && logger != null) { logger.LogWarning((object)("Download at: " + result.NexusUrl)); } } } } namespace HavensMirror { [BepInPlugin("com.azraelgodking.havensmirror", "Haven's Mirror", "1.0.0")] public class Plugin : BaseUnityPlugin { private static Harmony _harmony; private static MirrorPersistentRunner _runner; private bool _applicationQuitting; public static ManualLogSource Log { get; private set; } public static SpriteShelf Shelf { get; private set; } private void Awake() { //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; ConfigFile val = CreateNamedConfig(); ConfigFileHelper.ReplacePluginConfig((BaseUnityPlugin)(object)this, val, (Action<string>)Log.LogWarning); MirrorOptions.Bind(val); Shelf = new SpriteShelf(); if (MirrorOptions.Enabled == null || !MirrorOptions.Enabled.Value) { Log.LogInfo((object)"Haven's Mirror disabled in config."); ModDiagnostics.LogModStartup(Log, "com.azraelgodking.havensmirror", "Haven's Mirror", "1.0.0", "standalone", "disabled", false); return; } GalleryPaths.EnsureStarterLayout(delegate(string msg) { Log.LogInfo((object)msg); }); int num = SaveSlotFolderSync.EnsureCharacterFolders(); if (num > 0) { Log.LogInfo((object)$"[Gallery] Created {num} character folder(s) from saves."); } _harmony = new Harmony("com.azraelgodking.havensmirror"); try { DialogueMirrorBinder.Apply(_harmony); NpcLookPatches.Apply(_harmony); } catch (Exception arg) { Log.LogError((object)$"Harmony setup failed (game version drift?): {arg}"); } if ((Object)(object)_runner == (Object)null) { _runner = PersistentRunnerBase.CreateRunner<MirrorPersistentRunner>(); } ReloadGallery(notify: false); VersionChecker.CheckForUpdate("com.azraelgodking.havensmirror", "1.0.0", Log, delegate(VersionChecker.VersionCheckResult result) { result.NotifyUpdateAvailable(Log); }); Log.LogInfo((object)("Haven's Mirror v1.0.0 loaded. Gallery: " + GalleryPaths.GalleryRoot)); ModDiagnostics.LogModStartup(Log, "com.azraelgodking.havensmirror", "Haven's Mirror", "1.0.0", ModHealthIntegrationSummary.Build(("DevTools", "com.azraelgodking.havendevtools")), "startup", false); } public static void ReloadGallery(bool notify) { if (Shelf == null) { Shelf = new SpriteShelf(); } SaveSlotFolderSync.EnsureCharacterFolders(); string characterName = TryReadCharacterName(); string text = Shelf.LoadForCharacter(characterName); if (Shelf.HasAny) { ManualLogSource log = Log; if (log != null) { log.LogInfo((object)("[Gallery] " + text)); } } else { ManualLogSource log2 = Log; if (log2 != null) { log2.LogDebug((object)("[Gallery] " + text)); } } if (notify) { TryNotify("[Haven's Mirror] " + text); } } private static string TryReadCharacterName() { return GameSaveCharacterName.TryGetCurrent(null, delegate(string msg) { ManualLogSource log = Log; if (log != null) { log.LogDebug((object)("[Gallery] Character name resolve failed: " + msg)); } }); } private static void TryNotify(string message) { try { Type type = ReflectionHelper.FindWishType("NotificationStack"); object obj = ((type != null) ? ReflectionHelper.GetSingletonInstance(type) : null); if (obj == null) { return; } MethodInfo methodInfo = AccessTools.Method(type, "SendNotification", new Type[5] { typeof(string), typeof(int), typeof(int), typeof(bool), typeof(bool) }, (Type[])null) ?? AccessTools.Method(type, "SendNotification", (Type[])null, (Type[])null); if (methodInfo == null) { return; } ParameterInfo[] parameters = methodInfo.GetParameters(); if (parameters.Length == 1 && parameters[0].ParameterType == typeof(string)) { methodInfo.Invoke(obj, new object[1] { message }); return; } object[] array = new object[parameters.Length]; array[0] = message; for (int i = 1; i < parameters.Length; i++) { Type parameterType = parameters[i].ParameterType; if (parameterType == typeof(int)) { array[i] = 0; } else if (parameterType == typeof(bool)) { array[i] = false; } else if (parameters[i].HasDefaultValue) { array[i] = parameters[i].DefaultValue; } else { array[i] = GetDefault(parameterType); } } methodInfo.Invoke(obj, array); } catch (Exception ex) { ManualLogSource log = Log; if (log != null) { log.LogDebug((object)("[Notify] " + ex.Message)); } } } private static object GetDefault(Type type) { if (type == typeof(bool)) { return false; } if (type == typeof(int)) { return 0; } if (type == typeof(float)) { return 0f; } if (type.IsValueType) { return Activator.CreateInstance(type); } return null; } private static ConfigFile CreateNamedConfig() { return ConfigFileHelper.CreateNamedConfig("com.azraelgodking.havensmirror", "HavensMirror.cfg", delegate(string msg) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)msg); } }); } private void OnApplicationQuit() { _applicationQuitting = true; } private void OnDestroy() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) Scene activeScene = SceneManager.GetActiveScene(); string text = ((Scene)(ref activeScene)).name ?? string.Empty; string text2 = text.ToLowerInvariant(); if (_applicationQuitting || !Application.isPlaying || text2.Contains("menu") || text2.Contains("title")) { ManualLogSource log = Log; if (log != null) { log.LogInfo((object)("[Lifecycle] Plugin OnDestroy during expected teardown (scene: " + text + ")")); } Shelf?.Dispose(); Shelf = null; Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } _harmony = null; } else { ManualLogSource log2 = Log; if (log2 != null) { log2.LogWarning((object)("[Lifecycle] Plugin OnDestroy outside expected teardown (scene: " + text + ") — keeping Harmony patches, Shelf, and MirrorPersistentRunner")); } } } } public static class PluginInfo { public const string PLUGIN_GUID = "com.azraelgodking.havensmirror"; public const string PLUGIN_NAME = "Haven's Mirror"; public const string PLUGIN_VERSION = "1.0.0"; } } namespace HavensMirror.Look { public static class ForcedLookResolver { public static bool TryParseForced(out LookSlot slot) { slot = LookSlot.Spring; string text = MirrorOptions.ForcedLook?.Value; if (string.IsNullOrWhiteSpace(text)) { return false; } string text2 = text.Trim(); foreach (LookSlot value in Enum.GetValues(typeof(LookSlot))) { if (string.Equals(value.ToString(), text2, StringComparison.OrdinalIgnoreCase)) { slot = value; return true; } } switch (text2.ToLowerInvariant()) { case "fall": slot = LookSlot.Autumn; return true; case "wedding": case "ceremony": case "marriage": slot = LookSlot.Vows; return true; case "swim": case "swimsuit": case "beach": slot = LookSlot.Shore; return true; case "spooky": case "halloween": slot = LookSlot.Costume; return true; case "default": case "normal": slot = LookSlot.Spring; return true; default: { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[Look] Unknown ForcedLook '" + text + "' — ignored.")); } return false; } } } public static LookSlot ResolveActiveLook(bool vows, bool shore, bool costume) { if (TryParseForced(out var slot)) { if (slot != LookSlot.Vows && slot != LookSlot.Shore) { _ = 6; } return slot; } if (vows) { return LookSlot.Vows; } if (shore) { return LookSlot.Shore; } if (costume) { return LookSlot.Costume; } return ResolveSeasonalLook(); } public static LookSlot ResolveSeasonalLook() { if (TryParseForced(out var slot) && slot != LookSlot.Vows && slot != LookSlot.Shore && slot != LookSlot.Costume) { return slot; } string text = TryReadSeasonName(); if (string.IsNullOrEmpty(text)) { return LookSlot.Spring; } switch (text.ToLowerInvariant()) { case "summer": return LookSlot.Summer; case "fall": case "autumn": return LookSlot.Autumn; case "winter": return LookSlot.Winter; default: return LookSlot.Spring; } } public static int ResolveSeasonAnimatorIndex() { return ResolveSeasonalLook() switch { LookSlot.Summer => 1, LookSlot.Autumn => 2, LookSlot.Winter => 3, _ => 0, }; } public static void ApplyForcedFlags(ref bool vows, ref bool shore, ref bool costume) { if (TryParseForced(out var slot)) { switch (slot) { case LookSlot.Vows: vows = true; shore = false; costume = false; break; case LookSlot.Shore: shore = true; vows = false; costume = false; break; case LookSlot.Costume: costume = true; vows = false; shore = false; break; } } } private static string TryReadSeasonName() { try { Type type = ReflectionHelper.FindWishType("DayCycle"); if (type != null) { object singletonInstance = ReflectionHelper.GetSingletonInstance(type); if (singletonInstance != null) { object obj = (AccessToolsProp(singletonInstance.GetType(), "Season") ?? AccessToolsProp(singletonInstance.GetType(), "season"))?.GetValue(singletonInstance, null); if (obj != null) { return obj.ToString(); } } } Type type2 = ReflectionHelper.FindWishType("GameDate"); if (type2 != null) { FieldInfo fieldInfo = AccessTools.Field(type2, "Season") ?? AccessTools.Field(type2, "season"); if (fieldInfo != null && fieldInfo.IsStatic) { object value = fieldInfo.GetValue(null); if (value != null) { return value.ToString(); } } } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("[Look] Season read failed: " + ex.Message)); } } return null; } private static PropertyInfo AccessToolsProp(Type type, string name) { return AccessTools.Property(type, name); } } public static class NpcLookPatches { public static void Apply(Harmony harmony) { //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Expected O, but got Unknown Type type = ReflectionHelper.FindWishType("NPCAI"); if (type == null) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)"[Reflect] Wish.NPCAI not found — seasonal outfit override inactive."); } return; } MethodInfo methodInfo = AccessTools.Method(type, "LoadSeasonAnimators", (Type[])null, (Type[])null); if (methodInfo == null) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)"[Reflect] NPCAI.LoadSeasonAnimators not found."); } return; } try { harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(NpcLookPatches), "LoadSeasonAnimatorsPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogInfo((object)"Patched NPCAI.LoadSeasonAnimators for ForcedLook season override."); } } catch (Exception ex) { ManualLogSource log4 = Plugin.Log; if (log4 != null) { log4.LogWarning((object)("[Reflect] NPCAI.LoadSeasonAnimators patch failed: " + ex.Message)); } } } private static bool LoadSeasonAnimatorsPrefix(object __instance) { if (MirrorOptions.Enabled == null || !MirrorOptions.Enabled.Value) { return true; } if (!ForcedLookResolver.TryParseForced(out var slot)) { return true; } if (slot == LookSlot.Vows || slot == LookSlot.Shore || slot == LookSlot.Costume) { return true; } try { Type type = __instance.GetType(); FieldInfo obj = AccessTools.Field(type, "seasonAnimatorLoader") ?? AccessTools.Field(type, "_seasonAnimatorLoader"); FieldInfo fieldInfo = AccessTools.Field(type, "hasSeasonalSprites") ?? AccessTools.Field(type, "_hasSeasonalSprites"); FieldInfo layerField = AccessTools.Field(type, "currentAnimatorLayer") ?? AccessTools.Field(type, "_currentAnimatorLayer"); FieldInfo facingField = AccessTools.Field(type, "_facingDirection") ?? AccessTools.Field(type, "facingDirection"); FieldInfo meshField = AccessTools.Field(type, "_meshGenerator") ?? AccessTools.Field(type, "meshGenerator"); FieldInfo fieldInfo2 = AccessTools.Field(type, "_isBSDSluggedNPC") ?? AccessTools.Field(type, "isBSDSluggedNPC"); bool flag = default(bool); int num; if (fieldInfo2 != null) { object value = fieldInfo2.GetValue(__instance); if (value is bool) { flag = (bool)value; num = 1; } else { num = 0; } } else { num = 0; } if (((uint)num & (flag ? 1u : 0u)) != 0) { AccessTools.Method(type, "GetSluggedAnimator", (Type[])null, (Type[])null)?.Invoke(__instance, null); } object obj2 = obj?.GetValue(__instance); if (obj2 == null) { return false; } int num2 = 0; bool flag2 = default(bool); int num3; if (fieldInfo != null) { object value = fieldInfo.GetValue(__instance); if (value is bool) { flag2 = (bool)value; num3 = 1; } else { num3 = 0; } } else { num3 = 0; } if (((uint)num3 & (flag2 ? 1u : 0u)) != 0) { num2 = ForcedLookResolver.ResolveSeasonAnimatorIndex(); } MethodInfo methodInfo = AccessTools.Method(obj2.GetType(), "LoadAnimator", (Type[])null, (Type[])null); if (methodInfo == null) { return true; } object obj3 = CreateAnimatorCallback(__instance, layerField, facingField, meshField); if (obj3 != null && methodInfo.GetParameters().Length >= 2) { methodInfo.Invoke(obj2, new object[2] { num2, obj3 }); } else { if (methodInfo.GetParameters().Length != 1) { return true; } methodInfo.Invoke(obj2, new object[1] { num2 }); } return false; } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("[Look] LoadSeasonAnimatorsPrefix fallback to vanilla: " + ex.Message)); } return true; } } private static object CreateAnimatorCallback(object npc, FieldInfo layerField, FieldInfo facingField, FieldInfo meshField) { try { Type npcType = npc.GetType(); MethodInfo delay = AccessTools.Method(npcType, "DelayOneFrame", new Type[1] { typeof(Action) }, (Type[])null); if (delay == null) { return null; } Action body = delegate { try { object obj2 = (AccessTools.Property(npcType, "animator") ?? AccessTools.Property(npcType, "Animator"))?.GetValue(npc, null); if (obj2 != null) { object? obj3 = AccessTools.Property(obj2.GetType(), "gameObject")?.GetValue(obj2, null); GameObject val = (GameObject)((obj3 is GameObject) ? obj3 : null); object obj4 = AccessTools.Property(obj2.GetType(), "runtimeAnimatorController")?.GetValue(obj2, null); if (!((Object)(object)val == (Object)null) && val.activeInHierarchy && obj4 != null) { int num = 0; PropertyInfo propertyInfo = AccessTools.Property(obj2.GetType(), "layerCount"); if (propertyInfo != null) { num = (int)propertyInfo.GetValue(obj2, null); } int num2 = 0; if (layerField != null && layerField.GetValue(npc) is int num3) { num2 = num3; } MethodInfo methodInfo = AccessTools.Method(obj2.GetType(), "SetLayerWeight", new Type[2] { typeof(int), typeof(float) }, (Type[])null); if (methodInfo != null) { for (int i = 0; i < num; i++) { methodInfo.Invoke(obj2, new object[2] { i, (i == num2) ? 1f : 0f }); } } object mesh = meshField?.GetValue(npc); MethodInfo setDefault = ((mesh != null) ? AccessTools.Method(mesh.GetType(), "SetDefault", (Type[])null, (Type[])null) : null); Action action = delegate { try { setDefault?.Invoke(mesh, null); } catch { } }; if (delay != null) { delay.Invoke(npc, new object[1] { action }); } object obj5 = facingField?.GetValue(npc); MethodInfo methodInfo2 = AccessTools.Method(obj2.GetType(), "SetInteger", new Type[2] { typeof(string), typeof(int) }, (Type[])null); if (methodInfo2 != null && obj5 != null) { methodInfo2.Invoke(obj2, new object[2] { "Direction", Convert.ToInt32(obj5) }); } } } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("[Look] Animator callback: " + ex.Message)); } } }; return (Action)delegate { try { delay.Invoke(npc, new object[1] { body }); } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("[Look] DelayOneFrame: " + ex.Message)); } } }; } catch { return null; } } } } namespace HavensMirror.Hotkeys { public sealed class GalleryFolderSyncLoop : MonoBehaviour { private const float IntervalSeconds = 5f; private float _nextScan; private int _passes; private void OnEnable() { _nextScan = 0f; _passes = 0; } private void Update() { if (MirrorOptions.Enabled == null || !MirrorOptions.Enabled.Value || Time.unscaledTime < _nextScan) { return; } _nextScan = Time.unscaledTime + 5f; _passes++; int num = SaveSlotFolderSync.EnsureCharacterFolders(); if (num > 0) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)$"[Gallery] Save-slot sync created {num} folder(s)."); } } if (_passes > 24 && num == 0) { _nextScan = Time.unscaledTime + 30f; } } } public sealed class GalleryReloadLoop : MonoBehaviour { private void Update() { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) if (MirrorOptions.Enabled == null || !MirrorOptions.Enabled.Value) { return; } ConfigEntry<KeyCode> reloadKey = MirrorOptions.ReloadKey; KeyCode val = (KeyCode)((reloadKey != null) ? ((int)reloadKey.Value) : 0); if ((int)val != 0 && Input.GetKeyDown(val)) { ConfigEntry<KeyCode> reloadModifier = MirrorOptions.ReloadModifier; KeyCode val2 = (KeyCode)((reloadModifier != null) ? ((int)reloadModifier.Value) : 0); if ((int)val2 == 0 || Input.GetKey(val2)) { Plugin.ReloadGallery(MirrorOptions.NotifyOnReload?.Value ?? true); } } } } public sealed class MirrorPersistentRunner : PersistentRunnerBase { private const float SyncIntervalSeconds = 5f; private const float SyncSlowIntervalSeconds = 30f; private float _nextScan; private int _passes; protected override string RunnerName => "MirrorPersistentRunner"; protected override void OnUpdate() { if (MirrorOptions.Enabled != null && MirrorOptions.Enabled.Value) { CheckReloadHotkey(); TickFolderSync(); } } protected override void OnGameTransition() { Plugin.ReloadGallery(notify: false); } protected override void Log(string message) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)message); } } protected override void LogWarning(string message) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)message); } } private void CheckReloadHotkey() { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) ConfigEntry<KeyCode> reloadKey = MirrorOptions.ReloadKey; KeyCode val = (KeyCode)((reloadKey != null) ? ((int)reloadKey.Value) : 0); if ((int)val != 0 && Input.GetKeyDown(val)) { ConfigEntry<KeyCode> reloadModifier = MirrorOptions.ReloadModifier; KeyCode val2 = (KeyCode)((reloadModifier != null) ? ((int)reloadModifier.Value) : 0); if ((int)val2 == 0 || Input.GetKey(val2)) { Plugin.ReloadGallery(MirrorOptions.NotifyOnReload?.Value ?? true); } } } private void TickFolderSync() { if (Time.unscaledTime < _nextScan) { return; } _nextScan = Time.unscaledTime + 5f; _passes++; int num = SaveSlotFolderSync.EnsureCharacterFolders(); if (num > 0) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)$"[Gallery] Save-slot sync created {num} folder(s)."); } } if (_passes > 24 && num == 0) { _nextScan = Time.unscaledTime + 30f; } } } } namespace HavensMirror.Gallery { public static class GalleryPaths { public static string PluginDirectory { get { string location = Assembly.GetExecutingAssembly().Location; object obj; if (!string.IsNullOrEmpty(location)) { obj = Path.GetDirectoryName(location); if (obj == null) { return "."; } } else { obj = "."; } return (string)obj; } } public static string GalleryRoot { get { string text = MirrorOptions.GalleryFolderName?.Value; if (string.IsNullOrWhiteSpace(text)) { text = "gallery"; } return Path.Combine(PluginDirectory, text.Trim()); } } public static string SharedFolderName { get { string text = MirrorOptions.SharedFolderName?.Value; if (!string.IsNullOrWhiteSpace(text)) { return text.Trim(); } return "_shared"; } } public static string SharedFolder => Path.Combine(GalleryRoot, SharedFolderName); public static string CharacterFolder(string characterName) { string path = SanitizeFolderName(characterName); return Path.Combine(GalleryRoot, path); } public static string FileNameFor(LookSlot slot) { return slot switch { LookSlot.Summer => "summer.png", LookSlot.Autumn => "autumn.png", LookSlot.Winter => "winter.png", LookSlot.Vows => "vows.png", LookSlot.Shore => "shore.png", LookSlot.Costume => "costume.png", _ => "spring.png", }; } public static string SanitizeFolderName(string name) { if (string.IsNullOrWhiteSpace(name)) { return SharedFolderName; } string text = CharacterSaveStore.SanitizeFileName(name.Trim(), SharedFolderName); if (!string.IsNullOrEmpty(text)) { return text; } return SharedFolderName; } public static bool FolderHasPortraitFiles(string folder) { if (string.IsNullOrEmpty(folder) || !Directory.Exists(folder)) { return false; } foreach (LookSlot value in Enum.GetValues(typeof(LookSlot))) { if (File.Exists(Path.Combine(folder, FileNameFor(value)))) { return true; } } return false; } public static void EnsureStarterLayout(Action<string> logInfo = null) { if (MirrorOptions.CreateStarterFolders == null || !MirrorOptions.CreateStarterFolders.Value) { return; } try { CharacterSaveStore.EnsureDirectory(GalleryRoot); CharacterSaveStore.EnsureDirectory(SharedFolder); File.WriteAllText(Path.Combine(SharedFolder, "HOWTO.txt"), "Haven's Mirror\n==============\n\nWhat happens\n------------\n1. You install the mod.\n2. When the game runs, the mod reads your character saves and creates\n an empty folder for each one under gallery/ (named like the character).\n3. Empty folders are fine — they are ignored until you add PNGs.\n4. Drop PNG bust images into a character folder (or into this _shared folder).\n5. Talk to someone — if that folder has images, your bust appears.\n\nWhere images go\n---------------\n gallery/<YourCharacterName>/ (created automatically from saves)\n gallery/_shared/ (this folder — used if a character folder is empty)\n\nFile names (PNG)\n----------------\n spring.png everyday / spring (default)\n summer.png\n autumn.png\n winter.png\n vows.png wedding / ceremony\n shore.png swimsuit / beach\n costume.png halloween / costume\n\nYou do not need every file. Missing names reuse the first PNG in that folder.\n\nReload\n------\nAfter adding or changing images, press Ctrl+F8 in game (configurable).\nNo restart needed.\n"); logInfo?.Invoke("Gallery ready at '" + GalleryRoot + "'."); } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[Gallery] Could not create starter folders: " + ex.Message)); } } } } public enum LookSlot { Spring, Summer, Autumn, Winter, Vows, Shore, Costume } public static class SaveSlotFolderSync { private static int _lastKnownSlotCount = -1; private static string _lastSignature = string.Empty; public static int EnsureCharacterFolders() { if (MirrorOptions.CreateStarterFolders != null && !MirrorOptions.CreateStarterFolders.Value) { return 0; } List<string> list = CollectCharacterNamesFromSaves(); if (list.Count == 0) { return 0; } CharacterSaveStore.EnsureDirectory(GalleryPaths.GalleryRoot); int num = 0; foreach (string item in list) { string text = GalleryPaths.CharacterFolder(item); if (Directory.Exists(text)) { continue; } try { Directory.CreateDirectory(text); num++; ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)("[Gallery] Created character folder '" + text + "'.")); } } catch (Exception ex) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)("[Gallery] Could not create folder for '" + item + "': " + ex.Message)); } } } string text2 = string.Join("|", list); if (num > 0 || list.Count != _lastKnownSlotCount || text2 != _lastSignature) { _lastKnownSlotCount = list.Count; _lastSignature = text2; if (num == 0 && list.Count > 0) { ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogDebug((object)$"[Gallery] Save-slot sync: {list.Count} character folder(s) present."); } } } return num; } public static List<string> CollectCharacterNamesFromSaves() { List<string> list = new List<string>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); try { Type type = ReflectionHelper.FindWishType("GameSave"); if (type == null) { return list; } object obj = ReflectionHelper.GetSingletonInstance(type) ?? AccessTools.Property(type, "Instance")?.GetValue(null); if (obj == null) { return list; } if (!((AccessTools.Property(obj.GetType(), "Saves")?.GetValue(obj, null) ?? AccessTools.Field(obj.GetType(), "Saves")?.GetValue(obj) ?? AccessTools.Property(type, "Saves")?.GetValue(null)) is IEnumerable enumerable)) { return list; } foreach (object item in enumerable) { if (item == null) { continue; } string text = ReadCharacterNameFromSaveData(item); if (!string.IsNullOrWhiteSpace(text)) { text = text.Trim(); if (hashSet.Add(text)) { list.Add(text); } } } string text2 = GameSaveCharacterName.TryGetCurrent(); if (!string.IsNullOrWhiteSpace(text2) && hashSet.Add(text2.Trim())) { list.Add(text2.Trim()); } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("[Gallery] Save-slot scan failed: " + ex.Message)); } } list.Sort(StringComparer.OrdinalIgnoreCase); return list; } private static string ReadCharacterNameFromSaveData(object saveData) { if (saveData == null) { return null; } object obj = AccessTools.Property(saveData.GetType(), "characterData")?.GetValue(saveData, null) ?? AccessTools.Field(saveData.GetType(), "characterData")?.GetValue(saveData); if (obj != null) { string text = (AccessTools.Property(obj.GetType(), "characterName")?.GetValue(obj, null) ?? AccessTools.Field(obj.GetType(), "characterName")?.GetValue(obj)) as string; if (!string.IsNullOrWhiteSpace(text)) { return text; } } if ((AccessTools.Property(saveData.GetType(), "characterName")?.GetValue(saveData, null) ?? AccessTools.Field(saveData.GetType(), "characterName")?.GetValue(saveData)) is string text2 && !string.IsNullOrWhiteSpace(text2)) { return text2; } if ((AccessTools.Property(saveData.GetType(), "fileName")?.GetValue(saveData, null) ?? AccessTools.Field(saveData.GetType(), "fileName")?.GetValue(saveData)) is string text3 && !string.IsNullOrWhiteSpace(text3)) { string text4 = text3.Trim(); if (text4.EndsWith(".save", StringComparison.OrdinalIgnoreCase)) { text4 = text4.Substring(0, text4.Length - 5); } if (!string.IsNullOrWhiteSpace(text4)) { return text4; } } return null; } } public sealed class SpriteShelf : IDisposable { private readonly Dictionary<LookSlot, Sprite> _sprites = new Dictionary<LookSlot, Sprite>(); private readonly List<Texture2D> _textures = new List<Texture2D>(); private string _loadedFrom; public bool HasAny => _sprites.Count > 0; public string LoadedFrom => _loadedFrom; public void Clear() { foreach (KeyValuePair<LookSlot, Sprite> sprite in _sprites) { if ((Object)(object)sprite.Value != (Object)null) { Object.Destroy((Object)(object)sprite.Value); } } _sprites.Clear(); foreach (Texture2D texture in _textures) { if ((Object)(object)texture != (Object)null) { Object.Destroy((Object)(object)texture); } } _textures.Clear(); _loadedFrom = null; } public void Dispose() { Clear(); } public string LoadForCharacter(string characterName) { Clear(); string[] array = new string[2] { GalleryPaths.CharacterFolder(characterName), GalleryPaths.SharedFolder }; foreach (string text in array) { if (!GalleryPaths.FolderHasPortraitFiles(text)) { continue; } Sprite val = null; foreach (LookSlot value in Enum.GetValues(typeof(LookSlot))) { string path = Path.Combine(text, GalleryPaths.FileNameFor(value)); Sprite val2 = TryLoadPng(path); if (!((Object)(object)val2 == (Object)null)) { _sprites[value] = val2; if ((Object)(object)val == (Object)null) { val = val2; } } } if ((Object)(object)val == (Object)null) { continue; } foreach (LookSlot value2 in Enum.GetValues(typeof(LookSlot))) { if (!_sprites.ContainsKey(value2)) { _sprites[value2] = val; } } _loadedFrom = text; return "Loaded portraits from '" + text + "'."; } return "No portrait PNGs yet (empty character folders are ignored; drop files in gallery/<name>/ or gallery/_shared/)."; } public Sprite Get(LookSlot slot) { if (!_sprites.TryGetValue(slot, out Sprite value)) { return null; } return value; } private Sprite TryLoadPng(string path) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Expected O, but got Unknown //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) try { if (!File.Exists(path)) { return null; } byte[] array = File.ReadAllBytes(path); if (array == null || array.Length == 0) { return null; } Texture2D val = new Texture2D(2, 2, (TextureFormat)4, false); if (!ImageConversion.LoadImage(val, array)) { Object.Destroy((Object)(object)val); ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[Gallery] Failed to decode PNG: " + path)); } return null; } ((Texture)val).filterMode = (FilterMode)1; ((Texture)val).wrapMode = (TextureWrapMode)1; ((Object)val).name = Path.GetFileNameWithoutExtension(path); Sprite obj = Sprite.Create(val, new Rect(0f, 0f, (float)((Texture)val).width, (float)((Texture)val).height), new Vector2(0.5f, 0.5f), 100f); ((Object)obj).name = ((Object)val).name; _textures.Add(val); ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogInfo((object)$"[Gallery] Loaded {path} ({((Texture)val).width}x{((Texture)val).height})."); } return obj; } catch (Exception ex) { ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogWarning((object)("[Gallery] Error loading '" + path + "': " + ex.Message)); } return null; } } } } namespace HavensMirror.Dialogue { public static class DialogueMirrorBinder { private static bool _bound; private static FieldInfo _dialoguePanelField; private static FieldInfo _bustField; private static FieldInfo _npcNameField; private static readonly Dictionary<LookSlot, object> EmoteTables = new Dictionary<LookSlot, object>(); private static readonly Dictionary<LookSlot, object> EmoteAddressables = new Dictionary<LookSlot, object>(); public static void Apply(Harmony harmony) { if (_bound) { return; } Type type = ReflectionHelper.FindWishType("DialogueController"); if (type == null) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)"[Reflect] Wish.DialogueController not found — player bust overlay inactive."); } return; } _dialoguePanelField = FindField(type, "_dialoguePanel", "dialoguePanel", "DialoguePanel"); _bustField = FindField(type, "_bust", "bust", "Bust"); _npcNameField = FindField(type, "npcName", "_npcName", "NpcName"); TryPatchAwake(harmony, type); TryPatchBustVisualSetters(harmony, type); TryPatchInitialBustSelectors(harmony, type); TryPatchCancelDialogue(harmony, type); _bound = true; } private static void TryPatchAwake(Harmony harmony, Type dialogueType) { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(dialogueType, "Awake", (Type[])null, (Type[])null); if (methodInfo == null) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)"[Reflect] DialogueController.Awake missing."); } return; } harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(DialogueMirrorBinder), "AwakePostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogInfo((object)"Patched DialogueController.Awake for mirror HUD attach."); } } private static void TryPatchBustVisualSetters(Harmony harmony, Type dialogueType) { //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Expected O, but got Unknown //IL_0098: Expected O, but got Unknown int num = 0; foreach (MethodInfo declaredMethod in AccessTools.GetDeclaredMethods(dialogueType)) { if (declaredMethod == null || declaredMethod.IsAbstract) { continue; } string text = declaredMethod.Name ?? string.Empty; if (text.IndexOf("SetDialogueBust", StringComparison.OrdinalIgnoreCase) < 0 && text.IndexOf("SetBustVisual", StringComparison.OrdinalIgnoreCase) < 0) { continue; } try { harmony.Patch((MethodBase)declaredMethod, new HarmonyMethod(typeof(DialogueMirrorBinder), "BustSetterPrefix", (Type[])null), new HarmonyMethod(typeof(DialogueMirrorBinder), "BustSetterPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); num++; ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)("Patched " + dialogueType.Name + "." + declaredMethod.Name + " for player bust present.")); } } catch (Exception ex) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)("[Reflect] Could not patch " + declaredMethod.Name + ": " + ex.Message)); } } } if (num == 0) { ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogWarning((object)"[Reflect] No DialogueController bust visual setters found."); } } } private static void TryPatchInitialBustSelectors(Harmony harmony, Type dialogueType) { //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Expected O, but got Unknown //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Expected O, but got Unknown string[] array = new string[2] { "SetInitialBust", "GetInitialBust" }; foreach (string text in array) { foreach (MethodInfo declaredMethod in AccessTools.GetDeclaredMethods(dialogueType)) { if (declaredMethod == null || !string.Equals(declaredMethod.Name, text, StringComparison.Ordinal)) { continue; } try { if (text == "SetInitialBust") { harmony.Patch((MethodBase)declaredMethod, new HarmonyMethod(typeof(DialogueMirrorBinder), "SetInitialBustPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } else { harmony.Patch((MethodBase)declaredMethod, new HarmonyMethod(typeof(DialogueMirrorBinder), "GetInitialBustPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)("Patched DialogueController." + text + " for forced look.")); } } catch (Exception ex) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)("[Reflect] Could not patch " + text + ": " + ex.Message)); } } } } } private static void AwakePostfix(object __instance) { try { if (__instance == null) { return; } CaptureEmoteTables(__instance); EnsureHudOn(__instance); if ((Object)(object)PlayerBustHud.Instance != (Object)null) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)"[Dialogue] PlayerBustHud attached on DialogueController.Awake."); } } else { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)"[Dialogue] Awake: DialogueController host not found — HUD attach deferred."); } } SaveSlotFolderSync.EnsureCharacterFolders(); Plugin.ReloadGallery(notify: false); } catch (Exception ex) { ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogWarning((object)("[Dialogue] Awake hook failed: " + ex.Message)); } } } private static void BustSetterPrefix(object[] __args) { if (MirrorOptions.Enabled == null || !MirrorOptions.Enabled.Value) { return; } try { MutateLookFlagsInArgs(__args); } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("[Dialogue] BustSetterPrefix: " + ex.Message)); } } } private static void BustSetterPostfix(object __instance, object[] __args) { if (MirrorOptions.Enabled == null || !MirrorOptions.Enabled.Value) { return; } try { bool vows = false; bool shore = false; bool costume = false; bool refresh = false; ReadLookFlagsFromArgs(__args, ref vows, ref shore, ref costume, ref refresh); EnsureHudOn(__instance); PlayerBustHud instance = PlayerBustHud.Instance; if ((Object)(object)instance == (Object)null) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)"[Dialogue] Present skipped — PlayerBustHud not attached."); } } else { instance.Present(vows, shore, costume); } } catch (Exception ex) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)("[Dialogue] BustSetterPostfix: " + ex.Message)); } } } private static void TryPatchCancelDialogue(Harmony harmony, Type dialogueType) { //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Expected O, but got Unknown foreach (MethodInfo declaredMethod in AccessTools.GetDeclaredMethods(dialogueType)) { if (declaredMethod == null || !string.Equals(declaredMethod.Name, "CancelDialogue", StringComparison.Ordinal)) { continue; } try { harmony.Patch((MethodBase)declaredMethod, (HarmonyMethod)null, new HarmonyMethod(typeof(DialogueMirrorBinder), "CancelDialoguePostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)("Patched DialogueController." + declaredMethod.Name + " to hide player bust.")); } } catch (Exception ex) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)("[Reflect] Could not patch CancelDialogue: " + ex.Message)); } } } } private static void CancelDialoguePostfix() { try { PlayerBustHud.Instance?.Hide(); } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("[Dialogue] CancelDialogue hide: " + ex.Message)); } } } private static void EnsureHudOn(object dialogueController) { GameObject val = null; Component val2 = (Component)((dialogueController is Component) ? dialogueController : null); if (val2 != null) { val = val2.gameObject; } if ((Object)(object)val == (Object)null && _dialoguePanelField != null && dialogueController != null) { object? value = _dialoguePanelField.GetValue(dialogueController); val = (GameObject)((value is GameObject) ? value : null); } if ((Object)(object)val == (Object)null) { return; } bool flag = (Object)(object)PlayerBustHud.Instance == (Object)null || (Object)(object)((Component)PlayerBustHud.Instance).gameObject == (Object)null; PlayerBustHud playerBustHud = PlayerBustHud.EnsureOn(val); if ((Object)(object)playerBustHud == (Object)null) { return; } if (_bustField != null && dialogueController != null) { object? value2 = _bustField.GetValue(dialogueController); Image val3 = (Image)((value2 is Image) ? value2 : null); if (val3 != null) { playerBustHud.BindNpcBust(val3); } } if (flag) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)"[Dialogue] PlayerBustHud attached on DialogueController host."); } } } private static bool SetInitialBustPrefix(object __instance, object[] __args) { if (MirrorOptions.Enabled == null || !MirrorOptions.Enabled.Value) { return true; } try { MutateLookFlagsInArgs(__args); string text = ReadNpcName(__instance); if (string.IsNullOrWhiteSpace(text)) { return false; } if (!ForcedLookResolver.TryParseForced(out var _) && !ArgsRequestSpecialLook(__args)) { return true; } LookSlot slot2 = ForcedLookResolver.ResolveActiveLook(ArgsHasFlag(__args, "marriage", "wedding", "vow"), ArgsHasFlag(__args, "swimsuit", "swim", "shore"), ArgsHasFlag(__args, "halloween", "costume")); if (TryAssignNpcSprite(__instance, slot2, text, __args)) { return false; } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("[Dialogue] SetInitialBustPrefix: " + ex.Message)); } } return true; } private static bool GetInitialBustPrefix(object __instance, ref object __result, object[] __args) { if (MirrorOptions.Enabled == null || !MirrorOptions.Enabled.Value) { return true; } try { MutateLookFlagsInArgs(__args); string text = ReadNpcName(__instance); if (string.IsNullOrWhiteSpace(text)) { return false; } if (!ForcedLookResolver.TryParseForced(out var _) && !ArgsRequestSpecialLook(__args)) { return true; } LookSlot slot2 = ForcedLookResolver.ResolveActiveLook(ArgsHasFlag(__args, "marriage", "wedding", "vow"), ArgsHasFlag(__args, "swimsuit", "swim", "shore"), ArgsHasFlag(__args, "halloween", "costume")); if (TryResolveAddressable(__instance, slot2, text, out object assetRef)) { __result = assetRef; return false; } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("[Dialogue] GetInitialBustPrefix: " + ex.Message)); } } return true; } private static void CaptureEmoteTables(object dialogueController) { EmoteTables.Clear(); EmoteAddressables.Clear(); TryCapture(dialogueController, LookSlot.Spring, EmoteTables, "_npcEmotes", "npcEmotes"); TryCapture(dialogueController, LookSlot.Summer, EmoteTables, "_npcSummerEmotes", "npcSummerEmotes"); TryCapture(dialogueController, LookSlot.Autumn, EmoteTables, "_npcFallEmotes", "npcFallEmotes"); TryCapture(dialogueController, LookSlot.Winter, EmoteTables, "_npcWinterEmotes", "npcWinterEmotes"); TryCapture(dialogueController, LookSlot.Vows, EmoteTables, "_npcWeddingEmotes", "npcWeddingEmotes"); TryCapture(dialogueController, LookSlot.Shore, EmoteTables, "_npcSwimsuitEmotes", "npcSwimsuitEmotes"); TryCapture(dialogueController, LookSlot.Spring, EmoteAddressables, "_npcEmotes2", "npcEmotes2"); TryCapture(dialogueController, LookSlot.Summer, EmoteAddressables, "_npcSummerEmotes2", "npcSummerEmotes2"); TryCapture(dialogueController, LookSlot.Autumn, EmoteAddressables, "_npcFallEmotes2", "npcFallEmotes2"); TryCapture(dialogueController, LookSlot.Winter, EmoteAddressables, "_npcWinterEmotes2", "npcWinterEmotes2"); TryCapture(dialogueController, LookSlot.Vows, EmoteAddressables, "_npcWeddingEmotes2", "npcWeddingEmotes2"); TryCapture(dialogueController, LookSlot.Shore, EmoteAddressables, "_npcSwimsuitEmotes2", "npcSwimsuitEmotes2"); TryCapture(dialogueController, LookSlot.Costume, EmoteAddressables, "_npcHalloweenEmotes2", "npcHalloweenEmotes2"); } private static void TryCapture(object instance, LookSlot slot, Dictionary<LookSlot, object> bag, params string[] names) { FieldInfo fieldInfo = FindField(instance.GetType(), names); if (!(fieldInfo == null)) { object value = fieldInfo.GetValue(instance); if (value != null) { bag[slot] = value; } } } private static bool TryAssignNpcSprite(object dialogueController, LookSlot slot, string npcName, object[] args) { if (!EmoteTables.TryGetValue(slot, out object value) || value == null) { return false; } if (!(value is IDictionary dictionary)) { return false; } object obj = null; foreach (DictionaryEntry item in dictionary) { if (item.Key != null && string.Equals(item.Key.ToString(), npcName, StringComparison.OrdinalIgnoreCase)) { obj = item.Value; break; } } if (obj == null) { return false; } Sprite val = null; if (obj is IList { Count: >0 } list) { object? obj2 = list[0]; val = (Sprite)((obj2 is Sprite) ? obj2 : null); } if ((Object)(object)val == (Object)null) { return false; } ImageFieldAssign(dialogueController, val, args); return true; } private static void ImageFieldAssign(object dialogueController, Sprite sprite, object[] args) { if (args != null) { foreach (object obj in args) { Image val = (Image)((obj is Image) ? obj : null); if (val != null) { val.sprite = sprite; return; } } } FieldInfo fieldInfo = FindField(dialogueController.GetType(), "_bust", "bust", "Bust"); if (fieldInfo != null) { object? value = fieldInfo.GetValue(dialogueController); Image val2 = (Image)((value is Image) ? value : null); if (val2 != null) { val2.sprite = sprite; } } } private static bool TryResolveAddressable(object dialogueController, LookSlot slot, string npcName, out object assetRef) { assetRef = null; if (!EmoteAddressables.TryGetValue(slot, out object value) || value == null) { return false; } if (!(value is IDictionary dictionary)) { return false; } object obj = null; foreach (DictionaryEntry item in dictionary) { if (item.Key != null && string.Equals(item.Key.ToString(), npcName, StringComparison.OrdinalIgnoreCase)) { obj = item.Value; break; } } if (obj is Array { Length: >0 } array) { assetRef = array.GetValue(0); return assetRef != null; } return false; } private static string ReadNpcName(object dialogueController) { if (_npcNameField == null || dialogueController == null) { return null; } return _npcNameField.GetValue(dialogueController)?.ToString(); } private static void MutateLookFlagsInArgs(object[] args) { if (args == null || !ForcedLookResolver.TryParseForced(out var slot) || (slot != LookSlot.Vows && slot != LookSlot.Shore && slot != LookSlot.Costume)) { return; } List<int> list = new List<int>(); for (int i = 0; i < args.Length; i++) { if (args[i] is bool) { list.Add(i); } } if (list.Count < 2) { return; } if (list.Count > 1) { args[list[1]] = false; } if (list.Count > 2) { args[list[2]] = false; } if (list.Count > 5) { args[list[5]] = false; } switch (slot) { case LookSlot.Vows: args[list[1]] = true; return; case LookSlot.Shore: if (list.Count > 2) { args[list[2]] = true; return; } break; } if (slot == LookSlot.Costume && list.Count > 5) { args[list[5]] = true; } else if (slot == LookSlot.Costume) { args[list[list.Count - 1]] = true; } } private static void ReadLookFlagsFromArgs(object[] args, ref bool vows, ref bool shore, ref bool costume, ref bool refresh) { if (args == null) { return; } List<bool> list = new List<bool>(); for (int i = 0; i < args.Length; i++) { if (args[i] is bool item) { list.Add(item); } } if (list.Count > 1) { vows = list[1]; } if (list.Count > 2) { shore = list[2]; } if (list.Count > 4) { refresh = list[4]; } if (list.Count > 5) { costume = list[5]; } ForcedLookResolver.ApplyForcedFlags(ref vows, ref shore, ref costume); } private static bool ArgsRequestSpecialLook(object[] args) { bool vows = false; bool shore = false; bool costume = false; bool refresh = false; ReadLookFlagsFromArgs(args, ref vows, ref shore, ref costume, ref refresh); return vows || shore || costume; } private static bool ArgsHasFlag(object[] args, params string[] _) { bool vows = false; bool shore = false; bool costume = false; bool refresh = false; ReadLookFlagsFromArgs(args, ref vows, ref shore, ref costume, ref refresh); return vows || shore || costume; } private static FieldInfo FindField(Type type, params string[] names) { if (type == null || names == null) { return null; } foreach (string text in names) { FieldInfo fieldInfo = AccessTools.Field(type, text); if (fieldInfo != null) { return fieldInfo; } } return null; } } public sealed class PlayerBustHud : MonoBehaviour { private const float LeftMargin = 24f; private const float LeftFraction = 0.15f; private const float FallbackBustHeight = 250f; private const float FallbackAspect = 0.83f; private static PlayerBustHud _instance; private static bool _loggedLayout; private Image _image; private RectTransform _rect; private bool _ready; private Image _npcBust; private Coroutine _relayoutRoutine; public static PlayerBustHud Instance => _instance; public static PlayerBustHud EnsureOn(GameObject host) { if ((Object)(object)host == (Object)null) { return null; } if ((Object)(object)_instance != (Object)null && (Object)(object)((Component)_instance).gameObject != (Object)null) { return _instance; } PlayerBustHu