Decompiled source of HavensMirror v1.0.3
HavensMirror.dll
Decompiled 4 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.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; 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.3.0")] [assembly: AssemblyInformationalVersion("1.0.3+2ebb214373fb7d600120e36493d07fb894d787db")] [assembly: AssemblyProduct("HavensMirror")] [assembly: AssemblyTitle("HavensMirror")] [assembly: AssemblyVersion("1.0.3.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.10.10"; } 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.10.10"; } 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 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.10.10", IntegrationSummary = integrationSummary, Mode = mode, DataLoaded = dataLoaded, LastPersistenceOutcome = lastPersistenceOutcome }); } } 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 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 { private sealed class MemberAccessor { public static readonly MemberAccessor Missing = new MemberAccessor(null, null); public readonly PropertyInfo Property; public readonly FieldInfo Field; public MemberAccessor(PropertyInfo property, FieldInfo field) { Property = property; Field = field; } } public static readonly BindingFlags AllBindingFlags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy; private static readonly object MemberCacheLock = new object(); private static readonly Dictionary<(Type, string), MemberAccessor> StaticGetCache = new Dictionary<(Type, string), MemberAccessor>(); private static readonly Dictionary<(Type, string), MemberAccessor> InstanceGetCache = new Dictionary<(Type, string), MemberAccessor>(); private static readonly Dictionary<(Type, string), MemberAccessor> InstanceSetCache = new Dictionary<(Type, string), MemberAccessor>(); private static readonly string[] SingletonMemberNames = new string[5] { "Instance", "instance", "_instance", "Singleton", "singleton" }; private static bool TryGetCached(Dictionary<(Type, string), MemberAccessor> cache, Type type, string name, out MemberAccessor accessor) { lock (MemberCacheLock) { return cache.TryGetValue((type, name), out accessor); } } private static void StoreCached(Dictionary<(Type, string), MemberAccessor> cache, Type type, string name, MemberAccessor accessor) { lock (MemberCacheLock) { cache[(type, name)] = accessor; } } 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 || memberName == null) { return null; } if (!TryGetCached(StaticGetCache, type, memberName, out MemberAccessor accessor)) { accessor = ResolveStaticGetter(type, memberName); StoreCached(StaticGetCache, type, memberName, accessor); } if (accessor.Property != null) { return accessor.Property.GetValue(null); } if (accessor.Field != null) { return accessor.Field.GetValue(null); } return null; } private static MemberAccessor ResolveStaticGetter(Type type, string memberName) { try { PropertyInfo property = type.GetProperty(memberName, AllBindingFlags); if (property != null && property.GetMethod != null && property.GetIndexParameters().Length == 0) { return new MemberAccessor(property, null); } } catch (AmbiguousMatchException) { return MemberAccessor.Missing; } FieldInfo field = type.GetField(memberName, AllBindingFlags); if (!(field != null)) { return MemberAccessor.Missing; } return new MemberAccessor(null, field); } public static object GetSingletonInstance(Type type) { if (type == null) { return null; } string[] singletonMemberNames = SingletonMemberNames; foreach (string memberName in singletonMemberNames) { object staticValue = GetStaticValue(type, memberName); if (staticValue != null) { return staticValue; } } return null; } public static object GetInstanceValue(object instance, string memberName) { if (instance == null || memberName == null) { return null; } Type type = instance.GetType(); if (!TryGetCached(InstanceGetCache, type, memberName, out MemberAccessor accessor)) { accessor = ResolveInstanceMember(type, memberName, forSet: false); StoreCached(InstanceGetCache, type, memberName, accessor); } if (accessor.Property != null) { return accessor.Property.GetValue(instance); } if (accessor.Field != null) { return accessor.Field.GetValue(instance); } return null; } private static MemberAccessor ResolveInstanceMember(Type type, string memberName, bool forSet) { while (type != null) { PropertyInfo property = type.GetProperty(memberName, AllBindingFlags); if (property != null && (forSet ? property.SetMethod : property.GetMethod) != null) { return new MemberAccessor(property, null); } FieldInfo field = type.GetField(memberName, AllBindingFlags); if (field != null) { return new MemberAccessor(null, field); } type = type.BaseType; } return MemberAccessor.Missing; } public static bool SetInstanceValue(object instance, string memberName, object value) { if (instance == null || memberName == null) { return false; } Type type = instance.GetType(); if (!TryGetCached(InstanceSetCache, type, memberName, out MemberAccessor accessor)) { accessor = ResolveInstanceMember(type, memberName, forSet: true); StoreCached(InstanceSetCache, type, memberName, accessor); } if (accessor.Property != null) { accessor.Property.SetValue(instance, value); return true; } if (accessor.Field != null) { accessor.Field.SetValue(instance, value); return true; } 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 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 static class ReflectionProbe { private static readonly HashSet<string> Reported = new HashSet<string>(StringComparer.Ordinal); private static readonly object Lock = new object(); public static bool Try(string site, Action action, ManualLogSource log = null) { if (action == null) { return false; } try { action(); return true; } catch (Exception ex) { bool flag; lock (Lock) { flag = Reported.Add(site ?? "unknown"); } if (flag && log != null) { log.LogWarning((object)("[Probe] " + site + " failed (" + ex.GetType().Name + ": " + ex.Message + ") — game version drift?")); } return false; } } public static bool Try<T>(string site, Func<T> func, out T result, ManualLogSource log = null) { result = default(T); if (func == null) { return false; } try { result = func(); return true; } catch (Exception ex) { bool flag; lock (Lock) { flag = Reported.Add(site ?? "unknown"); } if (flag && log != null) { log.LogWarning((object)("[Probe] " + site + " failed (" + ex.GetType().Name + ": " + ex.Message + ") — game version drift?")); } return false; } } public static void LogOnce(string site, Exception ex, ManualLogSource log = null) { bool flag; lock (Lock) { flag = Reported.Add(site ?? "unknown"); } if (flag) { string text = ((ex != null) ? (ex.GetType().Name + ": " + ex.Message) : "unknown error"); if (log != null) { log.LogWarning((object)("[Probe] " + site + " failed (" + text + ") — game version drift?")); } } } } 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 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 }; string json = null; string fetchError = null; while (true) { SharedFetchState sharedFetchState = SharedFetchState.Read(); TimeSpan timeSpan = DateTime.UtcNow - sharedFetchState.AtUtc; if (sharedFetchState.Kind == "done" && timeSpan < SharedSuccessTtl && AppDomain.CurrentDomain.GetData("SunhavenMods.VersionChecker.Json") is string text) { json = text; break; } if (sharedFetchState.Kind == "failed" && timeSpan < SharedFailureTtl) { fetchError = sharedFetchState.Message; break; } if (sharedFetchState.Kind == "fetching" && timeSpan < SharedFetchTimeout) { yield return null; continue; } SharedFetchState.Write("fetching", null); UnityWebRequest www = UnityWebRequest.Get("https://raw.githubusercontent.com/AzraelGodKing/SunhavenMods/main/docs/versions.json"); try { www.timeout = 10; yield return www.SendWebRequest(); if ((int)www.result == 2 || (int)www.result == 3) { fetchError = "Network error: " + www.error; SharedFetchState.Write("failed", fetchError); } else { json = www.downloadHandler.text; AppDomain.CurrentDomain.SetData("SunhavenMods.VersionChecker.Json", json); SharedFetchState.Write("done", null); } } finally { ((IDisposable)www)?.Dispose(); } break; } string text2 = fetchError; if (text2 == null) { try { text2 = EvaluateManifest(json, pluginGuid, currentVersion, result); } catch (Exception ex) { text2 = "Parse error: " + ex.Message; } } if (text2 != null) { result.Success = false; result.UpdateAvailable = false; result.ErrorMessage = text2; RecordHealthError(pluginGuid, text2); LogWarningMsg(text2); } else { result.Success = true; if (result.UpdateAvailable) { LogInfo("Update available for " + result.ModName + ": " + currentVersion + " -> " + result.LatestVersion); } else { LogInfo(result.ModName + " is up to date (v" + currentVersion + ")"); } } onComplete?.Invoke(result); Object.Destroy((Object)(object)((Component)this).gameObject); } } private struct SharedFetchState { public const string Fetching = "fetching"; public const string Done = "done"; public const string Failed = "failed"; public string Kind; public DateTime AtUtc; public string Message; public static SharedFetchState Read() { SharedFetchState result = new SharedFetchState { AtUtc = DateTime.MinValue }; if (!(AppDomain.CurrentDomain.GetData("SunhavenMods.VersionChecker.State") is string text)) { return result; } string[] array = text.Split(new char[1] { '|' }, 3); if (array.Length < 2 || !long.TryParse(array[1], NumberStyles.None, CultureInfo.InvariantCulture, out var result2)) { return result; } result.Kind = array[0]; result.AtUtc = new DateTime(result2, DateTimeKind.Utc); result.Message = ((array.Length > 2) ? array[2] : null); return result; } public static void Write(string kind, string message) { string text = DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture); AppDomain.CurrentDomain.SetData("SunhavenMods.VersionChecker.State", kind + "|" + text + "|" + (message ?? string.Empty)); } } private const string VersionsUrl = "https://raw.githubusercontent.com/AzraelGodKing/SunhavenMods/main/docs/versions.json"; private const string SharedStateKey = "SunhavenMods.VersionChecker.State"; private const string SharedJsonKey = "SunhavenMods.VersionChecker.Json"; private static readonly TimeSpan SharedFetchTimeout = TimeSpan.FromSeconds(20.0); private static readonly TimeSpan SharedSuccessTtl = TimeSpan.FromMinutes(30.0); private static readonly TimeSpan SharedFailureTtl = TimeSpan.FromMinutes(1.0); private static readonly Dictionary<string, ModHealthSnapshot> HealthByPluginGuid = new Dictionary<string, ModHealthSnapshot>(StringComparer.OrdinalIgnoreCase); private static readonly object HealthLock = new object(); 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 int? TryCompareVersions(string v1, string v2) { if (!TryParseVersionCore(v1, out int[] parts) || !TryParseVersionCore(v2, out int[] parts2)) { return null; } int num = Math.Max(parts.Length, parts2.Length); for (int i = 0; i < num; i++) { int num2 = ((i < parts.Length) ? parts[i] : 0); int num3 = ((i < parts2.Length) ? parts2[i] : 0); if (num2 < num3) { return -1; } if (num2 > num3) { return 1; } } return 0; } private static bool TryParseVersionCore(string version, out int[] parts) { parts = null; if (string.IsNullOrWhiteSpace(version)) { return false; } string text = version.Trim().TrimStart('v', 'V'); int num = text.IndexOfAny(new char[2] { '-', '+' }); if (num >= 0) { text = text.Substring(0, num); } string[] array = text.Split(new char[1] { '.' }); int[] array2 = new int[array.Length]; for (int i = 0; i < array.Length; i++) { if (!int.TryParse(array[i], NumberStyles.None, CultureInfo.InvariantCulture, out array2[i])) { return false; } } parts = array2; return true; } internal static string EvaluateManifest(string json, string pluginGuid, string currentVersion, VersionCheckResult result) { Dictionary<string, object> dictionary; try { dictionary = MinimalJsonParser.Parse((json ?? string.Empty).TrimStart(new char[1] { '\ufeff' })) as Dictionary<string, object>; } catch (JsonParseException ex) { return "versions.json is not valid JSON (" + ex.Message + ")"; } if (dictionary == null) { return "versions.json root is not an object"; } if (string.IsNullOrEmpty(pluginGuid) || !dictionary.TryGetValue(pluginGuid, out var value) || !(value is Dictionary<string, object> entry)) { return "Mod '" + pluginGuid + "' not found in versions.json"; } result.LatestVersion = ManifestField(entry, "version"); result.ModName = ManifestField(entry, "name"); result.NexusUrl = ManifestField(entry, "nexus"); result.Changelog = ManifestField(entry, "changelog"); if (string.IsNullOrEmpty(result.LatestVersion)) { return "Could not parse version from response"; } int? num = TryCompareVersions(currentVersion, result.LatestVersion); if (!num.HasValue) { return "Could not compare installed version '" + currentVersion + "' with '" + result.LatestVersion + "'"; } result.UpdateAvailable = num.Value < 0; return null; } private static string ManifestField(Dictionary<string, object> entry, string key) { if (!entry.TryGetValue(key, out object value) || value == null) { return null; } return (value as string) ?? Convert.ToString(value, CultureInfo.InvariantCulture); } 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; } } } 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)); } } } public sealed class JsonParseException : Exception { public int Position { get; } public JsonParseException(string message, int position) : base(message) { Position = position; } } internal static class MinimalJsonParser { internal static void WriteJsonString(StringBuilder sb, string value) { sb.Append('"'); if (value != null) { foreach (char c in value) { switch (c) { case '"': sb.Append("\\\""); continue; case '\\': sb.Append("\\\\"); continue; case '\n': sb.Append("\\n"); continue; case '\r': sb.Append("\\r"); continue; case '\t': sb.Append("\\t"); continue; case '\b': sb.Append("\\b"); continue; case '\f': sb.Append("\\f"); continue; } if (c < ' ' || c == '\u007f') { sb.Append("\\u"); int num = c; sb.Append(num.ToString("x4", CultureInfo.InvariantCulture)); } else { sb.Append(c); } } } sb.Append('"'); } internal static object Parse(string json) { if (json == null) { throw new JsonParseException("JSON input is null.", 0); } int pos = 0; object result = ParseValue(json, ref pos); SkipWhitespace(json, ref pos); if (pos < json.Length) { throw new JsonParseException($"Unexpected trailing content at position {pos}.", pos); } return result; } internal static void SkipWhitespace(string json, ref int pos) { while (pos < json.Length && char.IsWhiteSpace(json[pos])) { pos++; } } internal static object ParseValue(string json, ref int pos) { SkipWhitespace(json, ref pos); if (pos >= json.Length) { throw new JsonParseException("Unexpected end of input.", pos); } char c = json[pos]; switch (c) { case '"': return ParseString(json, ref pos); case '{': return ParseObject(json, ref pos); case '[': return ParseArray(json, ref pos); case 't': return ParseLiteral(json, ref pos, "true", true); case 'f': return ParseLiteral(json, ref pos, "false", false); case 'n': return ParseLiteral(json, ref pos, "null", null); default: if (!char.IsDigit(c)) { throw new JsonParseException($"Unexpected character '{c}' at position {pos}.", pos); } goto case '-'; case '-': return ParseNumber(json, ref pos); } } internal static Dictionary<string, object> ParseObject(string json, ref int pos) { SkipWhitespace(json, ref pos); if (pos >= json.Length || json[pos] != '{') { throw new JsonParseException("Expected '{' to start object.", pos); } pos++; Dictionary<string, object> dictionary = new Dictionary<string, object>(); SkipWhitespace(json, ref pos); if (pos < json.Length && json[pos] == '}') { pos++; return dictionary; } while (true) { SkipWhitespace(json, ref pos); string key = ParseString(json, ref pos); SkipWhitespace(json, ref pos); if (pos >= json.Length || json[pos] != ':') { throw new JsonParseException("Expected ':' after object key.", pos); } pos++; SkipWhitespace(json, ref pos); dictionary[key] = ParseValue(json, ref pos); SkipWhitespace(json, ref pos); if (pos >= json.Length || json[pos] != ',') { break; } pos++; } SkipWhitespace(json, ref pos); if (pos >= json.Length || json[pos] != '}') { throw new JsonParseException("Expected '}' to close object.", pos); } pos++; return dictionary; } internal static List<object> ParseArray(string json, ref int pos) { SkipWhitespace(json, ref pos); if (pos >= json.Length || json[pos] != '[') { throw new JsonParseException("Expected '[' to start array.", pos); } pos++; List<object> list = new List<object>(); SkipWhitespace(json, ref pos); if (pos < json.Length && json[pos] == ']') { pos++; return list; } while (true) { SkipWhitespace(json, ref pos); list.Add(ParseValue(json, ref pos)); SkipWhitespace(json, ref pos); if (pos >= json.Length || json[pos] != ',') { break; } pos++; } SkipWhitespace(json, ref pos); if (pos >= json.Length || json[pos] != ']') { throw new JsonParseException("Expected ']' to close array.", pos); } pos++; return list; } internal static string ParseString(string json, ref int pos) { SkipWhitespace(json, ref pos); if (pos >= json.Length || json[pos] != '"') { throw new JsonParseException("Expected '\"' to start string.", pos); } pos++; StringBuilder stringBuilder = new StringBuilder(); while (pos < json.Length) { char c = json[pos]; if (c == '\\' && pos + 1 < json.Length) { pos++; switch (json[pos]) { case '"': stringBuilder.Append('"'); break; case '\\': stringBuilder.Append('\\'); break; case '/': stringBuilder.Append('/'); break; case 'n': stringBuilder.Append('\n'); break; case 'r': stringBuilder.Append('\r'); break; case 't': stringBuilder.Append('\t'); break; case 'b': stringBuilder.Append('\b'); break; case 'f': stringBuilder.Append('\f'); break; case 'u': { if (pos + 4 >= json.Length) { throw new JsonParseException("Incomplete Unicode escape in string.", pos); } string text = json.Substring(pos + 1, 4); if (!ushort.TryParse(text, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var result)) { throw new JsonParseException("Invalid Unicode escape \\u" + text + ".", pos); } pos += 4; if (result >= 55296 && result <= 56319) { if (pos + 5 >= json.Length || json[pos + 1] != '\\' || json[pos + 2] != 'u') { throw new JsonParseException("Unpaired high surrogate in string.", pos); } if (!ushort.TryParse(json.Substring(pos + 3, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var result2) || result2 < 56320 || result2 > 57343) { throw new JsonParseException("Invalid low surrogate in string.", pos); } stringBuilder.Append(char.ConvertFromUtf32(char.ConvertToUtf32((char)result, (char)result2))); pos += 6; } else { stringBuilder.Append((char)result); } break; } default: throw new JsonParseException($"Invalid escape '\\{json[pos]}' in string.", pos); } pos++; } else { if (c == '"') { pos++; return stringBuilder.ToString(); } if (c < ' ') { throw new JsonParseException($"Unescaped control character U+{(int)c:X4} in string.", pos); } stringBuilder.Append(c); pos++; } } throw new JsonParseException("Unterminated string.", pos); } internal static object ParseNumber(string json, ref int pos) { int num = pos; bool flag = false; if (pos < json.Length && json[pos] == '-') { pos++; } if (pos >= json.Length || !char.IsDigit(json[pos])) { throw new JsonParseException("Invalid number.", num); } while (pos < json.Length && char.IsDigit(json[pos])) { pos++; } if (pos < json.Length && json[pos] == '.') { flag = true; pos++; if (pos >= json.Length || !char.IsDigit(json[pos])) { throw new JsonParseException("Invalid number fraction.", num); } while (pos < json.Length && char.IsDigit(json[pos])) { pos++; } } if (pos < json.Length && (json[pos] == 'e' || json[pos] == 'E')) { flag = true; pos++; if (pos < json.Length && (json[pos] == '+' || json[pos] == '-')) { pos++; } if (pos >= json.Length || !char.IsDigit(json[pos])) { throw new JsonParseException("Invalid number exponent.", num); } while (pos < json.Length && char.IsDigit(json[pos])) { pos++; } } string text = json.Substring(num, pos - num); if (flag) { if (double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { return result; } throw new JsonParseException("Invalid floating-point number '" + text + "'.", num); } if (long.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result2)) { return result2; } throw new JsonParseException("Invalid integer '" + text + "'.", num); } internal static object ParseLiteral(string json, ref int pos, string literal, object result) { if (pos + literal.Length <= json.Length && string.CompareOrdinal(json, pos, literal, 0, literal.Length) == 0) { pos += literal.Length; return result; } throw new JsonParseException("Expected literal '" + literal + "'.", pos); } internal static int ToInt(object val) { if (val is long num) { return (int)num; } if (val is double num2) { return (int)num2; } if (val is int) { return (int)val; } return 0; } } } namespace HavensMirror { [BepInPlugin("com.azraelgodking.havensmirror", "Haven's Mirror", "1.0.3")] 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.3", "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.3", Log, delegate(VersionChecker.VersionCheckResult result) { result.NotifyUpdateAvailable(Log); }); Log.LogInfo((object)("Haven's Mirror v1.0.3 loaded. Gallery: " + GalleryPaths.GalleryRoot)); ModDiagnostics.LogModStartup(Log, "com.azraelgodking.havensmirror", "Haven's Mirror", "1.0.3", 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); } } public static void EnsureGalleryCurrent() { if (Shelf == null) { ReloadGallery(notify: false); return; } string text = TryReadCharacterName(); if (!string.IsNullOrWhiteSpace(text) && Shelf.IsStaleFor(text)) { ManualLogSource log = Log; if (log != null) { log.LogInfo((object)("[Gallery] Active character is now '" + (text ?? "(none)") + "' — reloading portraits.")); } ReloadGallery(notify: false); } } 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.3"; } } 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)) { 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 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)) { return; } ConfigEntry<KeyCode> reloadModifier = MirrorOptions.ReloadModifier; KeyCode val2 = (KeyCode)((reloadModifier != null) ? ((int)reloadModifier.Value) : 0); if ((int)val2 == 0 || IsModifierHeld(val2)) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)"[Gallery] Reload hotkey pressed."); } Plugin.ReloadGallery(MirrorOptions.NotifyOnReload?.Value ?? true); } } private static bool IsModifierHeld(KeyCode modifier) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Expected I4, but got Unknown if (Input.GetKey(modifier)) { return true; } return (modifier - 303) switch { 3 => Input.GetKey((KeyCode)305), 2 => Input.GetKey((KeyCode)306), 1 => Input.GetKey((KeyCode)303), 0 => Input.GetKey((KeyCode)304), 5 => Input.GetKey((KeyCode)307), 4 => Input.GetKey((KeyCode)308), 7 => Input.GetKey((KeyCode)309), 6 => Input.GetKey((KeyCode)310), _ => false, }; } 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 { private static readonly string[] ImageExtensions = new string[3] { ".png", ".jpg", ".jpeg" }; private const int MaxNestedDepth = 3; 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 List<string> ListImageFiles(string folder) { List<string> list = new List<string>(); if (string.IsNullOrEmpty(folder) || !Directory.Exists(folder)) { return list; } try { string[] files = Directory.GetFiles(folder); foreach (string text in files) { if (HasImageExtension(text)) { list.Add(text); } } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[Gallery] Could not list '" + folder + "': " + ex.Message)); } } list.Sort(StringComparer.OrdinalIgnoreCase); return list; } public static string FindSlotFile(List<string> files, LookSlot slot) { if (files == null) { return null; } string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(FileNameFor(slot)); foreach (string file in files) { if (string.Equals(StripImageExtensions(Path.GetFileName(file)), fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) { return file; } } return null; } public static string StripImageExtensions(string fileName) { if (string.IsNullOrEmpty(fileName)) { return fileName; } string text = fileName.Trim(); bool flag = true; while (flag) { flag = false; string[] imageExtensions = ImageExtensions; foreach (string text2 in imageExtensions) { if (text.Length > text2.Length && text.EndsWith(text2, StringComparison.OrdinalIgnoreCase)) { text = text.Substring(0, text.Length - text2.Length).TrimEnd(Array.Empty<char>()); flag = true; } } } return text; } private static bool HasImageExtension(string path) { string[] imageExtensions = ImageExtensions; foreach (string value in imageExtensions) { if (path.EndsWith(value, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } public static bool FolderHasPortraitFiles(string folder) { return ListImageFiles(folder).Count > 0; } public static List<string> ListImageFilesDeep(string folder) { List<string> list = ListImageFiles(folder); if (list.Count > 0 || string.IsNullOrEmpty(folder) || !Directory.Exists(folder)) { return list; } List<string> list2 = new List<string> { folder }; for (int i = 1; i <= 3; i++) { if (list2.Count <= 0) { break; } List<string> list3 = new List<string>(); foreach (string item in list2) { list3.AddRange(ListSubdirectories(item)); } foreach (string item2 in list3) { List<string> list4 = ListImageFiles(item2); if (list4.Count > 0) { return list4; } } list2 = list3; } return list; } public static string ResolveCharacterFolder(string characterName) { string text = CharacterFolder(characterName); if (string.IsNullOrWhiteSpace(characterName) || Directory.Exists(text)) { return text; } return FindLooseSubfolder(GalleryRoot, characterName, SanitizeFolderName(characterName)) ?? text; } public static string ResolveSharedFolder() { string sharedFolder = SharedFolder; if (Directory.Exists(sharedFolder)) { return sharedFolder; } return FindLooseSubfolder(GalleryRoot, SharedFolderName, "_shared", "shared") ?? sharedFolder; } private static string FindLooseSubfolder(string parent, params string[] names) { HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); for (int i = 0; i < names.Length; i++) { string text = LooseKey(names[i]); if (text.Length > 0) { hashSet.Add(text); } } if (hashSet.Count == 0) { return null; } foreach (string item in ListSubdirectories(parent)) { if (hashSet.Contains(LooseKey(Path.GetFileName(item)))) { return item; } } return null; } private static string LooseKey(string name) { if (string.IsNullOrEmpty(name)) { return string.Empty; } StringBuilder stringBuilder = new StringBuilder(name.Length); foreach (char c in name) { if (!char.IsWhiteSpace(c) && c != '_' && c != '-') { stringBuilder.Append(char.ToLowerInvariant(c)); } } return stringBuilder.ToString(); } private static List<string> ListSubdirectories(string folder) { List<string> list = new List<string>(); if (string.IsNullOrEmpty(folder) || !Directory.Exists(folder)) { return list; } try { list.AddRange(Directory.GetDirectories(folder)); } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("[Gallery] Could not list subfolders of '" + folder + "': " + ex.Message)); } } list.Sort(StringComparer.OrdinalIgnoreCase); return list; } 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 gallery/ (last resort: images placed directly here)\nImages inside a subfolder of these folders are found too.\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 image in that folder.\nIf no file uses one of these names, the first image in the folder is used for every look.\nAny image size works (it is scaled to the NPC portrait height). .jpg also works.\nTip: Windows may hide extensions, so 'spring.png' can really be 'spring.png.png'. That is fine too.\n\nReload\n------\nAfter adding or changing images, press Ctrl+F8 in game (either Ctrl key; configurable).\nNo restart needed. On some laptops F8 needs the Fn key.\n\nStill nothing? Open BepInEx/LogOutput.log and search for [Gallery]. It lists the exact\nfolders the mod checked and the files it found there.\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; private string _loadedForCharacter; private bool _hasLoaded; public bool HasAny => _sprites.Count > 0; public string LoadedFrom => _loadedFrom; public bool IsStaleFor(string characterName) { if (_hasLoaded) { return !string.Equals(_loadedForCharacter ?? string.Empty, characterName ?? string.Empty, StringComparison.Ordinal); } return true; } 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; _hasLoaded = false; _loadedForCharacter = null; } public void Dispose() { Clear(); } public string LoadForCharacter(string characterName) { Clear(); _hasLoaded = true; _loadedForCharacter = characterName; List<string> list = new List<string>(); if (!string.IsNullOrWhiteSpace(characterName)) { list.Add(GalleryPaths.ResolveCharacterFolder(characterName)); } string item = GalleryPaths.ResolveSharedFolder(); if (!list.Contains(item)) { list.Add(item); } list.Add(GalleryPaths.GalleryRoot); List<string> list2 = new List<string>(); foreach (string item2 in list) { List<string> list3 = (string.Equals(item2, GalleryPaths.GalleryRoot, StringComparison.OrdinalIgnoreCase) ? GalleryPaths.ListImageFiles(item2) : GalleryPaths.ListImageFilesDeep(item2)); list2.Add(DescribeFolder(item2, list3)); if (list3.Count == 0) { continue; } Sprite val = null; foreach (LookSlot value in Enum.GetValues(typeof(LookSlot))) { string text = GalleryPaths.FindSlotFile(list3, value); if (text == null) { continue; } Sprite val2 = TryLoadImage(text); if (!((Object)(object)val2 == (Object)null)) { _sprites[value] = val2; if ((Object)(object)val == (Object)null) { val = val2; } } } if ((Object)(object)val == (Object)null) { foreach (string item3 in list3) { val = TryLoadImage(item3); if (!((Object)(object)val == (Object)null)) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)("[Gallery] No file named spring/summer/autumn/winter/vows/shore/costume in '" + item2 + "'; using '" + Path.GetFileName(item3) + "' for every look.")); } break; } } } if ((Object)(object)val == (Object)null) { continue; } foreach (LookSlot value2 in Enum.GetValues(typeof(LookSlot))) { if (!_sprites.ContainsKey(value2)) { _sprites[value2] = val; } } _loadedFrom = item2; string text2 = ((list3.Count > 0) ? Path.GetDirectoryName(list3[0]) : item2); return string.Equals(text2, item2, StringComparison.OrdinalIgnoreCase) ? ("Loaded portraits from '" + item2 + "'.") : ("Loaded portraits from '" + text2 + "' (subfolder of '" + item2 + "')."); } ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogInfo((object)("[Gallery] No portrait images loaded for character '" + (characterName ?? "(none yet)") + "'. Checked: " + string.Join("; ", list2))); } return "No portrait images yet (drop a PNG in gallery/<name>/ or gallery/_shared/, then press Ctrl+F8)."; } private static string DescribeFolder(string folder, List<string> files) { if (!Directory.Exists(folder)) { return "'" + folder + "' (missing)"; } if (files.Count == 0) { return "'" + folder + "' (no .png/.jpg files)"; } List<string> list = new List<string>(); foreach (string file in files) { list.Add(Path.GetFileName(file)); } return "'" + folder + "' (" + string.Join(", ", list.ToArray()) + ")"; } public Sprite Get(LookSlot slot) { if (!_sprites.TryGetValue(slot, out Sprite value)) { return null; } return value; } private Sprite TryLoadImage(string path) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Expected O, but got Unknown //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: 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] Could not decode '" + path + "'. It must be a real PNG or JPG (a .webp/.avif renamed to .png will not load). Re-export or re-save it as PNG.")); } 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; }