Decompiled source of HavensBirthright v3.1.1
HavensBirthright.dll
Decompiled a week ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using HavensBirthright.Abilities; using HavensBirthright.Patches; using HavensBirthright.Session; using Microsoft.CodeAnalysis; using SunhavenMods.Shared; using UnityEngine; using UnityEngine.EventSystems; 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("HavensBirthright")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+79057c8571b97b27a9323b455b8f88a51829b688")] [assembly: AssemblyProduct("HavensBirthright")] [assembly: AssemblyTitle("HavensBirthright")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.0.0")] [module: UnverifiableCode] [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 VersionChecker { public class VersionCheckResult { public bool Success { get; set; } public bool UpdateAvailable { get; set; } public string CurrentVersion { get; set; } public string LatestVersion { get; set; } public string ModName { get; set; } public string NexusUrl { get; set; } public string Changelog { get; set; } public string ErrorMessage { get; set; } } public class ModHealthSnapshot { public string PluginGuid { get; set; } public DateTime LastCheckUtc { get; set; } public int ExceptionCount { get; set; } public string LastError { get; set; } } private class VersionCheckRunner : MonoBehaviour { private ManualLogSource _pluginLog; public void StartCheck(string pluginGuid, string currentVersion, ManualLogSource pluginLog, Action<VersionCheckResult> onComplete) { _pluginLog = pluginLog; ((MonoBehaviour)this).StartCoroutine(CheckVersionCoroutine(pluginGuid, currentVersion, onComplete)); } private void LogInfo(string message) { ManualLogSource pluginLog = _pluginLog; if (pluginLog != null) { pluginLog.LogInfo((object)("[VersionChecker] " + message)); } } private void LogWarningMsg(string message) { ManualLogSource pluginLog = _pluginLog; if (pluginLog != null) { pluginLog.LogWarning((object)("[VersionChecker] " + message)); } } private void LogErrorMsg(string message) { ManualLogSource pluginLog = _pluginLog; if (pluginLog != null) { pluginLog.LogError((object)("[VersionChecker] " + message)); } } private IEnumerator CheckVersionCoroutine(string pluginGuid, string currentVersion, Action<VersionCheckResult> onComplete) { VersionCheckResult result = new VersionCheckResult { CurrentVersion = currentVersion }; UnityWebRequest www = UnityWebRequest.Get("https://azraelgodking.github.io/SunhavenMod/versions.json"); try { www.timeout = 10; yield return www.SendWebRequest(); if ((int)www.result == 2 || (int)www.result == 3) { result.Success = false; result.ErrorMessage = "Network error: " + www.error; RecordHealthError(pluginGuid, result.ErrorMessage); LogWarningMsg(result.ErrorMessage); onComplete?.Invoke(result); Object.Destroy((Object)(object)((Component)this).gameObject); yield break; } try { string text = www.downloadHandler.text; Match match = GetModPattern(pluginGuid).Match(text); if (!match.Success) { result.Success = false; result.ErrorMessage = "Mod '" + pluginGuid + "' not found in versions.json"; RecordHealthError(pluginGuid, result.ErrorMessage); LogWarningMsg(result.ErrorMessage); onComplete?.Invoke(result); Object.Destroy((Object)(object)((Component)this).gameObject); yield break; } string value = match.Groups[1].Value; result.LatestVersion = ExtractJsonString(value, "version"); result.ModName = ExtractJsonString(value, "name"); result.NexusUrl = ExtractJsonString(value, "nexus"); result.Changelog = ExtractJsonString(value, "changelog"); if (string.IsNullOrEmpty(result.LatestVersion)) { result.Success = false; result.ErrorMessage = "Could not parse version from response"; RecordHealthError(pluginGuid, result.ErrorMessage); LogWarningMsg(result.ErrorMessage); onComplete?.Invoke(result); Object.Destroy((Object)(object)((Component)this).gameObject); yield break; } result.Success = true; result.UpdateAvailable = CompareVersions(currentVersion, result.LatestVersion) < 0; if (result.UpdateAvailable) { LogInfo("Update available for " + result.ModName + ": " + currentVersion + " -> " + result.LatestVersion); } else { LogInfo(result.ModName + " is up to date (v" + currentVersion + ")"); } } catch (Exception ex) { result.Success = false; result.ErrorMessage = "Parse error: " + ex.Message; RecordHealthError(pluginGuid, result.ErrorMessage); LogErrorMsg(result.ErrorMessage); } } finally { ((IDisposable)www)?.Dispose(); } onComplete?.Invoke(result); Object.Destroy((Object)(object)((Component)this).gameObject); } private string ExtractJsonString(string json, string key) { Match match = ExtractFieldRegex.Match(json); while (match.Success) { if (string.Equals(match.Groups["key"].Value, key, StringComparison.Ordinal)) { return match.Groups["value"].Value; } match = match.NextMatch(); } return null; } } private const string VersionsUrl = "https://azraelgodking.github.io/SunhavenMod/versions.json"; private static readonly Dictionary<string, ModHealthSnapshot> HealthByPluginGuid = new Dictionary<string, ModHealthSnapshot>(StringComparer.OrdinalIgnoreCase); private static readonly object HealthLock = new object(); private static readonly Dictionary<string, Regex> ModPatternCache = new Dictionary<string, Regex>(StringComparer.Ordinal); private static readonly object ModPatternCacheLock = new object(); private static readonly Regex ExtractFieldRegex = new Regex("\"(?<key>[^\"]+)\"\\s*:\\s*(?:\"(?<value>[^\"]*)\"|null)", RegexOptions.Compiled); public static void CheckForUpdate(string pluginGuid, string currentVersion, ManualLogSource logger = null, Action<VersionCheckResult> onComplete = null) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) TouchHealth(pluginGuid); VersionCheckRunner versionCheckRunner = new GameObject("VersionChecker").AddComponent<VersionCheckRunner>(); Object.DontDestroyOnLoad((Object)(object)((Component)versionCheckRunner).gameObject); SceneRootSurvivor.TryRegisterPersistentRunnerGameObject(((Component)versionCheckRunner).gameObject); versionCheckRunner.StartCheck(pluginGuid, currentVersion, logger, onComplete); } public static ModHealthSnapshot GetHealthSnapshot(string pluginGuid) { if (string.IsNullOrWhiteSpace(pluginGuid)) { return null; } lock (HealthLock) { if (!HealthByPluginGuid.TryGetValue(pluginGuid, out ModHealthSnapshot value)) { return null; } return new ModHealthSnapshot { PluginGuid = value.PluginGuid, LastCheckUtc = value.LastCheckUtc, ExceptionCount = value.ExceptionCount, LastError = value.LastError }; } } public static int CompareVersions(string v1, string v2) { if (string.IsNullOrEmpty(v1) || string.IsNullOrEmpty(v2)) { return 0; } v1 = v1.TrimStart('v', 'V'); v2 = v2.TrimStart('v', 'V'); int num = v1.IndexOfAny(new char[2] { '-', '+' }); if (num >= 0) { v1 = v1.Substring(0, num); } int num2 = v2.IndexOfAny(new char[2] { '-', '+' }); if (num2 >= 0) { v2 = v2.Substring(0, num2); } string[] array = v1.Split(new char[1] { '.' }); string[] array2 = v2.Split(new char[1] { '.' }); int num3 = Math.Max(array.Length, array2.Length); for (int i = 0; i < num3; i++) { int result; int num4 = ((i < array.Length && int.TryParse(array[i], out result)) ? result : 0); int result2; int num5 = ((i < array2.Length && int.TryParse(array2[i], out result2)) ? result2 : 0); if (num4 < num5) { return -1; } if (num4 > num5) { return 1; } } return 0; } private static void TouchHealth(string pluginGuid) { if (string.IsNullOrWhiteSpace(pluginGuid)) { return; } lock (HealthLock) { if (!HealthByPluginGuid.TryGetValue(pluginGuid, out ModHealthSnapshot value)) { value = new ModHealthSnapshot { PluginGuid = pluginGuid }; HealthByPluginGuid[pluginGuid] = value; } value.LastCheckUtc = DateTime.UtcNow; } } private static void RecordHealthError(string pluginGuid, string errorMessage) { if (string.IsNullOrWhiteSpace(pluginGuid)) { return; } lock (HealthLock) { if (!HealthByPluginGuid.TryGetValue(pluginGuid, out ModHealthSnapshot value)) { value = new ModHealthSnapshot { PluginGuid = pluginGuid }; HealthByPluginGuid[pluginGuid] = value; } value.LastCheckUtc = DateTime.UtcNow; value.ExceptionCount++; value.LastError = errorMessage; } } private static Regex GetModPattern(string pluginGuid) { lock (ModPatternCacheLock) { if (!ModPatternCache.TryGetValue(pluginGuid, out Regex value)) { value = new Regex("\"" + Regex.Escape(pluginGuid) + "\"\\s*:\\s*\\{([^}]+)\\}", RegexOptions.Compiled | RegexOptions.Singleline); ModPatternCache[pluginGuid] = value; } return value; } } } public static class VersionCheckerExtensions { public static void NotifyUpdateAvailable(this VersionChecker.VersionCheckResult result, ManualLogSource logger = null) { if (!result.UpdateAvailable) { return; } string text = result.ModName + " update available: v" + result.LatestVersion; try { Type type = ReflectionHelper.FindWishType("NotificationStack"); if (type != null) { Type type2 = ReflectionHelper.FindType("SingletonBehaviour`1", "Wish"); if (type2 != null) { object obj = type2.MakeGenericType(type).GetProperty("Instance")?.GetValue(null); if (obj != null) { MethodInfo method = type.GetMethod("SendNotification", new Type[5] { typeof(string), typeof(int), typeof(int), typeof(bool), typeof(bool) }); if (method != null) { method.Invoke(obj, new object[5] { text, 0, 1, false, true }); return; } } } } } catch (Exception ex) { if (logger != null) { logger.LogWarning((object)("Failed to send native notification: " + ex.Message)); } } if (logger != null) { logger.LogWarning((object)("[UPDATE AVAILABLE] " + text)); } if (!string.IsNullOrEmpty(result.NexusUrl) && logger != null) { logger.LogWarning((object)("Download at: " + result.NexusUrl)); } } } 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 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 abstract class PersistentRunnerBase : MonoBehaviour { private bool _wasInGame; private float _lastHeartbeat; private string _lastSceneName = ""; private float _lastSceneCheckTime; private const float SceneCheckInterval = 0.5f; protected virtual float HeartbeatInterval => 0f; protected virtual string RunnerName => ((object)this).GetType().Name; protected virtual void OnUpdate() { } protected virtual void OnMenuTransition() { } protected virtual void OnGameTransition() { } protected virtual void Log(string message) { } protected virtual void LogWarning(string message) { } public static T CreateRunner<T>() where T : PersistentRunnerBase { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Expected O, but got Unknown //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Expected O, but got Unknown GameObject val = new GameObject("[" + typeof(T).Name + "]") { hideFlags = (HideFlags)61 }; Object.DontDestroyOnLoad((Object)val); SceneRootSurvivor.TryRegisterPersistentRunnerGameObject(val); return val.AddComponent<T>(); } protected virtual void Awake() { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) Scene activeScene = SceneManager.GetActiveScene(); _lastSceneName = ((Scene)(ref activeScene)).name; _wasInGame = !SceneHelpers.IsMenuScene(_lastSceneName); Log("[" + RunnerName + "] Initialized in scene: " + _lastSceneName); } protected virtual void Update() { //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) if (HeartbeatInterval > 0f) { _lastHeartbeat += Time.deltaTime; if (_lastHeartbeat >= HeartbeatInterval) { _lastHeartbeat = 0f; Log("[" + RunnerName + "] Heartbeat - still alive"); } } float unscaledTime = Time.unscaledTime; if (unscaledTime - _lastSceneCheckTime >= 0.5f) { _lastSceneCheckTime = unscaledTime; Scene activeScene = SceneManager.GetActiveScene(); string name = ((Scene)(ref activeScene)).name; if (name != _lastSceneName) { _lastSceneName = name; HandleSceneChange(name); } } try { OnUpdate(); } catch (Exception ex) { LogWarning("[" + RunnerName + "] Error in OnUpdate: " + ex.Message); } } private void HandleSceneChange(string sceneName) { bool flag = SceneHelpers.IsMenuScene(sceneName); if (_wasInGame && flag) { Log("[" + RunnerName + "] Menu transition detected"); try { OnMenuTransition(); } catch (Exception ex) { LogWarning("[" + RunnerName + "] Error in OnMenuTransition: " + ex.Message); } } else if (!_wasInGame && !flag) { Log("[" + RunnerName + "] Game transition detected"); try { OnGameTransition(); } catch (Exception ex2) { LogWarning("[" + RunnerName + "] Error in OnGameTransition: " + ex2.Message); } } _wasInGame = !flag; } protected virtual void OnDestroy() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) Scene activeScene = SceneManager.GetActiveScene(); string text = (((Scene)(ref activeScene)).name ?? string.Empty).ToLowerInvariant(); if (!Application.isPlaying || text.Contains("menu") || text.Contains("title")) { Log("[" + RunnerName + "] OnDestroy during app quit/menu unload (expected)."); } else { LogWarning("[" + RunnerName + "] OnDestroy outside quit/menu (unexpected)."); } } } public static class SceneHelpers { private static readonly string[] MenuScenePatterns = new string[3] { "menu", "title", "bootstrap" }; private static readonly string[] ExactMenuScenes = new string[2] { "MainMenu", "Bootstrap" }; public static bool IsMenuScene(string sceneName) { if (string.IsNullOrEmpty(sceneName)) { return true; } string[] exactMenuScenes = ExactMenuScenes; foreach (string text in exactMenuScenes) { if (sceneName == text) { return true; } } string text2 = sceneName.ToLowerInvariant(); exactMenuScenes = MenuScenePatterns; foreach (string value in exactMenuScenes) { if (text2.Contains(value)) { return true; } } return false; } public static bool IsCurrentSceneMenu() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) Scene activeScene = SceneManager.GetActiveScene(); return IsMenuScene(((Scene)(ref activeScene)).name); } public static bool IsInGame() { return !IsCurrentSceneMenu(); } public static string GetCurrentSceneName() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) Scene activeScene = SceneManager.GetActiveScene(); return ((Scene)(ref activeScene)).name; } public static bool IsMainMenu() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) Scene activeScene = SceneManager.GetActiveScene(); return ((Scene)(ref activeScene)).name == "MainMenu"; } } public static class 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 SharedCodeRevision { public const string Value = "2026.07.28"; } public sealed class ModHealthReport { public string PluginGuid { get; set; } public string DisplayName { get; set; } public string PluginVersion { get; set; } public string SharedCodeRevision { get; set; } public string IntegrationSummary { get; set; } public string Mode { get; set; } public string CharacterName { get; set; } public bool? DataLoaded { get; set; } public string LastPersistenceOutcome { get; set; } public string LastError { get; set; } public DateTime ReportedUtc { get; set; } public ModHealthReport Clone() { return new ModHealthReport { PluginGuid = PluginGuid, DisplayName = DisplayName, PluginVersion = PluginVersion, SharedCodeRevision = SharedCodeRevision, IntegrationSummary = IntegrationSummary, Mode = Mode, CharacterName = CharacterName, DataLoaded = DataLoaded, LastPersistenceOutcome = LastPersistenceOutcome, LastError = LastError, ReportedUtc = ReportedUtc }; } public void AppendDiagnosticDump(StringBuilder sb) { if (sb != null) { sb.AppendLine("guid: " + (PluginGuid ?? "—")); sb.AppendLine("name: " + (DisplayName ?? "—")); sb.AppendLine("version: " + (PluginVersion ?? "—")); sb.AppendLine("shared: " + (SharedCodeRevision ?? "—")); sb.AppendLine("integrations: " + (IntegrationSummary ?? "—")); sb.AppendLine("mode: " + (Mode ?? "—")); sb.AppendLine("character: " + (string.IsNullOrEmpty(CharacterName) ? "—" : CharacterName)); sb.AppendLine("data loaded: " + (DataLoaded.HasValue ? DataLoaded.Value.ToString() : "—")); sb.AppendLine("last save: " + (LastPersistenceOutcome ?? "—")); sb.AppendLine("last error: " + (LastError ?? "—")); sb.AppendLine("reported: " + ((ReportedUtc == default(DateTime)) ? "—" : ReportedUtc.ToLocalTime().ToString("u"))); } } } public static class ModDiagnostics { private static readonly Dictionary<string, ModHealthReport> ReportsByPluginGuid = new Dictionary<string, ModHealthReport>(StringComparer.OrdinalIgnoreCase); private static readonly object Lock = new object(); public static void ReportStartup(ModHealthReport report) { if (report == null || string.IsNullOrWhiteSpace(report.PluginGuid)) { return; } report.ReportedUtc = DateTime.UtcNow; if (string.IsNullOrEmpty(report.SharedCodeRevision)) { report.SharedCodeRevision = "2026.07.28"; } 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.07.28", IntegrationSummary = integrationSummary, Mode = mode, DataLoaded = dataLoaded, LastPersistenceOutcome = lastPersistenceOutcome }); } } public static class SuitePluginGuids { public const string DevTools = "com.azraelgodking.havendevtools"; public const string SenpaisChest = "com.azraelgodking.senpaischest"; public const string BirthdayReminder = "com.azraelgodking.squirrelsbirthdayreminder"; public const string HavensBirthright = "com.azraelgodking.havensbirthright"; public const string Smut = "com.azraelgodking.sunhavenmuseumutilitytracker"; public const string SunhavenTodo = "com.azraelgodking.sunhaventodo"; public const string TheVault = "com.azraelgodking.thevault"; public const string HavensAlmanac = "com.azraelgodking.havensalmanac"; public const string FasterRaces = "com.azraelgodking.fasterraces"; public const string TrinketFortune = "com.azraelgodking.trinketfortune"; public const string CropOptimizer = "com.azraelgodking.cropoptimizer"; public const string HavensRespec = "com.azraelgodking.havensrespec"; public const string GiftingAssistant = "com.azraelgodking.giftingassistant"; } public 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"; } } } namespace HavensBirthright { public class BirthrightRunner : PersistentRunnerBase { private float _outdoorTime; private float _lastTidalBlessingCheck; private float _lastInfernalForgeCheck; private float _lastFontOfLightCheck; private float _lastLoyaltyAuraCheck; private const float LoyaltyAuraUpdateInterval = 2f; private bool _tidalBlessingDiagLogged; protected override string RunnerName => "BirthrightRunner"; protected override void OnUpdate() { //IL_0024: 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) if (!SceneHelpers.IsInGame()) { return; } CharacterSessionController.OnUpdateIdentityChecks(); CheckAbilityToggleHotkey(); if (!TextInputFocusGuard.ShouldDeferModHotkeys(Plugin.Log) && (int)Plugin.StaticReloadConfigKey != 0 && Input.GetKeyDown(Plugin.StaticReloadConfigKey)) { Plugin.Instance?.ReloadConfig(); } if (!RacialConfig.EnableRacialBonuses.Value) { return; } RacialBonusManager racialBonusManager = Plugin.GetRacialBonusManager(); if (racialBonusManager == null) { return; } Race? playerRace = racialBonusManager.GetPlayerRace(); if (!playerRace.HasValue) { RaceDetectionService.RetryRaceDetection(); playerRace = racialBonusManager.GetPlayerRace(); if (!playerRace.HasValue) { return; } } GameApis.EnsureWishCachesInitialized(); if (!StatFrameCache.IsCacheValid || (Time.frameCount & 3) == 0) { StatFrameCache.Update(playerRace.Value, GameApis.DayCycleType); } if (AbilityConfig.EnableActiveAbilities.Value) { Race race = ElementalVariantResolver.ResolveElementalAbilityRace(playerRace.Value); if (playerRace.Value == Race.AmariBird && AbilityConfig.EnableTailwind.Value) { UpdateTailwind(); } if (playerRace.Value == Race.AmariDog && AbilityConfig.EnableLoyaltyAura.Value) { UpdateLoyaltyAura(); } if (race == Race.WaterElemental && AbilityConfig.EnableTidalBlessing.Value) { UpdateTidalBlessing(); } if ((race == Race.FireElemental || race == Race.Elemental) && AbilityConfig.EnableInfernalForge.Value) { UpdateInfernalForge(); } if (playerRace.Value == Race.Angel && AbilityConfig.EnableFontOfLight.Value) { UpdateFontOfLight(); } } } protected override void OnMenuTransition() { BirthrightGameSaveContext.Reset(); ResetAllStateForNewSave(); } protected override void OnGameTransition() { ResetInstanceState(); } public static void ResetAllStateForNewSave() { CharacterSessionController.ClearTrackedCharacterId(); StatFrameCache.Reset(); ActiveAbilityManager.ResetAll(); Plugin.GetRacialBonusManager()?.ClearPlayerRace(); PlayerPatches.ResetRaceDetection(); CombatPatches.ResetNineLives(); GameApis.ResetAllApiCaches(); AbilityPatches.ResetReflectionCache(); Plugin.GetRunner()?.ResetRunnerTimersOnly(); } private void ResetRunnerTimersOnly() { _outdoorTime = 0f; _lastTidalBlessingCheck = 0f; _lastInfernalForgeCheck = 0f; _lastFontOfLightCheck = 0f; _lastLoyaltyAuraCheck = 0f; _tidalBlessingDiagLogged = false; ActiveAbilityManager.SetBonusValue("LoyaltyAura", 0f); } public void ResetInstanceState() { ResetRunnerTimersOnly(); StatFrameCache.Reset(); GameApis.ResetFarmingTileAndInventoryScanState(); } 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 UpdateLoyaltyAura() { if (Time.time - _lastLoyaltyAuraCheck < 2f) { return; } _lastLoyaltyAuraCheck = Time.time; try { CharacterData authoritativeCharacterData = CharacterFingerprint.GetAuthoritativeCharacterData(); Dictionary<string, float> dictionary = ((authoritativeCharacterData != null) ? authoritativeCharacterData.Relationships : null); if (dictionary == null || dictionary.Count == 0) { ActiveAbilityManager.SetBonusValue("LoyaltyAura", 0f); return; } int num = 0; foreach (KeyValuePair<string, float> item in dictionary) { float value = item.Value; int maxHeartsForRelationshipPoints = GetMaxHeartsForRelationshipPoints(value); if (value >= (float)maxHeartsForRelationshipPoints * 5f) { num++; } } float value2 = Mathf.Min((float)num * AbilityConfig.LoyaltyAuraBonusPerNPC.Value, AbilityConfig.LoyaltyAuraMaxBonus.Value); ActiveAbilityManager.SetBonusValue("LoyaltyAura", value2); } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[LoyaltyAura] Error: " + ex.Message)); } ActiveAbilityManager.SetBonusValue("LoyaltyAura", 0f); } } private static int GetMaxHeartsForRelationshipPoints(float points) { if (points >= 75f) { return 20; } if (points >= 50f) { return 15; } return 10; } private void UpdateTailwind() { string text = SceneHelpers.GetCurrentSceneName().ToLowerInvariant(); if (text.Contains("house") || text.Contains("inn") || text.Contains("shop") || text.Contains("cave") || text.Contains("mine") || text.Contains("dungeon") || text.Contains("interior")) { _outdoorTime = 0f; ActiveAbilityManager.SetBonusValue("TailwindOutdoor", 0f); } else { _outdoorTime += Time.deltaTime; float value = Mathf.Min(_outdoorTime / 60f * AbilityConfig.TailwindBonusPerMinute.Value, AbilityConfig.TailwindMaxBonus.Value); ActiveAbilityManager.SetBonusValue("TailwindOutdoor", value); } } private void UpdateTidalBlessing() { //IL_0340: Unknown result type (might be due to invalid IL or missing references) //IL_0355: Unknown result type (might be due to invalid IL or missing references) if (!ActiveAbilityManager.IsRuntimeEnabled("TidalBlessing")) { _tidalBlessingDiagLogged = false; } else { if (Time.time - _lastTidalBlessingCheck < AbilityConfig.TidalBlessingCooldown.Value) { return; } _lastTidalBlessingCheck = Time.time; if (ActiveAbilityManager.IsOnCooldown("TidalBlessing")) { return; } try { Player instance = Player.Instance; if ((Object)(object)instance == (Object)null) { return; } float maxHealth = instance.MaxHealth; float num = ReflectionHelper.TryGetValue(instance, "health", -1f); if (num < 0f) { num = ReflectionHelper.TryGetValue(instance, "Health", maxHealth); } if (maxHealth <= 0f) { return; } float num2 = num / maxHealth * 100f; if (!_tidalBlessingDiagLogged) { _tidalBlessingDiagLogged = true; ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)"[TidalBlessing] === DIAGNOSTIC ==="); } ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogInfo((object)$"[TidalBlessing] HP: {num:F0}/{maxHealth:F0} ({num2:F0}%), threshold: {AbilityConfig.TidalBlessingHPThreshold.Value}%"); } ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogInfo((object)("[TidalBlessing] TileManager: type=" + ((GameApis.TileManagerType != null) ? "ok" : "NULL") + ", instance=" + ((GameApis.TileManagerInstance != null) ? "ok" : "NULL"))); } ManualLogSource log4 = Plugin.Log; if (log4 != null) { log4.LogInfo((object)("[TidalBlessing] Methods: Water=" + ((GameApis.WaterTileMethod != null) ? "ok" : "NULL") + ", farmingData field=" + ((GameApis.FarmingDataField != null) ? "ok" : "NULL"))); } ManualLogSource log5 = Plugin.Log; if (log5 != null) { log5.LogInfo((object)$"[TidalBlessing] Cooldown: {AbilityConfig.TidalBlessingCooldown.Value}s, HP cost: {AbilityConfig.TidalBlessingHPCostPercent.Value}%"); } if (GameApis.FarmingDataField != null && GameApis.TileManagerInstance != null) { try { if (GameApis.FarmingDataField.GetValue(GameApis.TileManagerInstance) is IDictionary dictionary) { int num3 = 0; int num4 = 0; { IDictionaryEnumerator dictionaryEnumerator = dictionary.GetEnumerator(); try { while (dictionaryEnumerator.MoveNext()) { switch (((DictionaryEntry)dictionaryEnumerator.Current).Value.ToString()) { case "Hoed": case "2": num3++; break; case "Watered": case "3": num4++; break; } } } finally { IDisposable disposable = dictionaryEnumerator as IDisposable; if (disposable != null) { disposable.Dispose(); } } } ManualLogSource log6 = Plugin.Log; if (log6 != null) { log6.LogInfo((object)$"[TidalBlessing] farmingData: {dictionary.Count} total, {num3} hoed, {num4} watered"); } } } catch (Exception ex) { ManualLogSource log7 = Plugin.Log; if (log7 != null) { log7.LogDebug((object)("[BirthrightRunner] TidalBlessing diagnostic: " + ex.Message)); } } } ManualLogSource log8 = Plugin.Log; if (log8 != null) { log8.LogInfo((object)("[TidalBlessing] Scene: " + SceneHelpers.GetCurrentSceneName())); } ManualLogSource log9 = Plugin.Log; if (log9 != null) { log9.LogInfo((object)$"[TidalBlessing] Player world pos: ({((Component)instance).transform.position.x:F1}, {((Component)instance).transform.position.y:F1})"); } ManualLogSource log10 = Plugin.Log; if (log10 != null) { log10.LogInfo((object)("[TidalBlessing] Grid distance filter: " + ((GameApis.GridCellToWorldMethod != null && GameApis.GridInstance != null) ? "enabled (radius=1)" : "disabled (no Grid)"))); } ManualLogSource log11 = Plugin.Log; if (log11 != null) { log11.LogInfo((object)"[TidalBlessing] === END DIAGNOSTIC ==="); } } if (num2 <= AbilityConfig.TidalBlessingHPThreshold.Value) { return; } if (GameApis.TileManagerType != null && GameApis.IsWateredMethod != null && GameApis.WaterTileMethod != null) { UpdateTidalBlessingViaTileManager(instance, maxHealth, num); return; } ManualLogSource log12 = Plugin.Log; if (log12 != null) { log12.LogInfo((object)"[TidalBlessing] Using fallback Crop approach (TileManager methods not available)"); } UpdateTidalBlessingViaCropObjects(instance, maxHealth, num); } catch (Exception ex2) { ManualLogSource log13 = Plugin.Log; if (log13 != null) { log13.LogWarning((object)("[TidalBlessing] Error: " + ex2.Message)); } ManualLogSource log14 = Plugin.Log; if (log14 != null) { log14.LogWarning((object)("[TidalBlessing] Stack: " + ex2.StackTrace)); } } } } private void UpdateTidalBlessingViaTileManager(Player player, float maxHP, float currentHP) { //IL_01bc: Unknown result type (might be due to invalid IL or missing references) //IL_0293: Unknown result type (might be due to invalid IL or missing references) //IL_014c: Unknown result type (might be due to invalid IL or missing references) //IL_0151: Unknown result type (might be due to invalid IL or missing references) //IL_01c7: Unknown result type (might be due to invalid IL or missing references) //IL_0178: Unknown result type (might be due to invalid IL or missing references) //IL_0188: Unknown result type (might be due to invalid IL or missing references) //IL_018d: Unknown result type (might be due to invalid IL or missing references) //IL_018f: Unknown result type (might be due to invalid IL or missing references) //IL_0191: Unknown result type (might be due to invalid IL or missing references) //IL_0198: Unknown result type (might be due to invalid IL or missing references) //IL_019f: Unknown result type (might be due to invalid IL or missing references) //IL_020a: Unknown result type (might be due to invalid IL or missing references) //IL_020f: Unknown result type (might be due to invalid IL or missing references) //IL_01b0: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Unknown result type (might be due to invalid IL or missing references) //IL_0246: Unknown result type (might be due to invalid IL or missing references) object tileManagerInstance = GameApis.TileManagerInstance; Object val = (Object)((tileManagerInstance is Object) ? tileManagerInstance : null); if (val != null && val == (Object)null) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)"[TidalBlessing] TileManager instance was stale — re-acquiring"); } GameApis.TileManagerInstance = null; } if (GameApis.TileManagerInstance == null) { GameApis.TryGetTileManagerInstance(); } if (GameApis.TileManagerInstance == null) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogInfo((object)"[TidalBlessing] TileManager instance not available — skipping this cycle"); } } else { if (GameApis.FarmingDataField == null || !(GameApis.FarmingDataField.GetValue(GameApis.TileManagerInstance) is IDictionary { Count: not 0 } dictionary)) { return; } float num = maxHP * (AbilityConfig.TidalBlessingHPCostPercent.Value / 100f); Scene activeScene = SceneManager.GetActiveScene(); short num2 = (short)((Scene)(ref activeScene)).buildIndex; int num3 = 0; int num4 = 0; List<Vector2Int> list = new List<Vector2Int>(); Vector2 val2 = default(Vector2); ((Vector2)(ref val2))..ctor(((Component)player).transform.position.x, ((Component)player).transform.position.y); int num5 = 1; bool flag = GameApis.GridCellToWorldMethod != null && GameApis.GridInstance != null; foreach (DictionaryEntry item2 in dictionary) { string text = item2.Value.ToString(); if (!(text == "Hoed") && !(text == "2")) { continue; } Vector2Int item = (Vector2Int)item2.Key; if (flag) { try { Vector3 val3 = (Vector3)GameApis.GridCellToWorldMethod.Invoke(GameApis.GridInstance, new object[1] { (object)new Vector3Int(((Vector2Int)(ref item)).x, ((Vector2Int)(ref item)).y, 0) }); if (Vector2.Distance(val2, new Vector2(val3.x, val3.y)) <= (float)num5) { list.Add(item); } } catch { list.Add(item); } } else { list.Add(item); } } num4 = list.Count; bool flag2 = default(bool); foreach (Vector2Int item3 in list) { if ((currentHP - num * (float)(num3 + 1)) / maxHP * 100f <= AbilityConfig.TidalBlessingHPThreshold.Value) { break; } try { object obj2 = GameApis.WaterTileMethod.Invoke(GameApis.TileManagerInstance, new object[2] { item3, num2 }); int num6; if (obj2 is bool) { flag2 = (bool)obj2; num6 = 1; } else { num6 = 0; } if (((uint)num6 & (flag2 ? 1u : 0u)) != 0) { num3++; } } catch (Exception ex) { ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogWarning((object)$"[TidalBlessing] Water({item3}) error: {ex.Message}"); } } } if (num3 > 0) { float num7 = num * (float)num3; float num8 = currentHP - num7; if (num8 < 1f) { num8 = 1f; } ReflectionHelper.SetInstanceValue(player, "Health", num8); ManualLogSource log4 = Plugin.Log; if (log4 != null) { log4.LogInfo((object)$"[TidalBlessing] Watered {num3}/{num4} hoed tiles via farmingData scan"); } } else if (num4 > 0) { ManualLogSource log5 = Plugin.Log; if (log5 != null) { log5.LogInfo((object)$"[TidalBlessing] Found {num4} hoed tiles but Water() returned false for all"); } } else { ManualLogSource log6 = Plugin.Log; if (log6 != null) { log6.LogInfo((object)"[TidalBlessing] No hoed tiles in farmingData"); } } } } private void UpdateTidalBlessingViaCropObjects(Player player, float maxHP, float currentHP) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Unknown result type (might be due to invalid IL or missing references) if (GameApis.CropType == null) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)"[TidalBlessing] No Crop type and no TileManager — cannot water"); } return; } Vector3 position = ((Component)player).transform.position; int num = 1; int num2 = 0; Object[] array = Object.FindObjectsOfType(GameApis.CropType); ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogDebug((object)$"[TidalBlessing] Fallback mode: FindObjectsOfType found {((array != null) ? array.Length : 0)} Crop objects"); } if (array == null || array.Length == 0) { return; } float num3 = maxHP * (AbilityConfig.TidalBlessingHPCostPercent.Value / 100f); Object[] array2 = array; foreach (Object val in array2) { Component val2 = (Component)(object)((val is Component) ? val : null); if ((Object)(object)val2 == (Object)null || Vector2.Distance(new Vector2(position.x, position.y), new Vector2(val2.transform.position.x, val2.transform.position.y)) > (float)num) { continue; } object instanceValue = ReflectionHelper.GetInstanceValue(val, "data"); if (instanceValue == null || !ReflectionHelper.TryGetValue(instanceValue, "watered", defaultValue: false)) { if ((currentHP - num3 * (float)(num2 + 1)) / maxHP * 100f <= AbilityConfig.TidalBlessingHPThreshold.Value) { break; } MethodInfo method = ((object)val).GetType().GetMethod("Water", BindingFlags.Instance | BindingFlags.Public); if (method != null) { method.Invoke(val, null); num2++; } else if (instanceValue != null) { ReflectionHelper.SetInstanceValue(instanceValue, "watered", true); num2++; } } } if (num2 > 0) { float num4 = num3 * (float)num2; float num5 = currentHP - num4; if (num5 < 1f) { num5 = 1f; } ReflectionHelper.SetInstanceValue(player, "Health", num5); ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogInfo((object)$"[TidalBlessing] Watered {num2} crops via fallback Crop.Water()"); } } } private void UpdateInfernalForge() { if (!ActiveAbilityManager.IsRuntimeEnabled("InfernalForge")) { return; } float num = AbilityConfig.InfernalForgeScanInterval?.Value ?? 2f; if (Time.time - _lastInfernalForgeCheck < num) { return; } _lastInfernalForgeCheck = Time.time; try { Player instance = Player.Instance; if ((Object)(object)instance == (Object)null) { return; } object instanceValue = ReflectionHelper.GetInstanceValue(instance, "Inventory"); if (instanceValue == null) { instanceValue = ReflectionHelper.GetInstanceValue(instance, "inventory"); } if (instanceValue == null) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)"[InfernalForge] Could not find player inventory"); } return; } GameApis.EnsureInventoryMethodsCached(instanceValue); float maxMana = instance.MaxMana; if (maxMana <= 0f) { return; } float num2 = ReflectionHelper.TryGetValue(instance, "Mana", 0f); if (num2 / maxMana * 100f <= AbilityConfig.InfernalForgeManaThreshold.Value) { return; } int num3 = AbilityConfig.InfernalForgeOrePerBar.Value; if (num3 < 1) { num3 = 3; } MethodInfo addItemIntMethod = GameApis.GetAddItemIntMethod(instanceValue); int num4 = 0; float num5 = 0f; foreach (KeyValuePair<int, int> item in AbilityPatches.OreToBarMap) { int key = item.Key; int value = item.Value; int inventoryAmount = GameApis.GetInventoryAmount(instanceValue, key); if (inventoryAmount < num3) { continue; } float value2; float num6 = (AbilityPatches.OreManaCostMap.TryGetValue(key, out value2) ? value2 : 3f); float num7 = maxMana * (num6 / 100f); int num8 = inventoryAmount / num3; int num9 = num8 * num3; float num10 = num7 * (float)num8; if ((num2 - num5 - num10) / maxMana * 100f < AbilityConfig.InfernalForgeManaThreshold.Value) { num8 = 0; for (int num11 = inventoryAmount / num3; num11 >= 1; num11--) { float num12 = num7 * (float)num11; if ((num2 - num5 - num12) / maxMana * 100f >= AbilityConfig.InfernalForgeManaThreshold.Value) { num8 = num11; break; } } if (num8 <= 0) { continue; } num9 = num8 * num3; num10 = num7 * (float)num8; } if (!GameApis.RemoveInventoryItem(instanceValue, key, num9)) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)$"[InfernalForge] Failed to remove {num9}x ore {key}"); } continue; } bool flag = false; if (addItemIntMethod != null) { flag = GameApis.InvokeAddItem(addItemIntMethod, instanceValue, value, num8, notify: true); } if (!flag) { ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogError((object)$"[InfernalForge] Bar addition failed for {num8}x bar {value} — returning ore"); } GameApis.AddInventoryItem(instanceValue, key, num9); continue; } num4 += num8; num5 += num10; ManualLogSource log4 = Plugin.Log; if (log4 != null) { log4.LogInfo((object)$"[InfernalForge] Smelted {num9}x ore {key} → {num8}x bar {value} (cost: {num6}% per bar)"); } } if (num4 > 0) { float num13 = num2 - num5; if (num13 < 0f) { num13 = 0f; } ReflectionHelper.SetInstanceValue(instance, "Mana", num13); GameApis.SendGameNotification($"Infernal Forge: Smelted {num4} bar(s) (-{num5:F1} Mana)"); ManualLogSource log5 = Plugin.Log; if (log5 != null) { log5.LogInfo((object)$"[InfernalForge] Total: {num4} bars, {num5:F1} mana used"); } } } catch (Exception ex) { ManualLogSource log6 = Plugin.Log; if (log6 != null) { log6.LogWarning((object)("[InfernalForge] Error: " + ex.Message)); } ManualLogSource log7 = Plugin.Log; if (log7 != null) { log7.LogWarning((object)("[InfernalForge] Stack: " + ex.StackTrace)); } } } private void UpdateFontOfLight() { if (!ActiveAbilityManager.IsRuntimeEnabled("FontOfLight")) { return; } float num = AbilityConfig.FontOfLightInterval?.Value ?? 45f; if (Time.time - _lastFontOfLightCheck < num) { return; } _lastFontOfLightCheck = Time.time; try { Player instance = Player.Instance; if ((Object)(object)instance == (Object)null) { return; } float maxMana = instance.MaxMana; float num2 = ReflectionHelper.TryGetValue(instance, "Mana", 0f); if (((maxMana > 0f) ? (num2 / maxMana * 100f) : 0f) >= (AbilityConfig.FontOfLightManaThreshold?.Value ?? 80f)) { return; } int num3 = AbilityConfig.FontOfLightGoldCost?.Value ?? 10; if (num3 > 0) { Type type = AccessTools.TypeByName("Wish.GameSave"); if (type != null && ((ReflectionHelper.GetStaticValue(type, "Coins") is int num4) ? num4 : 0) < num3) { return; } } float num5 = AbilityConfig.FontOfLightManaPercent?.Value ?? 5f; float num6 = maxMana * (num5 / 100f); if (num6 <= 0f) { return; } ReflectionHelper.InvokeMethod(instance, "AddMana", num6, 1f); if (num3 > 0) { MethodInfo methodInfo = AccessTools.Method(((object)instance).GetType(), "AddMoney", new Type[4] { typeof(int), typeof(bool), typeof(bool), typeof(bool) }, (Type[])null); if (methodInfo != null) { methodInfo.Invoke(instance, new object[4] { -num3, false, false, false }); } } GameApis.SendGameNotification($"Font of Light: +{num5:F0}% mana" + ((num3 > 0) ? $" (-{num3} gold)" : "")); } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[FontOfLight] Error: " + ex.Message)); } } } public static float GetCurrentHour() { try { Type type = GameApis.DayCycleType ?? ReflectionHelper.FindWishType("DayCycle"); if (type == null) { return -1f; } object singletonInstance = ReflectionHelper.GetSingletonInstance(type); if (singletonInstance != null) { float num = ReflectionHelper.TryGetValue(singletonInstance, "Hour", -1f); if (num >= 0f) { return num; } num = ReflectionHelper.TryGetValue(singletonInstance, "CurrentHour", -1f); if (num >= 0f) { return num; } num = ReflectionHelper.TryGetValue(singletonInstance, "currentHour", -1f); if (num >= 0f) { return num; } int num2 = ReflectionHelper.TryGetValue(singletonInstance, "Hour", -1); if (num2 >= 0) { return num2; } } return ReflectionHelper.TryGetValue(null, "Hour", -1f); } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("[BirthrightRunner] Failed to read current hour: " + ex.Message)); } return -1f; } } public static string GetCurrentSeason() { try { Type type = GameApis.DayCycleType ?? ReflectionHelper.FindWishType("DayCycle"); if (type == null) { return null; } object singletonInstance = ReflectionHelper.GetSingletonInstance(type); if (singletonInstance != null) { object instanceValue = ReflectionHelper.GetInstanceValue(singletonInstance, "Season"); if (instanceValue != null) { return instanceValue.ToString(); } instanceValue = ReflectionHelper.GetInstanceValue(singletonInstance, "season"); if (instanceValue != null) { return instanceValue.ToString(); } instanceValue = ReflectionHelper.GetInstanceValue(singletonInstance, "CurrentSeason"); if (instanceValue != null) { return instanceValue.ToString(); } } return null; } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("[BirthrightRunner] Failed to read current season: " + ex.Message)); } return null; } } public static bool IsInMine() { string text = SceneHelpers.GetCurrentSceneName().ToLowerInvariant(); if (!text.Contains("mine") && !text.Contains("dungeon") && !text.Contains("cave") && !text.Contains("underground") && !text.Contains("nelvari")) { return text.Contains("withergate"); } return true; } public static bool IsDaytime() { float currentHour = GetCurrentHour(); if (currentHour < 0f) { return true; } if (currentHour >= 6f) { return currentHour < 18f; } return false; } public static float GetPlayerHPRatio() { try { Player instance = Player.Instance; if ((Object)(object)instance == (Object)null) { return 1f; } float maxHealth = instance.MaxHealth; if (maxHealth <= 0f) { return 1f; } return ReflectionHelper.TryGetValue(instance, "health", maxHealth) / maxHealth; } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("[BirthrightRunner] Failed to read player HP ratio: " + ex.Message)); } return 1f; } } private void CheckAbilityToggleHotkey() { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_01c0: Unknown result type (might be due to invalid IL or missing references) try { if (TextInputFocusGuard.ShouldDeferModHotkeys(Plugin.Log) || !Input.GetKeyDown(Plugin.StaticAbilityToggleKey)) { return; } RacialBonusManager racialBonusManager = Plugin.GetRacialBonusManager(); if (racialBonusManager == null) { return; } Race? playerRace = racialBonusManager.GetPlayerRace(); if (!playerRace.HasValue) { RaceDetectionService.RetryRaceDetection(); playerRace = racialBonusManager.GetPlayerRace(); } if (!playerRace.HasValue) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)"[AbilityToggle] F9 pressed but race is not detected yet; waiting for Player/GameSave (try again shortly)."); } return; } RaceDetectionService.RetryRaceDetection(); playerRace = racialBonusManager.GetPlayerRace(); if (!playerRace.HasValue) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogInfo((object)"[AbilityToggle] F9 pressed but race is not detected yet after retry; try again shortly."); } return; } Race value = playerRace.Value; Race race = ElementalVariantResolver.ResolveRaceForActiveAbilityToggle(value); if (race != value) { ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogInfo((object)$"[AbilityToggle] Body-resolved race for F9: {race} (cached was {value})."); } } race = ElementalVariantResolver.ResolveElementalAbilityRace(race); string activeAbilityForRace = GetActiveAbilityForRace(race); if (activeAbilityForRace == null) { ManualLogSource log4 = Plugin.Log; if (log4 != null) { log4.LogInfo((object)$"[AbilityToggle] No toggleable active ability for cached={value} resolved={race} (F9)."); } return; } bool flag = ActiveAbilityManager.ToggleRuntime(activeAbilityForRace); string abilityDisplayName = GetAbilityDisplayName(activeAbilityForRace); GameApis.SendGameNotification(abilityDisplayName + ": " + (flag ? "ON" : "OFF")); ManualLogSource log5 = Plugin.Log; if (log5 != null) { log5.LogInfo((object)("[AbilityToggle] " + (flag ? "ENABLED" : "DISABLED") + " " + abilityDisplayName + " (key=" + activeAbilityForRace + ") | " + $"storedRace={value} abilityRace={race} | " + $"ToggleKey={Plugin.StaticAbilityToggleKey}")); } } catch (Exception ex) { ManualLogSource log6 = Plugin.Log; if (log6 != null) { log6.LogWarning((object)("[BirthrightRunner] Toggle hotkey error: " + ex.Message)); } } } private static string GetActiveAbilityForRace(Race race) { return race switch { Race.FireElemental => "InfernalForge", Race.WaterElemental => "TidalBlessing", Race.Angel => "FontOfLight", Race.Demon => "SoulHarvest", Race.Elemental => "InfernalForge", _ => null, }; } private static string GetAbilityDisplayName(string abilityKey) { return abilityKey switch { "InfernalForge" => "Infernal Forge", "TidalBlessing" => "Tidal Blessing", "FontOfLight" => "Font of Light", "SoulHarvest" => "Soul Harvest", _ => abilityKey, }; } } internal static class BonusTransferRules { private readonly struct Rule { public readonly Race TargetRace; public readonly Race SourceRace; public readonly BonusType BonusType; public Rule(Race targetRace, Race sourceRace, BonusType bonusType) { TargetRace = targetRace; SourceRace = sourceRace; BonusType = bonusType; } } private static readonly List<Rule> Rules = new List<Rule>(); internal static void RebuildFromConfig(string raw) { Rules.Clear(); if (string.IsNullOrWhiteSpace(raw)) { return; } string[] array = raw.Split(new char[1] { ';' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length != 0) { string[] array2 = text.Split(new char[1] { '|' }); Race result; Race result2; BonusType result3; if (array2.Length != 3) { Plugin.Log.LogWarning((object)("[BonusTransfer] Skip invalid rule (need Target|Source|BonusType): \"" + text + "\"")); } else if (!Enum.TryParse<Race>(array2[0].Trim(), ignoreCase: true, out result) || !Enum.TryParse<Race>(array2[1].Trim(), ignoreCase: true, out result2) || !Enum.TryParse<BonusType>(array2[2].Trim(), ignoreCase: true, out result3)) { Plugin.Log.LogWarning((object)("[BonusTransfer] Skip rule with unknown Race/BonusType: \"" + text + "\"")); } else { Rules.Add(new Rule(result, result2, result3)); } } } Plugin.Log.LogInfo((object)$"[BonusTransfer] Loaded {Rules.Count} rule(s)"); } internal static float GetTransferredPercent(RacialBonusManager manager, Race currentRace, BonusType type) { if (Rules.Count == 0 || manager == null || RacialConfig.EnableBonusTransfers == null || !RacialConfig.EnableBonusTransfers.Value) { return 0f; } float num = 0f; foreach (Rule rule in Rules) { if (rule.TargetRace != currentRace || rule.BonusType != type) { continue; } foreach (RacialBonus item in manager.GetBonusesForRace(rule.SourceRace)) { if (item.Type == type) { num += item.Value; break; } } } return num; } } internal static class GameApis { internal static Type TileManagerType; internal static object TileManagerInstance; internal static MethodInfo IsWateredMethod; internal static MethodInfo WaterTileMethod; internal static MethodInfo IsHoedOrWateredMethod; internal static FieldInfo FarmingDataField; internal static FieldInfo FarmingTileMapField; internal static MethodInfo WorldToCellMethod; internal static object GridInstance; internal static MethodInfo GridCellToWorldMethod; private static bool _wishCachesInitialized; private static MethodInfo _cachedGetAmountMethod; private static MethodInfo _cachedRemoveItemMethod; private static bool _inventoryMethodsCached; private static object _notificationStackInstance; private static MethodInfo _sendNotificationMethod; private static bool _notificationInitialized; private static bool _reflectionInitialized; private static bool _reflectionInitFailed; private static int _reflectionInitAttempts; private const int MaxReflectionInitAttempts = 3; private static FieldInfo _cachedItemIdField; private static PropertyInfo _cachedItemIdProperty; private static MethodInfo _cachedItemIdMethod; private static MethodInfo _cachedAddItemIntMethod; private static int _cachedAddItemArgCount; internal static Type CropType { get; private set; } internal static Type DayCycleType { get; private set; } internal static void EnsureWishCachesInitialized() { if (_wishCachesInitialized) { return; } _wishCachesInitialized = true; try { CropType = ReflectionHelper.FindWishType("Crop"); DayCycleType = ReflectionHelper.FindWishType("DayCycle"); if (CropType != null) { Plugin.Log.LogInfo((object)"[GameApis] Found Crop type"); } if (DayCycleType != null) { Plugin.Log.LogInfo((object)"[GameApis] Found DayCycle type"); } CacheTileManager(); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[GameApis] Wish cache init failed: " + ex.Message)); } } internal static void ResetWishCachesInitializedFlag() { _wishCachesInitialized = false; } private static void CacheTileManager() { try { TileManagerType = ReflectionHelper.FindWishType("TileManager"); if (TileManagerType == null) { Plugin.Log.LogWarning((object)"[GameApis] TileManager type not found"); return; } IsWateredMethod = TileManagerType.GetMethod("IsWatered", new Type[1] { typeof(Vector2Int) }); WaterTileMethod = TileManagerType.GetMethod("Water", new Type[2] { typeof(Vector2Int), typeof(short) }); IsHoedOrWateredMethod = TileManagerType.GetMethod("IsHoedOrWatered", new Type[1] { typeof(Vector2Int) }); FarmingDataField = TileManagerType.GetField("farmingData", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); FarmingTileMapField = TileManagerType.GetField("farmingTileMap", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); Plugin.Log.LogInfo((object)("[GameApis] TileManager cached — " + $"IsWatered:{IsWateredMethod != null}, Water:{WaterTileMethod != null}, " + $"farmingData:{FarmingDataField != null}")); TryGetTileManagerInstance(); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[GameApis] TileManager cache failed: " + ex.Message)); } } internal static void TryGetTileManagerInstance() { if (TileManagerInstance != null) { object tileManagerInstance = TileManagerInstance; Object val = (Object)((tileManagerInstance is Object) ? tileManagerInstance : null); if (val == null || !(val == (Object)null)) { return; } ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)"[GameApis] TileManager instance stale — clearing"); } TileManagerInstance = null; } if (TileManagerType == null) { return; } try { TileManagerInstance = ReflectionHelper.GetSingletonInstance(TileManagerType); if (TileManagerInstance != null) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogInfo((object)"[GameApis] TileManager instance acquired"); } if (WorldToCellMethod == null && FarmingTileMapField != null) { try { object value = FarmingTileMapField.GetValue(TileManagerInstance); if (value != null) { WorldToCellMethod = value.GetType().GetMethod("WorldToCell", new Type[1] { typeof(Vector3) }); ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogInfo((object)("[GameApis] WorldToCell: " + ((WorldToCellMethod != null) ? "ok" : "missing"))); } } } catch (Exception ex) { ManualLogSource log4 = Plugin.Log; if (log4 != null) { log4.LogWarning((object)("[GameApis] WorldToCell cache failed: " + ex.Message)); } } } if (GridInstance != null || !(FarmingTileMapField != null)) { return; } try { object value2 = FarmingTileMapField.GetValue(TileManagerInstance); if (value2 == null) { return; } GridInstance = value2.GetType().GetProperty("layoutGrid")?.GetValue(value2); if (GridInstance != null) { GridCellToWorldMethod = GridInstance.GetType().GetMethod("CellToWorld", new Type[1] { typeof(Vector3Int) }); ManualLogSource log5 = Plugin.Log; if (log5 != null) { log5.LogInfo((object)("[GameApis] Grid CellToWorld: " + ((GridCellToWorldMethod != null) ? "ok" : "missing"))); } } return; } catch (Exception ex2) { ManualLogSource log6 = Plugin.Log; if (log6 != null) { log6.LogWarning((object)("[GameApis] Grid cache failed: " + ex2.Message)); } return; } } ManualLogSource log7 = Plugin.Log; if (log7 != null) { log7.LogInfo((object)"[GameApis] TileManager singleton null"); } } catch (Exception ex3) { ManualLogSource log8 = Plugin.Log; if (log8 != null) { log8.LogWarning((object)("[GameApis] TileManager instance lookup failed: " + ex3.Message)); } } } private static void ResetFarmingCaches() { TileManagerInstance = null; WorldToCellMethod = null; GridInstance = null; GridCellToWorldMethod = null; _cachedGetAmountMethod = null; _cachedRemoveItemMethod = null; _inventoryMethodsCached = false; ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)"[GameApis] Farming + inventory reflection reset"); } } internal static void ResetFarmingTileAndInventoryScanState() { ResetFarmingCaches(); ResetWishCachesInitializedFlag(); } internal static void CacheInventoryMethods(object inventory) { try { Type type = inventory.GetType(); _cachedGetAmountMethod = type.GetMethod("GetAmount", new Type[1] { typeof(int) }) ?? type.GetMethod("GetItemAmount", new Type[1] { typeof(int) }); _cachedRemoveItemMethod = type.GetMethod("RemoveAll", new Type[1] { typeof(int) }); Plugin.Log.LogInfo((object)("[GameApis] Inventory methods — GetAmount:" + (_cachedGetAmountMethod?.Name ?? "null") + ", Remove:" + (_cachedRemoveItemMethod?.Name ?? "null"))); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[GameApis] CacheInventoryMethods failed: " + ex.Message)); } } internal static void EnsureInventoryMethodsCached(object inventory) { if (!_inventoryMethodsCached) { CacheInventoryMethods(inventory); _inventoryMethodsCached = true; } } internal static int GetInventoryAmount(object inventory, int itemId) { try { if (_cachedGetAmountMethod != null && _cachedGetAmountMethod.Invoke(inventory, new object[1] { itemId }) is int result) { return result; } if (ReflectionHelper.InvokeMethod(inventory, "GetAmount", itemId) is int result2) { return result2; } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)$"[GameApis] GetInventoryAmount failed for {itemId}: {ex.Message}"); } } return 0; } internal static bool RemoveInventoryItem(object inventory, int itemId, int amount) { try { if (_cachedRemoveItemMethod == null) { return false; } int inventoryAmount = GetInventoryAmount(inventory, itemId); if (inventoryAmount < amount) { return false; } int num = inventoryAmount - amount; _cachedRemoveItemMethod.Invoke(inventory, new object[1] { itemId }); if (num > 0) { AddInventoryItem(inventory, itemId, num); } return true; } catch (Exception ex) { Exception ex2 = ex.InnerException ?? ex; ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[GameApis] RemoveInventoryItem: " + ex2.Message)); } return false; } } internal static void AddInventoryItem(object inventory, int itemId, int amount) { try { MethodInfo addItemIntMethod = GetAddItemIntMethod(inventory); if (addItemIntMethod != null) { InvokeAddItem(addItemIntMethod, inventory, itemId, amount, notify: false); } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogError((object)("[GameApis] AddInventoryItem failed: " + ex.Message)); } } } internal static void ResetAllApiCaches() { ResetItemAndNotificationCaches(); ResetFarmingTileAndInventoryScanState(); } private static void InitializeNotificationSystem() { if (_notificationInitialized) { return; } _notificationInitialized = true; try { Type type = AccessTools.TypeByName("Wish.NotificationStack"); if (type == null) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)"[GameApis] NotificationStack type not found"); } return; } _sendNotificationMethod = AccessTools.Method(type, "SendNotification", new Type[5] { typeof(string), typeof(int), typeof(int), typeof(bool), typeof(bool) }, (Type[])null); if (_sendNotificationMethod == null) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)"[GameApis] SendNotification(string,int,int,bool,bool) not found"); } return; } TryGetNotificationInstance(type); ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogInfo((object)("[GameApis] Notification system initialized" + ((_notificationStackInstance != null) ? " (instance ready)" : " (instance deferred)"))); } } catch (Exception ex) { ManualLogSource log4 = Plugin.Log; if (log4 != null) { log4.LogWarning((object)("[GameApis] Notification init failed: " + ex.Message)); } } } private static void TryGetNotificationInstance(Type notifStackType = null) { if (_notificationStackInstance != null) { return; } try { if (notifStackType == null) { notifStackType = AccessTools.TypeByName("Wish.NotificationStack"); } if (notifStackType == null) { return; } Type type = AccessTools.TypeByName("Wish.SingletonBehaviour`1"); if (type != null) { PropertyInfo propertyInfo = AccessTools.Property(type.MakeGenericType(notifStackType), "Instance"); if (propertyInfo != null) { _notificationStackInstance = propertyInfo.GetValue(null); if (_notificationStackInstance != null) { return; } } } _notificationStackInstance = ReflectionHelper.GetSingletonInstance(notifStackType); if (_notificationStackInstance == null) { MethodInfo method = typeof(Object).GetMethod("FindObjectOfType", Type.EmptyTypes); if (method != null) { _notificationStackInstance = method.MakeGenericMethod(notifStackType).Invoke(null, null); } } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[GameApis] Notification instance lookup failed: " + ex.Message)); } } } internal static void SendGameNotification(string text, int id = 0, int amount = 0, bool unique = false, bool error = false) { try { if (!_notificationInitialized) { InitializeNotificationSystem(); } if (!(_sendNotificationMethod == null)) { if (_notificationStackInstance == null) { TryGetNotificationInstance(); } if (_notificationStackInstance != null) { _sendNotificationMethod.Invoke(_notificationStackInstance, new object[5] { text, id, amount, unique, error }); } } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[GameApis] Notification send failed: " + ex.Message)); } } } private static void InitializeReflectionCache() { if (_reflectionInitialized || _reflectionInitFailed) { return; } _reflectionInitAttempts++; if (_reflectionInitAttempts > 3) { _reflectionInitFailed = true; ManualLogSource log = Plugin.Log; if (log != null) { log.LogError((object)$"[GameApis] Reflection init failed after {3} attempts - giving up"); } return; } try { Type type = AccessTools.TypeByName("Wish.Item"); if (type == null) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)$"[GameApis] Could not find Wish.Item type (attempt {_reflectionInitAttempts}/{3})"); } return; } string[] array = new string[6] { "id", "_id", "itemId", "_itemId", "ItemId", "m_id" }; foreach (string text in array) { _cachedItemIdField = AccessTools.Field(type, text); if (_cachedItemIdField != null) { break; } } array = new string[6] { "id", "Id", "ID", "ItemID", "itemId", "ItemId" }; foreach (string text2 in array) { _cachedItemIdProperty = AccessTools.Property(type, text2); if (_cachedItemIdProperty != null) { break; } } array = new string[5] { "ID", "GetID", "GetId", "GetItemID", "GetItemId" }; foreach (string text3 in array) { _cachedItemIdMethod = AccessTools.Method(type, text3, (Type[])null, (Type[])null); if (_cachedItemIdMethod != null) { break; } } if (_cachedItemIdField == null && _cachedItemIdProperty == null && _cachedItemIdMethod == null) { ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogWarning((object)$"[GameApis] ALL Item ID lookups failed (attempt {_reflectionInitAttempts}/{3})! Members containing 'id':"); } MemberInfo[] members = type.GetMembers(ReflectionHelper.AllBindingFlags); foreach (MemberInfo memberInfo in members) { if (memberInfo.Name.IndexOf("id", StringComparison.OrdinalIgnoreCase) >= 0) { ManualLogSource log4 = Plugin.Log; if (log4 != null) { log4.LogWarning((object)$" - {memberInfo.MemberType} {memberInfo.Name}"); } } } } else { _reflectionInitialized = true; ManualLogSource log5 = Plugin.Log; if (log5 != null) { log5.LogInfo((object)("[GameApis] Item ID reflection ready - field:" + (_cachedItemIdField?.Name ?? "null") + ", prop:" + (_cachedItemIdProperty?.Name ?? "null") + ", method:" + (_cachedItemIdMethod?.Name ?? "null"))); } } } catch (Exception ex) { ManualLogSource log6 = Plugin.Log; if (log6 != null) { log6.LogWarning((object)$"[GameApis] Reflection cache init failed (attempt {_reflectionInitAttempts}): {ex.Message}"); } } } internal static int GetItemId(object item) { if (item == null) { return -1; } InitializeReflectionCache(); try { if (_cachedItemIdField != null && _cachedItemIdField.GetValue(item) is int result) { return result; } if (_cachedItemIdProperty != null && _cachedItemIdProperty.GetValue(item) is int result2) { return result2; } if (_cachedItemIdMethod != null && _cachedItemIdMethod.Invoke(item, null) is int result3) { return result3; } return GetItemIdSlow(item); } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)("[GameApis] GetItemId failed: " + ex.Message)); } return -1; } } private static int GetItemIdSlow(object item) { try { Type type = item.GetType(); FieldInfo[] fields = type.GetFields(ReflectionHelper.AllBindingFlags); foreach (FieldInfo fieldInfo in fields) { if (!(fieldInfo.FieldType == typeof(int)) || fieldInfo.Name.IndexOf("id", StringComparison.OrdinalIgnoreCase) < 0) { continue; } int num = (int)fieldInfo.GetValue(item); if (num > 0) { _cachedItemIdField = fieldInfo; ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)$"[GameApis] Slow path item ID via field '{fieldInfo.Name}' = {num}"); } return num; } } PropertyInfo[] properties = type.GetProperties(ReflectionHelper.AllBindingFlags); foreach (PropertyInfo propertyInfo in properties) { if (!(propertyInfo.PropertyType == typeof(int)) || !propertyInfo.CanRead || propertyInfo.Name.IndexOf("id", StringComparison.OrdinalIgnoreCase) < 0) { continue; } int num2 = (int)propertyInfo.GetValue(item); if (num2 > 0) { _cachedItemIdProperty = propertyInfo; ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogInfo((object)$"[GameApis] Slow path item ID via property '{propertyInfo.Name}' = {num2}"); } return num2; } } } catch (Exception ex) { ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogDebug((object)("[GameApis] GetItemIdSlow failed: " + ex.Message)); } } return -1; } internal static MethodInfo GetAddItemIntMethod(object inventory) { if (_cachedAddItemIntMethod != null) { return _cachedAddItemIntMethod; } try { Type type = inventory.GetType(); _cachedAddItemIntMethod = AccessTools.Method(type, "AddItem", new Type[3] { typeof(int), typeof(int), typeof(bool) }, (Type[])null); if (_cachedAddItemIntMethod != null) { _cachedAddItemArgCount = 3; ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)"[GameApis] Cached Inventory.AddItem(int, int, bool)"); } return _cachedAddItemIntMethod; } _cachedAddItemIntMethod = AccessTools.Method(type, "AddItem", new Type[2] { typeof(int), typeof(int) }, (Type[])null); if (_cachedAddItemIntMethod != null) { _cachedAddItemArgCount = 2; ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogInfo((object)"[GameApis] Cached Inventory.AddItem(int, int)"); } return _cachedAddItemIntMethod; } ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogWarning((object)"[GameApis] Could not find Inventory.AddItem(int,...) overloads:"); } MethodInfo[] methods = type.GetMethods(ReflectionHelper.AllBindingFlags); foreach (MethodInfo methodInfo in methods) { if (methodInfo.Name == "AddItem") { ParameterInfo[] parameters = methodInfo.GetParameters(); string text = string.Join(", ", Array.ConvertAll(parameters, (ParameterInfo p) => p.ParameterType.Name + " " + p.Name)); ManualLogSource log4 = Plugin.Log; if (log4 != null) { log4.LogWarning((object)(" - AddItem(" + text + ")")); } } } } catch (Exception ex) { ManualLogSource log5 = Plugin.Log; if (log5 != null) { log5.LogWarning((object)("[GameApis] Failed to cache AddItem method: " + ex.Message)); } } return _cachedAddItemIntMethod; } internal static bool InvokeAddItem(MethodInfo addMethod, object inventory, int itemId, int amount, bool notify) { try { if (_cachedAddItemArgCount == 3) { addMethod.Invoke(inventory, new object[3] { itemId, amount, notify }); return true; } if (_cachedAddItemArgCount == 2) { addMethod.Invoke(inventory, new object[2] { itemId, amount }); return true; } ManualLogSource log = Plugin.Log; if (log != null) { log.LogError((object)$"[GameApis] InvokeAddItem: bad arg count {_cachedAddItemArgCount}"); } return false; } catch (Exception ex) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogError((object)$"[GameApis] InvokeAddItem threw: {ex.Message}. Item {itemId} x{amount}"); } return false; } } internal static void ResetItemAndNotificationCaches() { _notificationStackInstance = null; _notificationInitialized = false; _sendNotificationMethod = null; _reflectionInitialized = false; _reflectionInitFailed = false; _reflectionInitAttempts = 0; _cachedAddItemIntMethod = null; _cachedAddItemArgCount = 0; _cachedItemIdField = null; _cachedItemIdProperty = null; _cachedItemIdMethod = null; ManualLogSource log = Plugin.Log; if (log != null) { log.LogDebug((object)"[GameApis] Item + notification caches reset"); } } } [BepInPlugin("com.azraelgodking.havensbirthright", "Haven's Birthright", "3.1.1")] public class Plugin : BaseUnityPlugin { internal static KeyCode StaticAbilityToggleKey = (KeyCode)290; internal static KeyCode StaticReloadConfigKey = (KeyCode)293; private Harmony _harmony; private RacialBonusManager _racialBonusManager; private ConfigEntry<bool> _checkForUpdates; private ConfigEntry<KeyCode> _reloadConfigKey; private BirthrightRunner _runner; private EventHandler _updateKeybindsHandler; private EventHandler _rebuildCrossRaceRulesHandler; private bool _applicationQuitting; public static Plugin Instance { get; private set; } public static ManualLogSource Log { get; private set; } public static ConfigFile ConfigFile { get; private set; } public static bool CriticalBirthrightHarmonyIncomplete { get; private set; } private void Awake() { //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00c7: Unknown result type (might be due to invalid IL or missing references) //IL_00fd: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Expected O, but got Unknown //IL_01d7: Unknown result type (might be due to invalid IL or missing references) //IL_01e4: Expected O, but got Unknown //IL_02ec: Unknown result type (might be due to invalid IL or missing references) //IL_02fa: Expected O, but got Unknown //IL_03bb: Unknown result type (might be due to invalid IL or missing references) //IL_03c8: Expected O, but got Unknown //IL_05d8: Unknown result type (might be due to invalid IL or missing references) //IL_05f6: Unknown result type (might be due to invalid IL or missing references) Instance = this; Log = ((BaseUnityPlugin)this).Logger; ConfigFile = CreateNamedConfig(); ConfigFileHelper.ReplacePluginConfig((BaseUnityPlugin)(object)this, ConfigFile, (Action<string>)Log.LogWarning); Log.LogInfo((object)"Loading Haven's Birthright v3.1.1"); try { RacialConfig.Initialize(ConfigFile); AbilityConfig.Initialize(ConfigFile); StaticAbilityToggleKey = AbilityConfig.ActiveAbilityToggleKey.Value; _checkForUpdates = ConfigFile.Bind<bool>("Up