Decompiled source of GiftingAssistant v1.1.4
GiftingAssistant.dll
Decompiled a month 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.Linq.Expressions; 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 GiftingAssistant.Config; using GiftingAssistant.Data; using GiftingAssistant.Game; using GiftingAssistant.Integration; using GiftingAssistant.Patches; using GiftingAssistant.UI; using HarmonyLib; using I2.Loc; using Microsoft.CodeAnalysis; using SunhavenMods.Shared; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Events; using UnityEngine.Networking; using UnityEngine.SceneManagement; using UnityEngine.UI; using Wish; [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("GiftingAssistant")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+cebd1eefe0cae3e03fc87d19b7008bafe2135ad8")] [assembly: AssemblyProduct("GiftingAssistant")] [assembly: AssemblyTitle("GiftingAssistant")] [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.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 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.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 { 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 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 static void ResetReportedSitesForTests() { lock (Lock) { Reported.Clear(); } } } 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 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 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) { return TryCompareVersions(v1, v2).GetValueOrDefault(); } 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 TodoDraft { public string Title { get; set; } public string Description { get; set; } = ""; public string Priority { get; set; } = "Normal"; public string Category { get; set; } = "General"; public int IconItemId { get; set; } public string MuseumDestination { get; set; } } public sealed class TodoSnapshot { public string Id { get; } public string Title { get; } public string Description { get; } public bool IsCompleted { get; } public string Priority { get; } public TodoSnapshot(string id, string title, string description, bool isCompleted, string priority) { Id = id; Title = title ?? ""; Description = description ?? ""; IsCompleted = isCompleted; Priority = priority ?? ""; } } public sealed class TodoManagerReflection { public const string TodoItemTypeName = "SunhavenTodo.Data.TodoItem"; public const string TodoPriorityTypeName = "SunhavenTodo.Data.TodoPriority"; public const string TodoCategoryTypeName = "SunhavenTodo.Data.TodoCategory"; private readonly Type _itemType; private readonly Type _priorityType; private readonly Type _categoryType; private readonly MethodInfo _addTodo; private readonly MethodInfo _removeTodo; private readonly MethodInfo _toggleComplete; private readonly MethodInfo _getTodoById; private readonly MethodInfo _getAllTodos; private readonly MethodInfo _getActiveTodos; private readonly MethodInfo _getData; private readonly PropertyInfo _currentCharacter; private readonly PropertyInfo _id; private readonly PropertyInfo _title; private readonly PropertyInfo _description; private readonly PropertyInfo _isCompleted; private readonly PropertyInfo _priority; private readonly PropertyInfo _category; private readonly PropertyInfo _iconItemId; private readonly PropertyInfo _museumDestination; public object Manager { get; } public string CurrentCharacter => _currentCharacter.GetValue(Manager, null) as string; public bool HasData { get { if (!string.IsNullOrEmpty(CurrentCharacter)) { return _getData.Invoke(Manager, null) != null; } return false; } } private TodoManagerReflection(object manager, Type itemType, Type priorityType, Type categoryType) { Manager = manager; _itemType = itemType; _priorityType = priorityType; _categoryType = categoryType; Type type = manager.GetType(); _addTodo = Method(type, "AddTodo", itemType); _removeTodo = Method(type, "RemoveTodo", typeof(string)); _toggleComplete = Method(type, "ToggleComplete", typeof(string)); _getTodoById = Method(type, "GetTodoById", typeof(string)); _getAllTodos = Method(type, "GetAllTodos"); _getActiveTodos = Method(type, "GetActiveTodos"); _getData = Method(type, "GetData"); _currentCharacter = type.GetProperty("CurrentCharacter", BindingFlags.Instance | BindingFlags.Public); _id = Property(itemType, "Id"); _title = Property(itemType, "Title"); _description = Property(itemType, "Description"); _isCompleted = Property(itemType, "IsCompleted"); _priority = Property(itemType, "Priority"); _category = Property(itemType, "Category"); _iconItemId = Property(itemType, "IconItemId"); _museumDestination = Property(itemType, "MuseumDestination"); } public static TodoManagerReflection TryBind(object manager, out string error) { error = null; if (manager == null) { error = "TodoManager is null"; return null; } Assembly assembly = manager.GetType().Assembly; Type type = assembly.GetType("SunhavenTodo.Data.TodoItem"); Type type2 = assembly.GetType("SunhavenTodo.Data.TodoPriority"); Type type3 = assembly.GetType("SunhavenTodo.Data.TodoCategory"); if (type == null || type2 == null || type3 == null) { error = "Todo data types not found"; return null; } TodoManagerReflection todoManagerReflection = new TodoManagerReflection(manager, type, type2, type3); string text = todoManagerReflection.FirstMissingMember(); if (text != null) { error = "Todo member not found: " + text; return null; } return todoManagerReflection; } private string FirstMissingMember() { if (_addTodo == null) { return "TodoManager.AddTodo(TodoItem)"; } if (_removeTodo == null) { return "TodoManager.RemoveTodo(string)"; } if (_toggleComplete == null) { return "TodoManager.ToggleComplete(string)"; } if (_getTodoById == null) { return "TodoManager.GetTodoById(string)"; } if (_getAllTodos == null) { return "TodoManager.GetAllTodos()"; } if (_getActiveTodos == null) { return "TodoManager.GetActiveTodos()"; } if (_getData == null) { return "TodoManager.GetData()"; } if (_currentCharacter == null) { return "TodoManager.CurrentCharacter"; } if (_id == null) { return "TodoItem.Id"; } if (_title == null) { return "TodoItem.Title"; } if (_description == null) { return "TodoItem.Description"; } if (_isCompleted == null) { return "TodoItem.IsCompleted"; } if (_priority == null) { return "TodoItem.Priority"; } if (_category == null) { return "TodoItem.Category"; } if (_iconItemId == null) { return "TodoItem.IconItemId"; } if (_museumDestination == null) { return "TodoItem.MuseumDestination"; } if (_itemType.GetConstructor(Type.EmptyTypes) == null) { return "TodoItem()"; } return null; } public string AddTodo(TodoDraft draft) { if (draft == null || string.IsNullOrWhiteSpace(draft.Title)) { throw new ArgumentException("Todo title is required", "draft"); } object obj = Activator.CreateInstance(_itemType); _title.SetValue(obj, draft.Title, null); _description.SetValue(obj, draft.Description ?? "", null); _priority.SetValue(obj, Enum.Parse(_priorityType, draft.Priority ?? "Normal"), null); _category.SetValue(obj, Enum.Parse(_categoryType, draft.Category ?? "General"), null); if (draft.IconItemId > 0) { _iconItemId.SetValue(obj, draft.IconItemId, null); } if (draft.MuseumDestination != null) { _museumDestination.SetValue(obj, draft.MuseumDestination, null); } _addTodo.Invoke(Manager, new object[1] { obj }); return _id.GetValue(obj, null) as string; } public bool RemoveTodo(string id) { if (string.IsNullOrEmpty(id) || GetItem(id) == null) { return false; } _removeTodo.Invoke(Manager, new object[1] { id }); return true; } public bool? IsCompleted(string id) { object item = GetItem(id); if (item == null) { return null; } object value = _isCompleted.GetValue(item, null); bool flag = default(bool); int num; if (value is bool) { flag = (bool)value; num = 1; } else { num = 0; } return (byte)((uint)num & (flag ? 1u : 0u)) != 0; } public bool SetCompleted(string id, bool completed) { bool? flag = IsCompleted(id); if (!flag.HasValue || flag.Value == completed) { return false; } _toggleComplete.Invoke(Manager, new object[1] { id }); return true; } public TodoSnapshot GetById(string id) { object item = GetItem(id); if (item != null) { return Snapshot(item); } return null; } public List<TodoSnapshot> GetTodos(bool activeOnly) { List<TodoSnapshot> list = new List<TodoSnapshot>(); if ((activeOnly ? _getActiveTodos : _getAllTodos).Invoke(Manager, null) is IEnumerable enumerable) { foreach (object item in enumerable) { if (item != null) { list.Add(Snapshot(item)); } } } return list; } private object GetItem(string id) { if (string.IsNullOrEmpty(id)) { return null; } return _getTodoById.Invoke(Manager, new object[1] { id }); } private TodoSnapshot Snapshot(object item) { string id = _id.GetValue(item, null) as string; string title = _title.GetValue(item, null) as string; string description = _description.GetValue(item, null) as string; object value = _isCompleted.GetValue(item, null); bool flag = default(bool); int num; if (value is bool) { flag = (bool)value; num = 1; } else { num = 0; } return new TodoSnapshot(id, title, description, (byte)((uint)num & (flag ? 1u : 0u)) != 0, _priority.GetValue(item, null)?.ToString()); } private static MethodInfo Method(Type type, string name, params Type[] parameters) { return type.GetMethod(name, BindingFlags.Instance | BindingFlags.Public, null, parameters, null); } private static PropertyInfo Property(Type type, string name) { return type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public); } } public static class TodoSoftClient { public const string PluginGuid = "com.azraelgodking.sunhaventodo"; private static TodoManagerReflection _bound; private static readonly HashSet<string> Warned = new HashSet<string>(StringComparer.Ordinal); public static bool IsAvailable { get { if (Chainloader.PluginInfos != null) { return Chainloader.PluginInfos.ContainsKey("com.azraelgodking.sunhaventodo"); } return false; } } public static TodoManagerReflection TryGetManager(ManualLogSource log = null) { Type type = GetPluginInstance()?.GetType(); if (type == null) { return null; } object obj; try { obj = type.GetMethod("GetTodoManager", BindingFlags.Static | BindingFlags.Public, null, Type.EmptyTypes, null)?.Invoke(null, null); } catch (Exception ex) { Warn("manager", "GetTodoManager failed: " + ex.Message, log); return null; } if (obj == null) { if (Warned.Add("manager-null") && log != null) { log.LogDebug((object)"[TodoSoftClient] TodoManager not created yet"); } return null; } Warned.Remove("manager-null"); Warned.Remove("manager"); if (_bound != null && _bound.Manager == obj) { return _bound; } _bound = TodoManagerReflection.TryBind(obj, out string error); if (_bound == null) { Warn("bind", "Sun Haven Todo API changed, integration disabled (" + error + ")", log); } else { Warned.Remove("bind"); } return _bound; } public static TodoManagerReflection TryGetManagerForCharacter(string characterName, ManualLogSource log = null) { TodoManagerReflection todoManagerReflection = TryGetManager(log); if (todoManagerReflection == null) { return null; } try { if (IsLoadedFor(todoManagerReflection, characterName)) { Warned.Remove("data"); return todoManagerReflection; } if (string.IsNullOrEmpty(characterName)) { Warn("data", "no character loaded in Sun Haven Todo yet", log); return null; } object pluginInstance = GetPluginInstance(); MethodInfo methodInfo = pluginInstance?.GetType().GetMethod("LoadDataForCharacter", BindingFlags.Instance | BindingFlags.Public, null, new Type[1] { typeof(string) }, null); if (methodInfo == null) { Warn("data", "Sun Haven Todo data not loaded and LoadDataForCharacter is unavailable", log); return null; } methodInfo.Invoke(pluginInstance, new object[1] { characterName }); if (!IsLoadedFor(todoManagerReflection, characterName)) { Warn("data", "failed to load todo data for character \"" + characterName + "\"", log); return null; } Warned.Remove("data"); return todoManagerReflection; } catch (Exception ex) { Warn("data", "loading todo data failed: " + ex.Message, log); return null; } } public static void Save(ManualLogSource log = null) { try { GetPluginInstance()?.GetType().GetMethod("SaveData", BindingFlags.Static | BindingFlags.Public, null, Type.EmptyTypes, null)?.Invoke(null, null); } catch (Exception ex) { if (log != null) { log.LogWarning((object)("[TodoSoftClient] SaveData failed: " + ex.Message)); } } } public static bool TryAddTodo(string title, string description, string priorityName = "Normal", string categoryName = "General", ManualLogSource log = null) { if (!IsAvailable || string.IsNullOrWhiteSpace(title)) { return false; } try { TodoManagerReflection todoManagerReflection = TryGetManager(log); if (todoManagerReflection == null || !todoManagerReflection.HasData) { return false; } todoManagerReflection.AddTodo(new TodoDraft { Title = title, Description = (description ?? ""), Priority = priorityName, Category = categoryName }); return true; } catch (Exception ex) { if (log != null) { log.LogDebug((object)("[TodoSoftClient] AddTodo failed: " + ex.Message)); } return false; } } private static bool IsLoadedFor(TodoManagerReflection todo, string characterName) { if (!todo.HasData) { return false; } if (!string.IsNullOrEmpty(characterName)) { return string.Equals(todo.CurrentCharacter, characterName, StringComparison.OrdinalIgnoreCase); } return true; } private static object GetPluginInstance() { if (Chainloader.PluginInfos == null || !Chainloader.PluginInfos.TryGetValue("com.azraelgodking.sunhaventodo", out var value) || value == null) { return null; } return value.Instance; } private static void Warn(string key, string message, ManualLogSource log) { if (Warned.Add(key) && log != null) { log.LogWarning((object)("[TodoSoftClient] " + message)); } } } 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 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 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; } } public static class ItemSearch { private static readonly ManualLogSource _log = Logger.CreateLogSource("ItemSearch"); private static object _dbInstance; private static FieldInfo _dictField; public static string FormatDisplay(string name, int itemId) { if (string.IsNullOrEmpty(name)) { return $"#{itemId}"; } return $"{name} (#{itemId})"; } public static List<KeyValuePair<int, string>> SearchItems(string query, int maxResults = 50) { List<KeyValuePair<int, string>> list = new List<KeyValuePair<int, string>>(); if (string.IsNullOrEmpty(query) || query.Trim().Length < 2) { return list; } try { Dictionary<int, ItemSellInfo> itemDictionary = GetItemDictionary(); if (itemDictionary == null) { return list; } string text = query.Trim().ToLowerInvariant(); int result; bool flag = int.TryParse(query.Trim(), out result); List<KeyValuePair<int, string>> list2 = new List<KeyValuePair<int, string>>(); List<KeyValuePair<int, string>> list3 = new List<KeyValuePair<int, string>>(); List<KeyValuePair<int, string>> list4 = new List<KeyValuePair<int, string>>(); foreach (KeyValuePair<int, ItemSellInfo> item2 in itemDictionary) { int key = item2.Key; string text2 = item2.Value?.name; if (!string.IsNullOrEmpty(text2)) { KeyValuePair<int, string> item = new KeyValuePair<int, string>(key, text2); string text3 = text2.ToLowerInvariant(); if (flag && key == result) { list2.Add(item); } else if (text3 == text) { list2.Add(item); } else if (text3.StartsWith(text)) { list3.Add(item); } else if (text3.Contains(text)) { list4.Add(item); } else if (flag && key.ToString().Contains(query.Trim())) { list4.Add(item); } } } list3.Sort((KeyValuePair<int, string> a, KeyValuePair<int, string> b) => string.Compare(a.Value, b.Value, StringComparison.OrdinalIgnoreCase)); list4.Sort((KeyValuePair<int, string> a, KeyValuePair<int, string> b) => string.Compare(a.Value, b.Value, StringComparison.OrdinalIgnoreCase)); list.AddRange(list2); list.AddRange(list3); list.AddRange(list4); if (list.Count > maxResults) { list.RemoveRange(maxResults, list.Count - maxResults); } } catch (Exception ex) { ManualLogSource log = _log; if (log != null) { log.LogDebug((object)("[ItemSearch] SearchItems error: " + ex.Message)); } } return list; } public static string GetItemName(int itemId) { try { Dictionary<int, ItemSellInfo> itemDictionary = GetItemDictionary(); if (itemDictionary == null || !itemDictionary.ContainsKey(itemId)) { return null; } return itemDictionary[itemId]?.name; } catch (Exception ex) { ManualLogSource log = _log; if (log != null) { log.LogDebug((object)$"[ItemSearch] GetItemName({itemId}): {ex.Message}"); } return null; } } public static ItemSellInfo GetItemSellInfo(int itemId) { try { Dictionary<int, ItemSellInfo> itemDictionary = GetItemDictionary(); if (itemDictionary != null && itemDictionary.TryGetValue(itemId, out var value)) { return value; } } catch (Exception ex) { ManualLogSource log = _log; if (log != null) { log.LogDebug((object)$"[ItemSearch] GetItemSellInfo({itemId}): {ex.Message}"); } } return null; } public static List<KeyValuePair<int, string>> GetAllItems() { List<KeyValuePair<int, string>> list = new List<KeyValuePair<int, string>>(); try { Dictionary<int, ItemSellInfo> itemDictionary = GetItemDictionary(); if (itemDictionary == null) { return list; } foreach (KeyValuePair<int, ItemSellInfo> item in itemDictionary) { string value = item.Value?.name; if (!string.IsNullOrEmpty(value)) { list.Add(new KeyValuePair<int, string>(item.Key, value)); } } list.Sort((KeyValuePair<int, string> a, KeyValuePair<int, string> b) => string.Compare(a.Value, b.Value, StringComparison.OrdinalIgnoreCase)); } catch (Exception ex) { ManualLogSource log = _log; if (log != null) { log.LogDebug((object)("[ItemSearch] GetAllItems: " + ex.Message)); } } return list; } private static Dictionary<int, ItemSellInfo> GetItemDictionary() { try { if (_dbInstance != null) { object dbInstance = _dbInstance; Object val = (Object)((dbInstance is Object) ? dbInstance : null); if (val == null || !(val == (Object)null)) { goto IL_005b; } } _dbInstance = null; _dictField = null; _dbInstance = GetSingletonInstance("Wish.ItemInfoDatabase"); if (_dbInstance != null) { _dictField = _dbInstance.GetType().GetField("allItemSellInfos", BindingFlags.Instance | BindingFlags.Public); } goto IL_005b; IL_005b: if (_dbInstance == null || _dictField == null) { return null; } return _dictField.GetValue(_dbInstance) as Dictionary<int, ItemSellInfo>; } catch (Exception ex) { ManualLogSource log = _log; if (log != null) { log.LogDebug((object)("[ItemSearch] GetItemDictionary: " + ex.Message)); } return null; } } private static object GetSingletonInstance(string typeName) { try { Type type = AccessTools.TypeByName("Wish.SingletonBehaviour`1"); if (type == null) { return null; } Type type2 = AccessTools.TypeByName(typeName); if (type2 == null) { return null; } return type.MakeGenericType(type2).GetProperty("Instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy)?.GetValue(null); } catch (Exception ex) { ManualLogSource log = _log; if (log != null) { log.LogDebug((object)("[ItemSearch] GetSingletonInstance(" + typeName + "): " + ex.Message)); } return null; } } } public static class IconCache { private struct CachedIcon { public Texture2D Texture; public bool OwnsTexture; } private static readonly Dictionary<int, CachedIcon> _iconCache = new Dictionary<int, CachedIcon>(); private const int MaxCacheSize = 200; private static readonly HashSet<int> _loadingItems = new HashSet<int>(); private static readonly HashSet<int> _failedItems = new HashSet<int>(); private static readonly Dictionary<string, int> _currencyToItemId = new Dictionary<string, int>(); private static Texture2D _fallbackTexture; private static ManualLogSource _log; private static Type _databaseType; private static Type _itemDataType; private static MethodInfo _getDataMethod; private static bool _reflectionInitialized; private static bool _initialized; private static bool _iconsLoaded; private static int[] _pendingPreloadItemIds; public static void Initialize(ManualLogSource log, int[] preloadItemIds = null) { _log = log; if (_initialized) { ManualLogSource log2 = _log; if (log2 != null) { log2.LogDebug((object)"[IconCache] Already initialized"); } return; } _initialized = true; ManualLogSource log3 = _log; if (log3 != null) { log3.LogInfo((object)"[IconCache] Initializing icon cache..."); } _fallbackTexture = CreateFallbackTexture(); ManualLogSource log4 = _log; if (log4 != null) { log4.LogInfo((object)"[IconCache] Created fallback texture"); } if (preloadItemIds != null && preloadItemIds.Length != 0) { _pendingPreloadItemIds = (int[])preloadItemIds.Clone(); ManualLogSource log5 = _log; if (log5 != null) { log5.LogInfo((object)$"[IconCache] Preload: {_pendingPreloadItemIds.Length} item ID(s) will queue with LoadAllIcons"); } } } public static void RegisterCurrency(string currencyId, int itemId) { _currencyToItemId[currencyId] = itemId; } public static void LoadAllIcons() { if (_iconsLoaded) { ManualLogSource log = _log; if (log != null) { log.LogDebug((object)"[IconCache] Icons already loaded, skipping"); } return; } if (!InitializeReflection()) { ManualLogSource log2 = _log; if (log2 != null) { log2.LogError((object)"[IconCache] Failed to initialize reflection, cannot load icons"); } _iconsLoaded = true; return; } foreach (KeyValuePair<string, int> item in _currencyToItemId) { ManualLogSource log3 = _log; if (log3 != null) { log3.LogDebug((object)$"[IconCache] Queuing load for: {item.Key} (ItemID: {item.Value})"); } LoadIcon(item.Value); } if (_pendingPreloadItemIds != null) { int[] pendingPreloadItemIds = _pendingPreloadItemIds; foreach (int num in pendingPreloadItemIds) { if (num > 0) { ManualLogSource log4 = _log; if (log4 != null) { log4.LogDebug((object)$"[IconCache] Queuing preload item ID: {num}"); } LoadIcon(num); } } } _iconsLoaded = true; ManualLogSource log5 = _log; if (log5 != null) { log5.LogInfo((object)$"[IconCache] Queued {_currencyToItemId.Count} currency icon(s); preload queue processed."); } } public static Texture2D GetIconForCurrency(string currencyId) { if (_currencyToItemId.TryGetValue(currencyId, out var value)) { return GetIcon(value); } return GetFallbackTexture(); } public static Texture2D GetIcon(int itemId) { if (itemId <= 0) { return GetFallbackTexture(); } if (_iconCache.TryGetValue(itemId, out var value)) { return value.Texture; } if (!_loadingItems.Contains(itemId) && !_failedItems.Contains(itemId)) { LoadIcon(itemId); } return GetFallbackTexture(); } private static Texture2D GetFallbackTexture() { if ((Object)(object)_fallbackTexture == (Object)null) { _fallbackTexture = CreateFallbackTexture(); } return _fallbackTexture; } public static bool IsIconLoaded(int itemId) { return _iconCache.ContainsKey(itemId); } public static bool IsIconLoaded(string currencyId) { if (_currencyToItemId.TryGetValue(currencyId, out var value)) { return IsIconLoaded(value); } return false; } public static int GetItemIdForCurrency(string currencyId) { if (!_currencyToItemId.TryGetValue(currencyId, out var value)) { return -1; } return value; } private static bool InitializeReflection() { if (_reflectionInitialized) { if (_databaseType != null && _itemDataType != null) { return _getDataMethod != null; } return false; } _reflectionInitialized = true; try { string[] array = new string[4] { "Database", "Wish.Database", "PSS.Database", "SunHaven.Database" }; for (int i = 0; i < array.Length; i++) { _databaseType = AccessTools.TypeByName(array[i]); if (_databaseType != null) { ManualLogSource log = _log; if (log != null) { log.LogInfo((object)("[IconCache] Found Database type: " + _databaseType.FullName)); } break; } } MethodInfo[] methods; if (_databaseType == null) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { try { Type[] types = assembly.GetTypes(); foreach (Type type in types) { if (!(type.Name == "Database") || type.IsNested) { continue; } methods = type.GetMethods(BindingFlags.Static | BindingFlags.Public); foreach (MethodInfo methodInfo in methods) { if (methodInfo.Name == "GetData" && methodInfo.IsGenericMethod) { _databaseType = type; ManualLogSource log2 = _log; if (log2 != null) { log2.LogInfo((object)("[IconCache] Found Database type: " + type.FullName)); } break; } } if (_databaseType != null) { break; } } if (_databaseType != null) { break; } } catch (Exception ex) { ManualLogSource log3 = _log; if (log3 != null) { log3.LogDebug((object)("[IconCache] Skipping assembly " + assembly.GetName().Name + ": " + ex.Message)); } } } } if (_databaseType == null) { ManualLogSource log4 = _log; if (log4 != null) { log4.LogError((object)"[IconCache] Could not find Database type"); } return false; } _itemDataType = AccessTools.TypeByName("Wish.ItemData"); if (_itemDataType == null) { ManualLogSource log5 = _log; if (log5 != null) { log5.LogError((object)"[IconCache] Could not find Wish.ItemData type"); } return false; } methods = _databaseType.GetMethods(BindingFlags.Static | BindingFlags.Public); foreach (MethodInfo methodInfo2 in methods) { if (!(methodInfo2.Name == "GetData") || !methodInfo2.IsGenericMethod) { continue; } ParameterInfo[] parameters = methodInfo2.GetParameters(); if (methodInfo2.GetGenericArguments().Length == 1 && parameters.Length == 3 && parameters[0].ParameterType == typeof(int)) { _getDataMethod = methodInfo2.MakeGenericMethod(_itemDataType); ManualLogSource log6 = _log; if (log6 != null) { log6.LogInfo((object)"[IconCache] Found Database.GetData method"); } break; } } if (_getDataMethod == null) { ManualLogSource log7 = _log; if (log7 != null) { log7.LogError((object)"[IconCache] Could not find Database.GetData method"); } return false; } return true; } catch (Exception ex2) { ManualLogSource log8 = _log; if (log8 != null) { log8.LogError((object)("[IconCache] Error initializing reflection: " + ex2.Message)); } return false; } } private static void LoadIcon(int itemId) { if (itemId <= 0 || _loadingItems.Contains(itemId) || _iconCache.ContainsKey(itemId)) { return; } _loadingItems.Add(itemId); try { if (!InitializeReflection() || _getDataMethod == null) { _failedItems.Add(itemId); _loadingItems.Remove(itemId); return; } Type delegateType = typeof(Action<>).MakeGenericType(_itemDataType); ParameterExpression parameterExpression = Expression.Parameter(_itemDataType, "itemData"); ConstantExpression arg = Expression.Constant(itemId); MethodCallExpression body = Expression.Call(typeof(IconCache).GetMethod("OnIconLoadedInternal", BindingFlags.Static | BindingFlags.NonPublic), arg, Expression.Convert(parameterExpression, typeof(object))); Delegate obj = Expression.Lambda(delegateType, body, parameterExpression).Compile(); Action action = Expression.Lambda<Action>(Expression.Call(typeof(IconCache).GetMethod("OnIconLoadFailed", BindingFlags.Static | BindingFlags.NonPublic), arg), Array.Empty<ParameterExpression>()).Compile(); _getDataMethod.Invoke(null, new object[3] { itemId, obj, action }); } catch (Exception ex) { ManualLogSource log = _log; if (log != null) { log.LogDebug((object)$"[IconCache] Error loading icon {itemId}: {ex.Message}"); } _failedItems.Add(itemId); _loadingItems.Remove(itemId); } } private static void OnIconLoadedInternal(int itemId, object itemData) { _loadingItems.Remove(itemId); if (itemData == null) { _failedItems.Add(itemId); return; } try { Type type = itemData.GetType(); object obj = null; BindingFlags[] array = new BindingFlags[4] { BindingFlags.Instance | BindingFlags.Public, BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy, BindingFlags.Instance | BindingFlags.NonPublic, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy }; BindingFlags[] array2 = array; foreach (BindingFlags bindingAttr in array2) { PropertyInfo property = type.GetProperty("icon", bindingAttr); if (property != null) { obj = property.GetValue(itemData); break; } } if (obj == null) { array2 = array; foreach (BindingFlags bindingAttr2 in array2) { FieldInfo field = type.GetField("icon", bindingAttr2); if (field != null) { obj = field.GetValue(itemData); break; } } } if (obj == null) { Type type2 = type; while (type2 != null && obj == null) { PropertyInfo[] properties = type2.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (PropertyInfo propertyInfo in properties) { if (propertyInfo.Name.Equals("icon", StringComparison.OrdinalIgnoreCase)) { obj = propertyInfo.GetValue(itemData); break; } } if (obj == null) { FieldInfo[] fields = type2.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo fieldInfo in fields) { if (fieldInfo.Name.Equals("icon", StringComparison.OrdinalIgnoreCase)) { obj = fieldInfo.GetValue(itemData); break; } } } type2 = type2.BaseType; } } Sprite val = (Sprite)((obj is Sprite) ? obj : null); if (val != null) { CacheSprite(itemId, val); } else { _failedItems.Add(itemId); } } catch (Exception ex) { ManualLogSource log = _log; if (log != null) { log.LogDebug((object)$"[IconCache] Error processing icon {itemId}: {ex.Message}"); } _failedItems.Add(itemId); } } private static void OnIconLoadFailed(int itemId) { _loadingItems.Remove(itemId); _failedItems.Add(itemId); } private static void CacheSprite(int itemId, Sprite sprite) { //IL_0026: 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_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)sprite == (Object)null || (Object)(object)sprite.texture == (Object)null) { _failedItems.Add(itemId); return; } try { Rect rect = sprite.rect; Texture2D val; bool ownsTexture; if (((Rect)(ref rect)).width == (float)((Texture)sprite.texture).width) { rect = sprite.rect; if (((Rect)(ref rect)).height == (float)((Texture)sprite.texture).height) { val = sprite.texture; ownsTexture = false; goto IL_0077; } } val = ExtractSpriteTexture(sprite); ownsTexture = (Object)(object)val != (Object)null; goto IL_0077; IL_0077: if ((Object)(object)val != (Object)null) { _iconCache[itemId] = new CachedIcon { Texture = val, OwnsTexture = ownsTexture }; if (_iconCache.Count <= 200) { return; } int num = -1; int num2 = -1; foreach (int key in _iconCache.Keys) { if (num2 < 0) { num2 = key; } if (!_loadingItems.Contains(key) && !_failedItems.Contains(key)) { num = key; break; } } if (num < 0) { num = num2; } if (num >= 0 && _iconCache.TryGetValue(num, out var value)) { if (value.OwnsTexture && (Object)(object)value.Texture != (Object)null) { Object.Destroy((Object)(object)value.Texture); } _iconCache.Remove(num); } } else { _failedItems.Add(itemId); } } catch (Exception ex) { ManualLogSource log = _log; if (log != null) { log.LogDebug((object)$"[IconCache] Error caching sprite {itemId}: {ex.Message}"); } _failedItems.Add(itemId); } } private static Texture2D ExtractSpriteTexture(Sprite sprite) { //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) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Expected O, but got Unknown try { Rect rect = sprite.rect; int num = (int)((Rect)(ref rect)).width; int num2 = (int)((Rect)(ref rect)).height; if (!((Texture)sprite.texture).isReadable) { return CopyTextureViaRenderTexture(sprite); } Texture2D val = new Texture2D(num, num2, (TextureFormat)4, false); Color[] pixels = sprite.texture.GetPixels((int)((Rect)(ref rect)).x, (int)((Rect)(ref rect)).y, num, num2); val.SetPixels(pixels); val.Apply(); return val; } catch (Exception ex) { ManualLogSource log = _log; if (log != null) { log.LogDebug((object)("[IconCache] Error extracting sprite texture: " + ex.Message)); } return null; } } private static Texture2D CopyTextureViaRenderTexture(Sprite sprite) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Expected O, but got Unknown RenderTexture val = null; RenderTexture active = RenderTexture.active; try { Rect rect = sprite.rect; int num = (int)((Rect)(ref rect)).width; int num2 = (int)((Rect)(ref rect)).height; val = RenderTexture.GetTemporary(((Texture)sprite.texture).width, ((Texture)sprite.texture).height, 0, (RenderTextureFormat)0); Graphics.Blit((Texture)(object)sprite.texture, val); RenderTexture.active = val; Texture2D val2 = new Texture2D(num, num2, (TextureFormat)4, false); val2.ReadPixels(new Rect(((Rect)(ref rect)).x, ((Rect)(ref rect)).y, (float)num, (float)num2), 0, 0); val2.Apply(); return val2; } catch (Exception ex) { ManualLogSource log = _log; if (log != null) { log.LogDebug((object)("[IconCache] Error copying texture via RenderTexture: " + ex.Message)); } return null; } finally { RenderTexture.active = active; if ((Object)(object)val != (Object)null) { RenderTexture.ReleaseTemporary(val); } } } private static Texture2D CreateFallbackTexture() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) int num = 32; Texture2D val = new Texture2D(num, num); Color val2 = default(Color); ((Color)(ref val2))..ctor(0.3f, 0.3f, 0.4f, 0.8f); Color val3 = default(Color); ((Color)(ref val3))..ctor(0.5f, 0.5f, 0.6f, 1f); for (int i = 0; i < num; i++) { for (int j = 0; j < num; j++) { if (j == 0 || j == num - 1 || i == 0 || i == num - 1) { val.SetPixel(j, i, val3); } else { val.SetPixel(j, i, val2); } } } val.Apply(); return val; } public static void Clear() { foreach (KeyValuePair<int, CachedIcon> item in _iconCache) { if (item.Value.OwnsTexture && (Object)(object)item.Value.Texture != (Object)null) { Object.Destroy((Object)(object)item.Value.Texture); } } _iconCache.Clear(); _loadingItems.Clear(); _failedItems.Clear(); _initialized = false; _iconsLoaded = false; _pendingPreloadItemIds = null; } public static (int loaded, int loading, int failed) GetStats() { return (loaded: _iconCache.Count, loading: _loadingItems.Count, failed: _failedItems.Count); } public static void LogStatus() { (int, int, int) stats = GetStats(); ManualLogSource log = _log; if (log != null) { log.LogInfo((object)$"[IconCache] Loaded: {stats.Item1}, Loading: {stats.Item2}, Failed: {stats.Item3}"); } } } public static class OvernightHookUtility { public static bool TryHookOvernightEvent(ref bool overnightHooked, ref UnityAction overnightCallback, UnityAction callback, Func<Type, object> singletonResolver, Action<string> logInfo = null, Action<string> logWarning = null) { if (overnightHooked) { return true; } try { Type type = AccessTools.TypeByName("Wish.DayCycle"); if (type != null) { FieldInfo fieldInfo = AccessTools.Field(type, "OnDayStart"); if (fieldInfo != null && TryCombineCallback(fieldInfo, null, callback, logWarning)) { overnightCallback = callback; overnightHooked = true; logInfo?.Invoke("Hooked into DayCycle.OnDayStart"); return true; } } Type type2 = AccessTools.TypeByName("Wish.UIHandler"); if (type2 == null) { return false; } object obj = singletonResolver?.Invoke(type2); if (obj == null) { return false; } FieldInfo fieldInfo2 = AccessTools.Field(type2, "OnCompleteOvernight"); if (fieldInfo2 == null) { return false; } if (!TryCombineCallback(fieldInfo2, obj, callback, logWarning)) { return false; } overnightCallback = callback; overnightHooked = true; logInfo?.Invoke("Hooked into UIHandler.OnCompleteOvernight"); return true; } catch (Exception ex) { logWarning?.Invoke("Failed to hook overnight event: " + ex.Message); return false; } } public static void TryUnhookOvernightEvent(ref bool overnightHooked, ref UnityAction overnightCallback, Func<Type, object> singletonResolver = null, Action<string> logInfo = null, Action<string> logWarning = null) { UnityAction val = overnightCallback; try { if (val != null) { try { Type type = AccessTools.TypeByName("Wish.DayCycle"); FieldInfo fieldInfo = ((type != null) ? AccessTools.Field(type, "OnDayStart") : null); if (fieldInfo != null) { TryRemoveCallback(fieldInfo, null, val); } } catch (Exception ex) { logWarning?.Invoke("Failed to unhook DayCycle.OnDayStart: " + ex.Message); } try { Type type2 = AccessTools.TypeByName("Wish.UIHandler"); if (type2 != null) { object obj = singletonResolver?.Invoke(type2); FieldInfo fieldInfo2 = AccessTools.Field(type2, "OnCompleteOvernight"); if (obj != null && fieldInfo2 != null) { TryRemoveCallback(fieldInfo2, obj, val); } } } catch (Exception ex2) { logWarning?.Invoke("Failed to unhook UIHandler.OnCompleteOvernight: " + ex2.Message); } } logInfo?.Invoke("Overnight hook cleared"); } finally { overnightHooked = false; overnightCallback = null; } } private static bool TryCombineCallback(FieldInfo field, object instance, UnityAction callback, Action<string> logWarning) { if (field == null || callback == null) { return false; } object value = field.GetValue(instance); Delegate value2; if (value == null) { value2 = (Delegate)(object)callback; } else { if (!(value is Delegate source)) { logWarning?.Invoke(field.DeclaringType?.Name + "." + field.Name + " is set but is not a delegate; left unchanged."); return false; } value2 = Delegate.Combine(Delegate.Remove(source, (Delegate?)(object)callback), (Delegate?)(object)callback); } field.SetValue(instance, value2); return true; } private static void TryRemoveCallback(FieldInfo field, object instance, UnityAction callback) { if (!(field == null) && callback != null && field.GetValue(instance) is Delegate source) { field.SetValue(instance, Delegate.Remove(source, (Delegate?)(object)callback)); } } } public static class TextInputFocusGuard { private const float DefaultPollIntervalSeconds = 0.25f; private static float _nextPollTime = -1f; private static bool _cachedDefer; private static bool _tmpTypeLookupDone; private static Type _tmpInputFieldType; private static bool _qcLookupDone; private static Type _qcType; private static PropertyInfo _qcInstanceProp; private static PropertyInfo _qcIsActiveProp; private static FieldInfo _qcIsActiveField; public static bool ShouldDeferModHotkeys(ManualLogSource debugLog = null, float pollIntervalSeconds = 0.25f) { float realtimeSinceStartup = Time.realtimeSinceStartup; if (realtimeSinceStartup < _nextPollTime) { return _cachedDefer; } _nextPollTime = realtimeSinceStartup + Mathf.Max(0.05f, pollIntervalSeconds); bool flag = false; try { if (GUIUtility.keyboardControl != 0) { flag = true; } if (!flag) { EventSystem current = EventSystem.current; GameObject val = ((current != null) ? current.currentSelectedGameObject : null); if ((Object)(object)val != (Object)null) { if ((Object)(object)val.GetComponent<InputField>() != (Object)null) { flag = true; } else if (TryGetTmpInputField(val)) { flag = true; } } } if (!flag && IsQuantumConsoleActiveInternal(debugLog)) { flag = true; } } catch (Exception ex) { if (debugLog != null) { debugLog.LogDebug((object)("[TextInputFocusGuard] " + ex.Message)); } } _cachedDefer = flag; return flag; } private static bool TryGetTmpInputField(GameObject go) { if (!_tmpTypeLookupDone) { _tmpTypeLookupDone = true; _tmpInputFieldType = AccessTools.TypeByName("TMPro.TMP_InputField"); } if (_tmpInputFieldType == null) { return false; } return (Object)(object)go.GetComponent(_tmpInputFieldType) != (Object)null; } public static bool IsQuantumConsoleActive(ManualLogSource debugLog = null) { return IsQuantumConsoleActiveInternal(debugLog); } public static bool IsUnityUiTextInputFocused() { try { EventSystem current = EventSystem.current; GameObject val = ((current != null) ? current.currentSelectedGameObject : null); if ((Object)(object)val == (Object)null) { return false; } if ((Object)(object)val.GetComponent<InputField>() != (Object)null) { return true; } return TryGetTmpInputField(val); } catch { return false; } } private static bool IsQuantumConsoleActiveInternal(ManualLogSource debugLog) { try { if (!_qcLookupDone) { _qcLookupDone = true; _qcType = AccessTools.TypeByName("QFSW.QC.QuantumConsole"); if (_qcType != null) { _qcInstanceProp = AccessTools.Property(_qcType, "Instance"); _qcIsActiveProp = AccessTools.Property(_qcType, "IsActive"); _qcIsActiveField = AccessTools.Field(_qcType, "isActive") ?? AccessTools.Field(_qcType, "_isActive"); } } if (_qcType == null) { return false; } object obj = _qcInstanceProp?.GetValue(null); if (obj == null) { return false; } if (_qcIsActiveProp != null && _qcIsActiveProp.PropertyType == typeof(bool)) { return (bool)_qcIsActiveProp.GetValue(obj); } if (_qcIsActiveField != null && _qcIsActiveField.FieldType == typeof(bool)) { return (bool)_qcIsActiveField.GetValue(obj); } } catch (Exception ex) { if (debugLog != null) { debugLog.LogDebug((object)("[TextInputFocusGuard] Quantum Console focus check failed: " + ex.Message)); } } return false; } } public static class ModLocalization { private static readonly string[] SupportedLanguageCodes = new string[16] { "en", "da", "de", "es", "fr", "it", "ja", "ko", "nl", "pt", "pt-BR", "ru", "sv", "zh-CN", "zh-TW", "uk" }; private static readonly Dictionary<string, string> LanguageAlias = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { { "pt-br", "pt-BR" }, { "pt_br", "pt-BR" }, { "zh-cn", "zh-CN" }, { "zh_cn", "zh-CN" }, { "zh-tw", "zh-TW" }, { "zh_tw", "zh-TW" } }; private static string _modId; private static Dictionary<string, Dictionary<string, string>> _tables; private static ManualLogSource _log; private static bool _initialized; private static bool _forceEnglish; public static string CurrentLanguage { get; private set; } = "en"; public static bool ForceEnglish => _forceEnglish; public static bool IsReady { get { if (_initialized && _tables != null) { return _tables.Count > 0; } return false; } } public static event Action<string> LanguageChanged { add { LanguageChangeWatcher.LanguageChanged += value; } remove { LanguageChangeWatcher.LanguageChanged -= value; } } public static void Init(string modId, Dictionary<string, Dictionary<string, string>> tables, Harmony harmony, ManualLogSource log) { _modId = modId ?? string.Empty; _tables = tables ?? new Dictionary<string, Dictionary<string, string>>(); _log = log; _initialized = true; RefreshCurren