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.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+5c08b5aa5d0be9c4b93df77f697dc55d5ac97088")] [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] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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] [Microsoft.CodeAnalysis.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] [Microsoft.CodeAnalysis.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 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 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("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 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 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 HealthByPluginGuid = new Dictionary(StringComparer.OrdinalIgnoreCase); private static readonly object HealthLock = new object(); private static readonly Dictionary ModPatternCache = new Dictionary(StringComparer.Ordinal); private static readonly object ModPatternCacheLock = new object(); private static readonly Regex ExtractFieldRegex = new Regex("\"(?[^\"]+)\"\\s*:\\s*(?:\"(?[^\"]*)\"|null)", RegexOptions.Compiled); public static void CheckForUpdate(string pluginGuid, string currentVersion, ManualLogSource logger = null, Action onComplete = null) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) TouchHealth(pluginGuid); VersionCheckRunner versionCheckRunner = new GameObject("VersionChecker").AddComponent(); 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 NoKillSubstrings = new List(); 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 list; lock (Lock) { if (NoKillSubstrings.Count == 0) { return; } list = new List(NoKillSubstrings); } List list2 = new List(__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) { string typeName2 = typeName; Type type = AccessTools.TypeByName(typeName2); if (type != null) { return type; } for (int i = 0; i < namespaces.Length; i++) { type = AccessTools.TypeByName(namespaces[i] + "." + typeName2); if (type != null) { return type; } } Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { try { type = assembly.GetTypes().FirstOrDefault((Type t) => t.Name == typeName2 || t.FullName == typeName2); if (type != null) { return type; } } catch (ReflectionTypeLoadException) { } } return null; } 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[] fields = type.GetFields(AllBindingFlags); IEnumerable second; if (!(type.BaseType != null) || !(type.BaseType != typeof(object))) { second = Enumerable.Empty(); } else { IEnumerable 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[] properties = type.GetProperties(AllBindingFlags); IEnumerable second; if (!(type.BaseType != null) || !(type.BaseType != typeof(object))) { second = Enumerable.Empty(); } else { IEnumerable 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(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() 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(); } 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() != (Object)null) { flag = true; } else if (TryGetTmpInputField(val)) { flag = true; } } } if (!flag && IsQuantumConsoleActive(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; } private static bool IsQuantumConsoleActive(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; } } } namespace HavensBirthright { public class BirthrightRunner : PersistentRunnerBase { private float _outdoorTime; private float _lastTidalBlessingCheck; private float _lastInfernalForgeCheck; private float _lastFontOfLightCheck; 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 (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; _tidalBlessingDiagLogged = false; } 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 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_0333: Unknown result type (might be due to invalid IL or missing references) //IL_0348: 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); } 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) { Plugin.Log.LogWarning((object)("[TidalBlessing] Error: " + ex2.Message)); ManualLogSource log13 = Plugin.Log; if (log13 != null) { log13.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 dictionary) || dictionary.Count == 0) { 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 list = new List(); 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.LogInfo((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; 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 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 Rules = new List(); 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(array2[0].Trim(), ignoreCase: true, out result) || !Enum.TryParse(array2[1].Trim(), ignoreCase: true, out result2) || !Enum.TryParse(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; } object obj = ReflectionHelper.InvokeMethod(inventory, "GetAmount", itemId); if (obj is int) { return (int)obj; } } 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", "2.2.2")] public class Plugin : BaseUnityPlugin { internal static KeyCode StaticAbilityToggleKey = (KeyCode)290; internal static KeyCode StaticReloadConfigKey = (KeyCode)293; private Harmony _harmony; private RacialBonusManager _racialBonusManager; private ConfigEntry _checkForUpdates; private ConfigEntry _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_0578: Unknown result type (might be due to invalid IL or missing references) //IL_0596: 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)Log.LogWarning); Log.LogInfo((object)"Loading Haven's Birthright v2.2.2"); try { RacialConfig.Initialize(ConfigFile); AbilityConfig.Initialize(ConfigFile); StaticAbilityToggleKey = AbilityConfig.ActiveAbilityToggleKey.Value; _checkForUpdates = ConfigFile.Bind("Updates", "CheckForUpdates", true, "Check for mod updates on startup"); _reloadConfigKey = ConfigFile.Bind("General", "ReloadConfigKey", (KeyCode)293, "Key to reload config from file (edit the .cfg file, then press this key in-game to apply)"); StaticAbilityToggleKey = AbilityConfig.ActiveAbilityToggleKey.Value; StaticReloadConfigKey = _reloadConfigKey.Value; SubscribeConfigChanged(); _racialBonusManager = new RacialBonusManager(); _runner = PersistentRunnerBase.CreateRunner(); Log.LogInfo((object)"BirthrightRunner created for active abilities"); _harmony = new Harmony("com.azraelgodking.havensbirthright"); try { Type typeFromHandle = typeof(Player); Log.LogInfo((object)("Player type: " + typeFromHandle.FullName + " from " + typeFromHandle.Assembly.GetName().Name)); PatchMethod(typeFromHandle, "InitializeAsOwner", typeof(PlayerPatches), "OnPlayerInitialized", null, essential: true); PatchMethod(typeFromHandle, "Initialize", typeof(PlayerPatches), "OnPlayerInitialize", Type.EmptyTypes, essential: true); Type type = AccessTools.TypeByName("Wish.GameSave"); if (type != null) { MethodInfo methodInfo = AccessTools.Method(type, "LoadCharacter", new Type[1] { typeof(int) }, (Type[])null); if (methodInfo != null) { _harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(PlayerPatches), "OnGameSaveLoadCharacter", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Log.LogInfo((object)"Patched GameSave.LoadCharacter (race/actives reset on character load)"); } else { Log.LogWarning((object)"Could not find GameSave.LoadCharacter(int) — character-switch reset may be incomplete"); } } PatchMethod(typeFromHandle, "GetStat", typeof(StatPatches), "ModifyGetStat", new Type[1] { typeof(StatType) }, essential: true); PatchMethodPrefix(typeFromHandle, "ReceiveDamage", typeof(CombatPatches), "ModifyDamageReceived", null, essential: true); PatchMethod(typeFromHandle, "ReceiveDamage", typeof(CombatPatches), "OnDamageReceivedPostfix", null, essential: true); Type type2 = AccessTools.TypeByName("Wish.NPCAI"); if (type2 != null) { MethodInfo methodInfo2 = AccessTools.Method(type2, "AddRelationship", new Type[3] { typeof(float), typeof(float), typeof(bool) }, (Type[])null); if (methodInfo2 != null) { MethodInfo methodInfo3 = AccessTools.Method(typeof(EconomyPatches), "ModifyRelationshipGain", (Type[])null, (Type[])null); _harmony.Patch((MethodBase)methodInfo2, new HarmonyMethod(methodInfo3), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Log.LogInfo((object)"Successfully patched NPCAI.AddRelationship"); } else { Log.LogWarning((object)"Could not find NPCAI.AddRelationship - relationship bonuses will not work"); } } else { Log.LogWarning((object)"Could not find NPCAI type - relationship bonuses will not work"); } PatchShopBuyItemForDiscount(); PatchMethodPrefix(typeFromHandle, "AddMana", typeof(AbilityPatches), "OnPlayerAddManaPrefix", null, essential: true); Type type3 = AccessTools.TypeByName("Wish.EnemyAI"); if (type3 != null) { MethodInfo methodInfo4 = AccessTools.Method(type3, "Die", new Type[1] { typeof(bool) }, (Type[])null); if (methodInfo4 != null) { MethodInfo methodInfo5 = AccessTools.Method(typeof(SoulHarvestPatches), "OnEnemyDiePostfix", (Type[])null, (Type[])null); if (methodInfo5 != null) { _harmony.Patch((MethodBase)methodInfo4, (HarmonyMethod)null, new HarmonyMethod(methodInfo5), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Log.LogInfo((object)"Successfully patched EnemyAI.Die (Soul Harvest)"); } } } IEnumerable patchedMethods = _harmony.GetPatchedMethods(); int num = 0; foreach (MethodBase item in patchedMethods) { Log.LogInfo((object)("Patched: " + item.DeclaringType?.Name + "." + item.Name)); num++; } Log.LogInfo((object)$"Total methods patched: {num}"); if (CriticalBirthrightHarmonyIncomplete) { Log.LogError((object)"[Haven's Birthright] One or more essential Harmony patches failed — racial stat/combat hooks are disabled for this session. Check the log above for 'Could not find method' / patch errors."); } } catch (Exception arg) { Log.LogError((object)$"Harmony patching failed: {arg}"); CriticalBirthrightHarmonyIncomplete = true; } if (_checkForUpdates.Value) { VersionChecker.CheckForUpdate("com.azraelgodking.havensbirthright", "2.2.2", Log, delegate(VersionChecker.VersionCheckResult result) { result.NotifyUpdateAvailable(Log); }); } Log.LogInfo((object)"Haven's Birthright loaded successfully!"); Log.LogInfo((object)("Active abilities: " + (AbilityConfig.EnableActiveAbilities.Value ? "ENABLED" : "DISABLED"))); Log.LogInfo((object)("Racial drawbacks: " + (AbilityConfig.EnableRacialDrawbacks.Value ? "ENABLED" : "DISABLED"))); Log.LogInfo((object)("Conditional synergies: " + (AbilityConfig.EnableConditionalSynergies.Value ? "ENABLED" : "DISABLED"))); Log.LogInfo((object)$"Ability toggle key: {StaticAbilityToggleKey}"); Log.LogInfo((object)$"Config reload key: {StaticReloadConfigKey} (edit .cfg then press in-game to apply)"); } catch (Exception arg2) { Log.LogError((object)string.Format("Failed to load {0}: {1}", "Haven's Birthright", arg2)); } } private void SubscribeConfigChanged() { _updateKeybindsHandler = delegate { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) StaticAbilityToggleKey = AbilityConfig.ActiveAbilityToggleKey.Value; StaticReloadConfigKey = _reloadConfigKey.Value; }; AbilityConfig.ActiveAbilityToggleKey.SettingChanged += _updateKeybindsHandler; _reloadConfigKey.SettingChanged += _updateKeybindsHandler; _rebuildCrossRaceRulesHandler = delegate { BonusTransferRules.RebuildFromConfig((RacialConfig.CrossRaceBonusRules != null) ? RacialConfig.CrossRaceBonusRules.Value : ""); }; RacialConfig.CrossRaceBonusRules.SettingChanged += _rebuildCrossRaceRulesHandler; RacialConfig.EnableBonusTransfers.SettingChanged += _rebuildCrossRaceRulesHandler; } public void ReloadConfig() { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) try { ConfigFile.Reload(); RacialConfig.Initialize(ConfigFile); AbilityConfig.Initialize(ConfigFile); StaticAbilityToggleKey = AbilityConfig.ActiveAbilityToggleKey.Value; StaticReloadConfigKey = _reloadConfigKey.Value; _racialBonusManager = new RacialBonusManager(); ManualLogSource log = Log; if (log != null) { log.LogInfo((object)"[Haven's Birthright] Config reloaded from file"); } } catch (Exception ex) { ManualLogSource log2 = Log; if (log2 != null) { log2.LogError((object)("[Haven's Birthright] Config reload failed: " + ex.Message)); } } } private static ConfigFile CreateNamedConfig() { //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Expected O, but got Unknown string text = Path.Combine(Paths.ConfigPath, "HavensBirthright.cfg"); string text2 = Path.Combine(Paths.ConfigPath, "com.azraelgodking.havensbirthright.cfg"); try { if (!File.Exists(text) && File.Exists(text2)) { File.Copy(text2, text); } } catch (Exception ex) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("[Config] Migration to HavensBirthright.cfg failed: " + ex.Message)); } } return new ConfigFile(text, true); } private void OnApplicationQuit() { _applicationQuitting = true; } private void OnDestroy() { //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) try { if (_updateKeybindsHandler != null) { AbilityConfig.ActiveAbilityToggleKey.SettingChanged -= _updateKeybindsHandler; _reloadConfigKey.SettingChanged -= _updateKeybindsHandler; } if (_rebuildCrossRaceRulesHandler != null) { RacialConfig.CrossRaceBonusRules.SettingChanged -= _rebuildCrossRaceRulesHandler; RacialConfig.EnableBonusTransfers.SettingChanged -= _rebuildCrossRaceRulesHandler; } } catch (Exception ex) { ManualLogSource log = Log; if (log != null) { log.LogDebug((object)("[Lifecycle] Config handler teardown encountered an issue: " + ex.Message)); } } Scene activeScene = SceneManager.GetActiveScene(); string text = ((Scene)(ref activeScene)).name ?? string.Empty; string text2 = text.ToLowerInvariant(); if (_applicationQuitting || !Application.isPlaying || text2.Contains("menu") || text2.Contains("title")) { ManualLogSource log2 = Log; if (log2 != null) { log2.LogInfo((object)("[Lifecycle] Plugin OnDestroy during expected teardown (scene: " + text + ")")); } } else { ManualLogSource log3 = Log; if (log3 != null) { log3.LogWarning((object)("[Lifecycle] Plugin OnDestroy outside expected teardown (scene: " + text + ")")); } } Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } private void PatchMethod(Type targetType, string methodName, Type patchType, string patchMethodName, Type[] parameters = null, bool essential = false) { //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Expected O, but got Unknown try { MethodInfo methodInfo = ((parameters == null) ? AccessTools.Method(targetType, methodName, (Type[])null, (Type[])null) : AccessTools.Method(targetType, methodName, parameters, (Type[])null)); if (methodInfo == null) { Log.LogWarning((object)("Could not find method " + targetType.Name + "." + methodName)); if (essential) { CriticalBirthrightHarmonyIncomplete = true; } return; } MethodInfo methodInfo2 = AccessTools.Method(patchType, patchMethodName, (Type[])null, (Type[])null); if (methodInfo2 == null) { Log.LogWarning((object)("Could not find patch method " + patchType.Name + "." + patchMethodName)); if (essential) { CriticalBirthrightHarmonyIncomplete = true; } } else { _harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Log.LogInfo((object)("Successfully patched " + targetType.Name + "." + methodName)); } } catch (Exception ex) { Log.LogError((object)("Failed to patch " + targetType.Name + "." + methodName + ": " + ex.Message)); if (essential) { CriticalBirthrightHarmonyIncomplete = true; } } } private void PatchMethodPrefix(Type targetType, string methodName, Type patchType, string patchMethodName, Type[] parameters = null, bool essential = false) { //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Expected O, but got Unknown try { MethodInfo methodInfo = ((parameters == null) ? AccessTools.Method(targetType, methodName, (Type[])null, (Type[])null) : AccessTools.Method(targetType, methodName, parameters, (Type[])null)); if (methodInfo == null) { Log.LogWarning((object)("Could not find method " + targetType.Name + "." + methodName)); if (essential) { CriticalBirthrightHarmonyIncomplete = true; } return; } MethodInfo methodInfo2 = AccessTools.Method(patchType, patchMethodName, (Type[])null, (Type[])null); if (methodInfo2 == null) { Log.LogWarning((object)("Could not find patch method " + patchType.Name + "." + patchMethodName)); if (essential) { CriticalBirthrightHarmonyIncomplete = true; } } else { _harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Log.LogInfo((object)("Successfully patched " + targetType.Name + "." + methodName + " (prefix)")); } } catch (Exception ex) { Log.LogError((object)("Failed to patch " + targetType.Name + "." + methodName + ": " + ex.Message)); if (essential) { CriticalBirthrightHarmonyIncomplete = true; } } } private void PatchShopBuyItemForDiscount() { try { Type type = AccessTools.TypeByName("Wish.Shop"); if (type == null) { Log.LogWarning((object)"Could not find Wish.Shop type - shop discounts will not work"); return; } Type type2 = AccessTools.TypeByName("Wish.ShopItemInfo2"); Type type3 = AccessTools.TypeByName("Wish.ShopLoot2"); if (type2 != null && AccessTools.Method(type, "BuyItem", new Type[2] { type2, typeof(int) }, (Type[])null) != null) { PatchMethodPrefix(type, "BuyItem", typeof(EconomyPatches), "OnBeforeShopBuyItem", new Type[2] { type2, typeof(int) }); } if (type3 != null) { if (AccessTools.Method(type, "BuyItem", new Type[2] { type3, typeof(int) }, (Type[])null) != null) { PatchMethodPrefix(type, "BuyItem", typeof(EconomyPatches), "OnBeforeShopBuyItem", new Type[2] { type3, typeof(int) }); } if (AccessTools.Method(type, "BuyItem", new Type[1] { type3 }, (Type[])null) != null) { PatchMethodPrefix(type, "BuyItem", typeof(EconomyPatches), "OnBeforeShopBuyItemSingle", new Type[1] { type3 }); } } } catch (Exception ex) { Log.LogError((object)("Failed to patch Shop for discount: " + ex.Message)); } } public static RacialBonusManager GetRacialBonusManager() { return Instance?._racialBonusManager; } internal static BirthrightRunner GetRunner() { return Instance?._runner; } public static void EnsureRunner() { if ((Object)(object)Instance == (Object)null) { return; } if (!((Object)(object)Instance._runner == (Object)null)) { Object runner = (Object)(object)Instance._runner; if (runner == null || !(runner == (Object)null)) { return; } } Instance._runner = PersistentRunnerBase.CreateRunner(); ManualLogSource log = Log; if (log != null) { log.LogInfo((object)"[Plugin] BirthrightRunner recreated after destruction"); } } } public static class PluginInfo { public const string PLUGIN_GUID = "com.azraelgodking.havensbirthright"; public const string PLUGIN_NAME = "Haven's Birthright"; public const string PLUGIN_VERSION = "2.2.2"; } internal static class ElementalVariantResolver { internal static Race ResolveElementalFromBodyStyleName(string bodyStyleName) { if (string.IsNullOrEmpty(bodyStyleName)) { Plugin.Log.LogWarning((object)"[RaceDetection] No body style name for Elemental, using generic"); return Race.Elemental; } Plugin.Log.LogInfo((object)("[RaceDetection] Elemental body style (key 14): " + bodyStyleName)); string text = bodyStyleName.ToLowerInvariant(); if (text.Contains("fire")) { return Race.FireElemental; } if (text.Contains("water")) { return Race.WaterElemental; } Plugin.Log.LogWarning((object)("[RaceDetection] Could not determine Elemental variant from '" + bodyStyleName + "', using generic")); return Race.Elemental; } internal static Race ResolveElementalAbilityRace(Race race) { if (race != Race.Elemental) { return race; } return ResolveElementalFromBodyStyleName(CharacterFingerprint.GetCurrentBodyStyleName()); } internal static Race ResolveRaceForActiveAbilityToggle(Race stored) { if (stored != Race.WaterElemental && stored != Race.FireElemental && stored != Race.Elemental) { return stored; } string currentBodyStyleName = CharacterFingerprint.GetCurrentBodyStyleName(); if (string.IsNullOrEmpty(currentBodyStyleName)) { return stored; } Race race = ResolveElementalFromBodyStyleName(currentBodyStyleName); if (race == Race.WaterElemental || race == Race.FireElemental) { return race; } return stored; } } internal static class RaceDetectionService { private static bool _raceDetected; private const int SUBRACE_DEFAULT = 0; private const int SUBRACE_CAT = 1; private const int SUBRACE_DOG = 2; private const int SUBRACE_BIRD = 3; private const int SUBRACE_AQUATIC = 4; private const int SUBRACE_GREEN_REPTILE = 5; private const int SUBRACE_ORANGE_REPTILE = 6; internal static void DetectFromPlayerInitialized() { _raceDetected = false; DetectAndSetRace(); } internal static void DetectIfNeeded() { if (!_raceDetected) { DetectAndSetRace(); } } internal static void ResetRaceDetection() { _raceDetected = false; } internal static void RetryRaceDetection() { RacialBonusManager racialBonusManager = Plugin.GetRacialBonusManager(); if (racialBonusManager != null && !racialBonusManager.GetPlayerRace().HasValue) { _raceDetected = false; } else if (_raceDetected && racialBonusManager != null && CachedElementalContradictsBody(racialBonusManager)) { Plugin.Log.LogInfo((object)"[RaceDetection] Cached elemental race disagrees with body style; re-detecting."); _raceDetected = false; } if (!_raceDetected) { DetectAndSetRace(); } } private static bool CachedElementalContradictsBody(RacialBonusManager mgr) { Race? playerRace = mgr.GetPlayerRace(); if (!playerRace.HasValue) { return false; } Race value = playerRace.Value; if (value != Race.WaterElemental && value != Race.FireElemental && value != Race.Elemental) { return false; } string currentBodyStyleName = CharacterFingerprint.GetCurrentBodyStyleName(); if (string.IsNullOrEmpty(currentBodyStyleName)) { return false; } Race race = ElementalVariantResolver.ResolveElementalFromBodyStyleName(currentBodyStyleName); if (value == Race.Elemental) { if (race != Race.WaterElemental) { return race == Race.FireElemental; } return true; } if (race != Race.WaterElemental && race != Race.FireElemental) { return false; } return value != race; } private static void DetectAndSetRace() { //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_009f: 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_00b3: Expected I4, but got Unknown if (_raceDetected) { return; } try { if ((Object)(object)Player.Instance == (Object)null) { return; } CharacterData authoritativeCharacterData = CharacterFingerprint.GetAuthoritativeCharacterData(); if (authoritativeCharacterData == null || authoritativeCharacterData.StyleData == null || authoritativeCharacterData.StyleData.Count == 0) { return; } RacialBonusManager racialBonusManager = Plugin.GetRacialBonusManager(); if (racialBonusManager == null) { Plugin.Log.LogError((object)"RacialBonusManager is NULL - cannot proceed"); return; } byte race = authoritativeCharacterData.race; Race val = (Race)race; Plugin.Log.LogInfo((object)$"[RaceDetection] Tier 1 - Game race byte: {race}, Wish.Race: {val}"); string value = null; authoritativeCharacterData.StyleData.TryGetValue(14, out value); int num = -1; Race race2; switch (val - 2) { case 0: num = TryGetSubRaceFromClothingData(value); if (num >= 0) { Plugin.Log.LogInfo((object)$"[RaceDetection] Tier 2 - Amari SubRace from ClothingData: {num}"); race2 = ResolveAmariRace(num); } else { Plugin.Log.LogInfo((object)"[RaceDetection] Tier 2 - ClothingData lookup failed, falling back to string parsing"); race2 = ResolveAmariFromString(value); } break; case 2: race2 = ElementalVariantResolver.ResolveElementalFromBodyStyleName(value); break; case 1: race2 = Race.Naga; num = TryGetSubRaceFromClothingData(value); Plugin.Log.LogInfo((object)$"[RaceDetection] Tier 2 - Naga SubRace: {num} (no variant needed)"); break; default: race2 = ConvertGameRaceByName(((object)(Race)(ref val)).ToString()); break; } racialBonusManager.SetPlayerRace(race2, num); _raceDetected = true; Plugin.Log.LogInfo((object)$"Player race set to: {race2} (SubRace cached: {num})"); CharacterSessionController.SyncTrackedCharacterIdFromSave(); } catch (Exception ex) { Plugin.Log.LogError((object)("!!! CRITICAL ERROR in race detection: " + ex.Message)); Plugin.Log.LogError((object)("Stack trace: " + ex.StackTrace)); } } private static int TryGetSubRaceFromClothingData(string bodyStyleName) { if (string.IsNullOrEmpty(bodyStyleName)) { return -1; } try { Type type = ReflectionHelper.FindWishType("CharacterClothingStyles"); if (type == null) { Plugin.Log.LogDebug((object)"[RaceDetection] Could not find CharacterClothingStyles type"); return -1; } object staticValue = ReflectionHelper.GetStaticValue(type, "ClothingStyles"); if (staticValue == null) { Plugin.Log.LogDebug((object)"[RaceDetection] ClothingStyles is null"); return -1; } object obj = ReflectionHelper.InvokeMethod(staticValue, "get_Item", (object)(ClothingLayer)14); if (obj == null) { Plugin.Log.LogDebug((object)"[RaceDetection] No Body layer in ClothingStyles"); return -1; } MethodInfo method = obj.GetType().GetMethod("ContainsKey"); if (method != null && !(bool)method.Invoke(obj, new object[1] { bodyStyleName })) { Plugin.Log.LogDebug((object)("[RaceDetection] Body style '" + bodyStyleName + "' not found in ClothingStyles[Body]")); return -1; } object obj2 = ReflectionHelper.InvokeMethod(obj, "get_Item", bodyStyleName); if (obj2 == null) { Plugin.Log.LogDebug((object)("[RaceDetection] ClothingLayerData is null for '" + bodyStyleName + "'")); return -1; } object instanceValue = ReflectionHelper.GetInstanceValue(obj2, "subRace"); if (instanceValue == null) { Plugin.Log.LogDebug((object)"[RaceDetection] subRace field not found on ClothingLayerData"); return -1; } int num = (int)instanceValue; Plugin.Log.LogInfo((object)$"[RaceDetection] ClothingLayerData.subRace = {instanceValue} ({num})"); return num; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[RaceDetection] ClothingLayerData lookup failed: " + ex.Message)); return -1; } } private static Race ResolveAmariRace(int subRace) { switch (subRace) { case 1: return Race.AmariCat; case 2: return Race.AmariDog; case 3: return Race.AmariBird; case 4: return Race.AmariAquatic; case 5: case 6: return Race.AmariReptile; default: Plugin.Log.LogWarning((object)$"[RaceDetection] Unknown Amari SubRace: {subRace}, using generic Amari"); return Race.Amari; } } private static Race ResolveAmariFromString(string bodyStyleName) { if (string.IsNullOrEmpty(bodyStyleName)) { Plugin.Log.LogWarning((object)"[RaceDetection] No body style name for Amari, using generic"); return Race.Amari; } Plugin.Log.LogInfo((object)("[RaceDetection] Amari string fallback, body style: " + bodyStyleName)); string text = bodyStyleName.ToLowerInvariant(); if (text.Contains("cat")) { return Race.AmariCat; } if (text.Contains("dog") || text.Contains("wolf") || text.Contains("canine")) { return Race.AmariDog; } if (text.Contains("bird") || text.Contains("avian") || text.Contains("feather")) { return Race.AmariBird; } if (text.Contains("aquatic") || text.Contains("fish") || text.Contains("amphibian") || text.Contains("frog")) { return Race.AmariAquatic; } if (text.Contains("reptile") || text.Contains("lizard") || text.Contains("dragon") || text.Contains("snake")) { return Race.AmariReptile; } Plugin.Log.LogWarning((object)("[RaceDetection] Could not determine Amari variant from '" + bodyStyleName + "', using generic")); return Race.Amari; } private static Race ConvertGameRaceByName(string raceName) { string text = raceName?.ToLowerInvariant() ?? ""; if (text.Contains("human")) { return Race.Human; } if (text.Contains("elf")) { return Race.Elf; } if (text.Contains("angel")) { return Race.Angel; } if (text.Contains("demon")) { return Race.Demon; } if (text.Contains("naga")) { return Race.Naga; } if (text.Contains("fire") && text.Contains("element")) { return Race.FireElemental; } if (text.Contains("water") && text.Contains("element")) { return Race.WaterElemental; } if (text.Contains("element")) { return Race.Elemental; } if (text.Contains("amari")) { return Race.Amari; } Plugin.Log.LogWarning((object)("Unknown race name: " + raceName + ", defaulting to Human")); return Race.Human; } } public enum Race { Human, Elf, Angel, Demon, Elemental, FireElemental, WaterElemental, Amari, AmariCat, AmariDog, AmariBird, AmariAquatic, AmariReptile, Naga } public enum ElementalVariant { None, Fire, Water } public enum BonusType { MeleeStrength, MagicPower, Defense, CriticalChance, AttackSpeed, FarmingSpeed, CropQuality, CropYield, WateringEfficiency, MiningSpeed, MiningYield, WoodcuttingSpeed, WoodcuttingYield, FishingSpeed, FishingLuck, ForagingChance, CraftingSpeed, CraftingQuality, RelationshipGain, ShopDiscount, MovementSpeed, MaxHealth, MaxMana, ManaRegen, HealthRegen, ExperienceGain, GoldFind, LuckBonus, DodgeChance, FishingMinigameSpeed, WoodcuttingDamage, MiningDamage, SpellAttackSpeed, AirSkipChance, CommunityTokenGain, TripleGoldChance } public class RacialBonus { public BonusType Type { get; set; } public float Value { get; set; } public bool IsPercentage { get; set; } public string Description { get; set; } public RacialBonus(BonusType type, float value, bool isPercentage, string description) { Type = type; Value = value; IsPercentage = isPercentage; Description = description; } public string GetFormattedValue() { if (IsPercentage) { if (!(Value >= 0f)) { return $"{Value}%"; } return $"+{Value}%"; } if (!(Value >= 0f)) { return $"{Value}"; } return $"+{Value}"; } } public class RacialBonusManager { private Dictionary> _racialBonuses; private Race? _currentPlayerRace; private int _cachedGameSubRace = -1; public RacialBonusManager() { _racialBonuses = new Dictionary>(); InitializeDefaultBonuses(); } private void InitializeDefaultBonuses() { _racialBonuses[Race.Human] = new List { new RacialBonus(BonusType.ExperienceGain, RacialConfig.HumanExpBonus.Value, isPercentage: true, "Adaptable: Gain experience faster"), new RacialBonus(BonusType.RelationshipGain, RacialConfig.HumanRelationshipBonus.Value, isPercentage: true, "Charismatic: Build relationships faster"), new RacialBonus(BonusType.ShopDiscount, RacialConfig.HumanShopDiscount.Value, isPercentage: true, "Silver Tongue: Small discount at shops"), new RacialBonus(BonusType.CommunityTokenGain, RacialConfig.HumanCommunityTokenBonus.Value, isPercentage: true, "Community Leader: Earn more community tokens") }; _racialBonuses[Race.Elf] = new List { new RacialBonus(BonusType.FarmingSpeed, RacialConfig.ElfFarmingBonus.Value, isPercentage: true, "Nature's Touch: Farm crops faster"), new RacialBonus(BonusType.CropQuality, RacialConfig.ElfCropQualityBonus.Value, isPercentage: true, "Green Thumb: Higher chance for quality crops"), new RacialBonus(BonusType.ForagingChance, RacialConfig.ElfForagingBonus.Value, isPercentage: true, "Forest Walker: Find more foragables"), new RacialBonus(BonusType.ManaRegen, RacialConfig.ElfManaRegenBonus.Value, isPercentage: true, "Arcane Heritage: Faster mana regeneration"), new RacialBonus(BonusType.WoodcuttingDamage, RacialConfig.ElfWoodcuttingDamageBonus.Value, isPercentage: true, "Timber Master: Deal more damage to trees") }; _racialBonuses[Race.Angel] = new List { new RacialBonus(BonusType.MaxMana, RacialConfig.AngelMaxManaBonus.Value, isPercentage: true, "Divine Reservoir: Increased maximum mana"), new RacialBonus(BonusType.MagicPower, RacialConfig.AngelMagicBonus.Value, isPercentage: true, "Holy Light: Enhanced magic damage"), new RacialBonus(BonusType.HealthRegen, RacialConfig.AngelHealthRegenBonus.Value, isPercentage: true, "Blessed Recovery: Faster health regeneration"), new RacialBonus(BonusType.LuckBonus, RacialConfig.AngelLuckBonus.Value, isPercentage: true, "Fortune's Favor: Blessed with good luck"), new RacialBonus(BonusType.SpellAttackSpeed, RacialConfig.AngelSpellAttackSpeedBonus.Value, isPercentage: true, "Divine Haste: Cast spells faster") }; _racialBonuses[Race.Demon] = new List { new RacialBonus(BonusType.MeleeStrength, RacialConfig.DemonMeleeBonus.Value, isPercentage: true, "Infernal Might: Increased melee damage"), new RacialBonus(BonusType.CriticalChance, RacialConfig.DemonCritBonus.Value, isPercentage: true, "Ruthless: Higher critical hit chance"), new RacialBonus(BonusType.MaxHealth, RacialConfig.DemonHealthBonus.Value, isPercentage: true, "Hellforged Vitality: Increased maximum health"), new RacialBonus(BonusType.GoldFind, RacialConfig.DemonGoldBonus.Value, isPercentage: true, "Greed: Find more gold"), new RacialBonus(BonusType.TripleGoldChance, RacialConfig.DemonTripleGoldBonus.Value, isPercentage: true, "Avarice: Chance for triple gold drops"), new RacialBonus(BonusType.MiningDamage, RacialConfig.DemonMiningDamageBonus.Value, isPercentage: true, "Hellfire Pick: Deal more damage to rocks") }; _racialBonuses[Race.FireElemental] = new List { new RacialBonus(BonusType.MeleeStrength, RacialConfig.FireElementalMeleeBonus.Value, isPercentage: true, "Burning Fury: Increased melee damage"), new RacialBonus(BonusType.MagicPower, RacialConfig.FireElementalMagicBonus.Value, isPercentage: true, "Inferno: Enhanced magic damage"), new RacialBonus(BonusType.AttackSpeed, RacialConfig.FireElementalAttackSpeedBonus.Value, isPercentage: true, "Wildfire: Faster attack speed"), new RacialBonus(BonusType.CriticalChance, RacialConfig.FireElementalCritBonus.Value, isPercentage: true, "Scorching Strike: Higher critical hit chance") }; _racialBonuses[Race.WaterElemental] = new List { new RacialBonus(BonusType.Defense, RacialConfig.WaterElementalDefenseBonus.Value, isPercentage: true, "Tidal Shield: Increased defense"), new RacialBonus(BonusType.HealthRegen, RacialConfig.WaterElementalHealthRegenBonus.Value, isPercentage: true, "Healing Waters: Faster health regeneration"), new RacialBonus(BonusType.ManaRegen, RacialConfig.WaterElementalManaRegenBonus.Value, isPercentage: true, "Flowing Spirit: Faster mana regeneration"), new RacialBonus(BonusType.FishingLuck, RacialConfig.WaterElementalFishingBonus.Value, isPercentage: true, "Aquatic Kinship: Better fishing luck"), new RacialBonus(BonusType.FishingMinigameSpeed, RacialConfig.WaterElementalFishingMinigameBonus.Value, isPercentage: true, "Water Mastery: Faster fishing minigame"), new RacialBonus(BonusType.AirSkipChance, RacialConfig.WaterElementalAirSkipBonus.Value, isPercentage: true, "Breathless: No need for air underwater") }; _racialBonuses[Race.Elemental] = new List { new RacialBonus(BonusType.MiningSpeed, RacialConfig.ElementalMiningSpeedBonus.Value, isPercentage: true, "Stone Affinity: Mine faster"), new RacialBonus(BonusType.MiningYield, RacialConfig.ElementalMiningYieldBonus.Value, isPercentage: true, "Earth's Bounty: Chance for extra ore"), new RacialBonus(BonusType.MagicPower, RacialConfig.ElementalMagicBonus.Value, isPercentage: true, "Elemental Mastery: Enhanced magic damage"), new RacialBonus(BonusType.Defense, RacialConfig.ElementalDefenseBonus.Value, isPercentage: true, "Hardened Form: Increased defense") }; _racialBonuses[Race.Amari] = new List { new RacialBonus(BonusType.MovementSpeed, RacialConfig.AmariSpeedBonus.Value, isPercentage: true, "Swift Paws: Move faster"), new RacialBonus(BonusType.AttackSpeed, RacialConfig.AmariAttackSpeedBonus.Value, isPercentage: true, "Predator's Reflexes: Attack faster"), new RacialBonus(BonusType.CraftingSpeed, RacialConfig.AmariCraftingBonus.Value, isPercentage: true, "Skilled Artisan: Craft items faster"), new RacialBonus(BonusType.WoodcuttingSpeed, RacialConfig.AmariWoodcuttingBonus.Value, isPercentage: true, "Forest Hunter: Chop trees faster") }; _racialBonuses[Race.AmariCat] = new List { new RacialBonus(BonusType.MovementSpeed, RacialConfig.AmariCatSpeedBonus.Value, isPercentage: true, "Feline Grace: Move faster"), new RacialBonus(BonusType.CriticalChance, RacialConfig.AmariCatCritBonus.Value, isPercentage: true, "Predator's Strike: Higher critical hit chance"), new RacialBonus(BonusType.ForagingChance, RacialConfig.AmariCatForagingBonus.Value, isPercentage: true, "Keen Senses: Find more foragables") }; _racialBonuses[Race.AmariDog] = new List { new RacialBonus(BonusType.MaxHealth, RacialConfig.AmariDogHealthBonus.Value, isPercentage: true, "Loyal Heart: Increased maximum health"), new RacialBonus(BonusType.Defense, RacialConfig.AmariDogDefenseBonus.Value, isPercentage: true, "Guardian's Resolve: Increased defense"), new RacialBonus(BonusType.RelationshipGain, RacialConfig.AmariDogRelationshipBonus.Value, isPercentage: true, "Best Friend: Build relationships faster"), new RacialBonus(BonusType.ExperienceGain, RacialConfig.AmariDogExpBonus.Value, isPercentage: true, "Eager Learner: Gain experience faster") }; _racialBonuses[Race.AmariBird] = new List { new RacialBonus(BonusType.MovementSpeed, RacialConfig.AmariBirdSpeedBonus.Value, isPercentage: true, "Wind Rider: Move faster"), new RacialBonus(BonusType.ForagingChance, RacialConfig.AmariBirdForagingBonus.Value, isPercentage: true, "Keen Eye: Find more foragables"), new RacialBonus(BonusType.ManaRegen, RacialConfig.AmariBirdManaRegenBonus.Value, isPercentage: true, "Sky Spirit: Faster mana regeneration"), new RacialBonus(BonusType.DodgeChance, RacialConfig.AmariBirdDodgeBonus.Value, isPercentage: true, "Evasive Flight: Higher chance to dodge attacks") }; _racialBonuses[Race.AmariAquatic] = new List { new RacialBonus(BonusType.FishingSpeed, RacialConfig.AmariAquaticFishingSpeedBonus.Value, isPercentage: true, "Water Born: Fish faster"), new RacialBonus(BonusType.FishingLuck, RacialConfig.AmariAquaticFishingLuckBonus.Value, isPercentage: true, "Tidal Blessing: Better fishing luck"), new RacialBonus(BonusType.ManaRegen, RacialConfig.AmariAquaticManaRegenBonus.Value, isPercentage: true, "Flowing Spirit: Faster mana regeneration"), new RacialBonus(BonusType.HealthRegen, RacialConfig.AmariAquaticHealthRegenBonus.Value, isPercentage: true, "Healing Waters: Faster health regeneration"), new RacialBonus(BonusType.FishingMinigameSpeed, RacialConfig.AmariAquaticFishingMinigameBonus.Value, isPercentage: true, "Aquatic Instinct: Faster fishing minigame"), new RacialBonus(BonusType.AirSkipChance, RacialConfig.AmariAquaticAirSkipBonus.Value, isPercentage: true, "Amphibious: Chance to skip air requirement") }; _racialBonuses[Race.AmariReptile] = new List { new RacialBonus(BonusType.Defense, RacialConfig.AmariReptileDefenseBonus.Value, isPercentage: true, "Scaled Hide: Increased defense"), new RacialBonus(BonusType.MeleeStrength, RacialConfig.AmariReptileMeleeBonus.Value, isPercentage: true, "Primal Strength: Increased melee damage"), new RacialBonus(BonusType.MaxHealth, RacialConfig.AmariReptileHealthBonus.Value, isPercentage: true, "Cold Blood: Increased maximum health"), new RacialBonus(BonusType.MiningSpeed, RacialConfig.AmariReptileMiningBonus.Value, isPercentage: true, "Burrow Instinct: Mine faster") }; _racialBonuses[Race.Naga] = new List { new RacialBonus(BonusType.FishingSpeed, RacialConfig.NagaFishingSpeedBonus.Value, isPercentage: true, "Aquatic Nature: Fish faster"), new RacialBonus(BonusType.FishingLuck, RacialConfig.NagaFishingLuckBonus.Value, isPercentage: true, "Sea's Blessing: Better fishing luck"), new RacialBonus(BonusType.Defense, RacialConfig.NagaDefenseBonus.Value, isPercentage: true, "Scaled Hide: Increased defense"), new RacialBonus(BonusType.ManaRegen, RacialConfig.NagaManaRegenBonus.Value, isPercentage: true, "Tidal Magic: Faster mana regeneration"), new RacialBonus(BonusType.AirSkipChance, RacialConfig.NagaAirSkipBonus.Value, isPercentage: true, "Serpent's Breath: Chance to skip air requirement") }; Plugin.Log.LogInfo((object)$"Initialized racial bonuses for {_racialBonuses.Count} races"); BonusTransferRules.RebuildFromConfig((RacialConfig.CrossRaceBonusRules != null) ? RacialConfig.CrossRaceBonusRules.Value : ""); } public void SetPlayerRace(Race race) { _currentPlayerRace = race; Plugin.Log.LogInfo((object)$"Player race set to: {race}"); } public void SetPlayerRace(Race race, ElementalVariant variant) { if (race == Race.Elemental) { _currentPlayerRace = variant switch { ElementalVariant.Fire => Race.FireElemental, ElementalVariant.Water => Race.WaterElemental, _ => Race.Elemental, }; } else { _currentPlayerRace = race; } Plugin.Log.LogInfo((object)$"Player race set to: {_currentPlayerRace} (variant: {variant})"); } public void SetPlayerRace(Race race, int gameSubRace) { _currentPlayerRace = race; _cachedGameSubRace = gameSubRace; Plugin.Log.LogInfo((object)$"Player race set to: {race} (cached SubRace: {gameSubRace})"); } public bool IsElemental() { if (_currentPlayerRace.GetValueOrDefault() != Race.Elemental && _currentPlayerRace.GetValueOrDefault() != Race.FireElemental) { return _currentPlayerRace.GetValueOrDefault() == Race.WaterElemental; } return true; } public bool IsAmari() { if (_currentPlayerRace.GetValueOrDefault() != Race.Amari && _currentPlayerRace.GetValueOrDefault() != Race.AmariCat && _currentPlayerRace.GetValueOrDefault() != Race.AmariDog && _currentPlayerRace.GetValueOrDefault() != Race.AmariBird && _currentPlayerRace.GetValueOrDefault() != Race.AmariAquatic) { return _currentPlayerRace.GetValueOrDefault() == Race.AmariReptile; } return true; } public bool IsNaga() { return _currentPlayerRace.GetValueOrDefault() == Race.Naga; } public int GetCachedSubRace() { return _cachedGameSubRace; } public void ClearPlayerRace() { _currentPlayerRace = null; _cachedGameSubRace = -1; Plugin.Log.LogDebug((object)"[RacialBonusManager] Player race cleared"); } public Race? GetPlayerRace() { return _currentPlayerRace; } public List GetBonusesForRace(Race race) { if (_racialBonuses.TryGetValue(race, out List value)) { return value; } return new List(); } public List GetCurrentPlayerBonuses() { if (_currentPlayerRace.HasValue) { return GetBonusesForRace(_currentPlayerRace.Value); } return new List(); } public float GetBonusValue(BonusType type) { if (!_currentPlayerRace.HasValue) { return 0f; } float num = 0f; foreach (RacialBonus item in GetBonusesForRace(_currentPlayerRace.Value)) { if (item.Type == type) { num = item.Value; break; } } return num + BonusTransferRules.GetTransferredPercent(this, _currentPlayerRace.Value, type); } public float ApplyBonus(float baseValue, BonusType type) { float bonusValue = GetBonusValue(type); if (bonusValue == 0f) { return baseValue; } return baseValue * (1f + bonusValue / 100f); } public bool HasBonus(BonusType type) { return GetBonusValue(type) != 0f; } public void RefreshBonuses() { InitializeDefaultBonuses(); Plugin.Log.LogInfo((object)"Racial bonuses refreshed from config"); } } public static class RacialConfig { public static ConfigEntry HumanExpBonus; public static ConfigEntry HumanRelationshipBonus; public static ConfigEntry HumanShopDiscount; public static ConfigEntry ElfFarmingBonus; public static ConfigEntry ElfCropQualityBonus; public static ConfigEntry ElfForagingBonus; public static ConfigEntry ElfManaRegenBonus; public static ConfigEntry AngelMaxManaBonus; public static ConfigEntry AngelMagicBonus; public static ConfigEntry AngelHealthRegenBonus; public static ConfigEntry AngelLuckBonus; public static ConfigEntry DemonMeleeBonus; public static ConfigEntry DemonCritBonus; public static ConfigEntry DemonHealthBonus; public static ConfigEntry DemonGoldBonus; public static ConfigEntry FireElementalMeleeBonus; public static ConfigEntry FireElementalMagicBonus; public static ConfigEntry FireElementalAttackSpeedBonus; public static ConfigEntry FireElementalCritBonus; public static ConfigEntry WaterElementalDefenseBonus; public static ConfigEntry WaterElementalHealthRegenBonus; public static ConfigEntry WaterElementalManaRegenBonus; public static ConfigEntry WaterElementalFishingBonus; public static ConfigEntry ElementalMiningSpeedBonus; public static ConfigEntry ElementalMiningYieldBonus; public static ConfigEntry ElementalMagicBonus; public static ConfigEntry ElementalDefenseBonus; public static ConfigEntry AmariSpeedBonus; public static ConfigEntry AmariAttackSpeedBonus; public static ConfigEntry AmariCraftingBonus; public static ConfigEntry AmariWoodcuttingBonus; public static ConfigEntry AmariCatSpeedBonus; public static ConfigEntry AmariCatCritBonus; public static ConfigEntry AmariCatForagingBonus; public static ConfigEntry AmariDogHealthBonus; public static ConfigEntry AmariDogDefenseBonus; public static ConfigEntry AmariDogRelationshipBonus; public static ConfigEntry AmariDogExpBonus; public static ConfigEntry AmariBirdSpeedBonus; public static ConfigEntry AmariBirdForagingBonus; public static ConfigEntry AmariBirdManaRegenBonus; public static ConfigEntry AmariBirdDodgeBonus; public static ConfigEntry AmariAquaticFishingSpeedBonus; public static ConfigEntry AmariAquaticFishingLuckBonus; public static ConfigEntry AmariAquaticManaRegenBonus; public static ConfigEntry AmariAquaticHealthRegenBonus; public static ConfigEntry AmariReptileDefenseBonus; public static ConfigEntry AmariReptileMeleeBonus; public static ConfigEntry AmariReptileHealthBonus; public static ConfigEntry AmariReptileMiningBonus; public static ConfigEntry NagaFishingSpeedBonus; public static ConfigEntry NagaFishingLuckBonus; public static ConfigEntry NagaDefenseBonus; public static ConfigEntry NagaManaRegenBonus; public static ConfigEntry NagaAirSkipBonus; public static ConfigEntry ElfWoodcuttingDamageBonus; public static ConfigEntry HumanCommunityTokenBonus; public static ConfigEntry DemonTripleGoldBonus; public static ConfigEntry DemonMiningDamageBonus; public static ConfigEntry AngelSpellAttackSpeedBonus; public static ConfigEntry WaterElementalFishingMinigameBonus; public static ConfigEntry WaterElementalAirSkipBonus; public static ConfigEntry AmariAquaticFishingMinigameBonus; public static ConfigEntry AmariAquaticAirSkipBonus; public static ConfigEntry EnableRacialBonuses; public static ConfigEntry ShowBonusNotifications; public static ConfigEntry AmariCatReduceCombatStutter; public static ConfigEntry EnableBonusTransfers; public static ConfigEntry CrossRaceBonusRules; public static void Initialize(ConfigFile config) { EnableRacialBonuses = config.Bind("General", "EnableRacialBonuses", true, "Enable or disable all racial bonuses"); ShowBonusNotifications = config.Bind("General", "ShowBonusNotifications", true, "Show notifications when racial bonuses are applied"); AmariCatReduceCombatStutter = config.Bind("Performance", "AmariCatReduceCombatStutter", false, "If true, Amari Cat does not receive attack speed bonuses (melee and spell) to reduce stutter with fast weapons (e.g. scythe). Damage, crit, dodge, movement speed, and other bonuses still apply."); EnableBonusTransfers = config.Bind("BonusTransfers", "EnableBonusTransfers", false, "When true, grant extra passive racial bonuses per Rules below. Values are read from the source race's bonus table (same as that race's config section). Does not copy active abilities, drawbacks, or conditional synergies."); CrossRaceBonusRules = config.Bind("BonusTransfers", "Rules", "", "Semicolon-separated rules. Each: TargetRace|SourceRace|BonusType. Example: Human|WaterElemental|Defense — adds Water Elemental's Defense % to Humans. Races: Human, Elf, Angel, Demon, Elemental, FireElemental, WaterElemental, Amari, AmariCat, AmariDog, AmariBird, AmariAquatic, AmariReptile, Naga. BonusType: MeleeStrength, MagicPower, Defense, CriticalChance, AttackSpeed, FarmingSpeed, CropQuality, MiningSpeed, FishingLuck, MovementSpeed, MaxHealth, MaxMana, ManaRegen, HealthRegen, ExperienceGain, GoldFind, LuckBonus, DodgeChance, SpellAttackSpeed, AirSkipChance, CommunityTokenGain, TripleGoldChance, etc. (see BonusType in mod docs)."); HumanExpBonus = config.Bind("Human", "ExperienceBonus", 10f, "Percentage bonus to experience gain"); HumanRelationshipBonus = config.Bind("Human", "RelationshipBonus", 15f, "Percentage bonus to relationship point gain"); HumanShopDiscount = config.Bind("Human", "ShopDiscount", 5f, "Percentage discount at shops"); ElfFarmingBonus = config.Bind("Elf", "FarmingSpeedBonus", 15f, "Percentage bonus to farming speed"); ElfCropQualityBonus = config.Bind("Elf", "CropQualityBonus", 20f, "Percentage bonus to crop quality chance"); ElfForagingBonus = config.Bind("Elf", "ForagingBonus", 25f, "Percentage bonus to foraging find chance"); ElfManaRegenBonus = config.Bind("Elf", "ManaRegenBonus", 15f, "Percentage bonus to mana regeneration"); AngelMaxManaBonus = config.Bind("Angel", "MaxManaBonus", 20f, "Percentage bonus to maximum mana"); AngelMagicBonus = config.Bind("Angel", "MagicPowerBonus", 15f, "Percentage bonus to magic damage"); AngelHealthRegenBonus = config.Bind("Angel", "HealthRegenBonus", 25f, "Percentage bonus to health regeneration"); AngelLuckBonus = config.Bind("Angel", "LuckBonus", 10f, "Percentage bonus to luck"); DemonMeleeBonus = config.Bind("Demon", "MeleeDamageBonus", 20f, "Percentage bonus to melee damage"); DemonCritBonus = config.Bind("Demon", "CriticalChanceBonus", 15f, "Percentage bonus to critical hit chance"); DemonHealthBonus = config.Bind("Demon", "MaxHealthBonus", 15f, "Percentage bonus to maximum health"); DemonGoldBonus = config.Bind("Demon", "GoldFindBonus", 20f, "Percentage bonus to gold drops"); FireElementalMeleeBonus = config.Bind("Fire Elemental", "MeleeDamageBonus", 15f, "Percentage bonus to melee damage"); FireElementalMagicBonus = config.Bind("Fire Elemental", "MagicPowerBonus", 20f, "Percentage bonus to magic damage"); FireElementalAttackSpeedBonus = config.Bind("Fire Elemental", "AttackSpeedBonus", 10f, "Percentage bonus to attack speed"); FireElementalCritBonus = config.Bind("Fire Elemental", "CriticalChanceBonus", 15f, "Percentage bonus to critical hit chance"); WaterElementalDefenseBonus = config.Bind("Water Elemental", "DefenseBonus", 20f, "Percentage bonus to defense"); WaterElementalHealthRegenBonus = config.Bind("Water Elemental", "HealthRegenBonus", 20f, "Percentage bonus to health regeneration"); WaterElementalManaRegenBonus = config.Bind("Water Elemental", "ManaRegenBonus", 25f, "Percentage bonus to mana regeneration"); WaterElementalFishingBonus = config.Bind("Water Elemental", "FishingLuckBonus", 20f, "Percentage bonus to fishing luck"); ElementalMiningSpeedBonus = config.Bind("Elemental (Generic)", "MiningSpeedBonus", 20f, "Percentage bonus to mining speed (used if Fire/Water variant cannot be detected)"); ElementalMiningYieldBonus = config.Bind("Elemental (Generic)", "MiningYieldBonus", 15f, "Percentage bonus to mining yield"); ElementalMagicBonus = config.Bind("Elemental (Generic)", "MagicPowerBonus", 10f, "Percentage bonus to magic damage"); ElementalDefenseBonus = config.Bind("Elemental (Generic)", "DefenseBonus", 15f, "Percentage bonus to defense"); AmariSpeedBonus = config.Bind("Amari (Generic)", "MovementSpeedBonus", 15f, "Percentage bonus to movement speed (used if variant cannot be detected)"); AmariAttackSpeedBonus = config.Bind("Amari (Generic)", "AttackSpeedBonus", 15f, "Percentage bonus to attack speed"); AmariCraftingBonus = config.Bind("Amari (Generic)", "CraftingSpeedBonus", 20f, "Percentage bonus to crafting speed"); AmariWoodcuttingBonus = config.Bind("Amari (Generic)", "WoodcuttingSpeedBonus", 15f, "Percentage bonus to woodcutting speed"); AmariCatSpeedBonus = config.Bind("Amari Cat", "MovementSpeedBonus", 20f, "Feline Grace: Percentage bonus to movement speed"); AmariCatCritBonus = config.Bind("Amari Cat", "CriticalChanceBonus", 15f, "Percentage bonus to critical hit chance"); AmariCatForagingBonus = config.Bind("Amari Cat", "ForagingChanceBonus", 15f, "Keen Senses: Percentage bonus to find foragables"); AmariDogHealthBonus = config.Bind("Amari Dog", "MaxHealthBonus", 20f, "Percentage bonus to maximum health"); AmariDogDefenseBonus = config.Bind("Amari Dog", "DefenseBonus", 15f, "Percentage bonus to defense"); AmariDogRelationshipBonus = config.Bind("Amari Dog", "RelationshipBonus", 25f, "Percentage bonus to relationship point gain"); AmariDogExpBonus = config.Bind("Amari Dog", "ExperienceBonus", 10f, "Percentage bonus to experience gain"); AmariBirdSpeedBonus = config.Bind("Amari Bird", "MovementSpeedBonus", 25f, "Percentage bonus to movement speed"); AmariBirdForagingBonus = config.Bind("Amari Bird", "ForagingBonus", 25f, "Percentage bonus to foraging find chance"); AmariBirdManaRegenBonus = config.Bind("Amari Bird", "ManaRegenBonus", 15f, "Percentage bonus to mana regeneration"); AmariBirdDodgeBonus = config.Bind("Amari Bird", "DodgeChanceBonus", 15f, "Percentage bonus to dodge chance"); AmariAquaticFishingSpeedBonus = config.Bind("Amari Aquatic", "FishingSpeedBonus", 25f, "Percentage bonus to fishing speed"); AmariAquaticFishingLuckBonus = config.Bind("Amari Aquatic", "FishingLuckBonus", 25f, "Percentage bonus to fishing luck"); AmariAquaticManaRegenBonus = config.Bind("Amari Aquatic", "ManaRegenBonus", 15f, "Percentage bonus to mana regeneration"); AmariAquaticHealthRegenBonus = config.Bind("Amari Aquatic", "HealthRegenBonus", 15f, "Percentage bonus to health regeneration"); AmariReptileDefenseBonus = config.Bind("Amari Reptile", "DefenseBonus", 25f, "Percentage bonus to defense"); AmariReptileMeleeBonus = config.Bind("Amari Reptile", "MeleeDamageBonus", 15f, "Percentage bonus to melee damage"); AmariReptileHealthBonus = config.Bind("Amari Reptile", "MaxHealthBonus", 15f, "Percentage bonus to maximum health"); AmariReptileMiningBonus = config.Bind("Amari Reptile", "MiningSpeedBonus", 20f, "Percentage bonus to mining speed"); NagaFishingSpeedBonus = config.Bind("Naga", "FishingSpeedBonus", 25f, "Percentage bonus to fishing speed"); NagaFishingLuckBonus = config.Bind("Naga", "FishingLuckBonus", 20f, "Percentage bonus to fishing luck"); NagaDefenseBonus = config.Bind("Naga", "DefenseBonus", 10f, "Percentage bonus to defense"); NagaManaRegenBonus = config.Bind("Naga", "ManaRegenBonus", 15f, "Percentage bonus to mana regeneration"); NagaAirSkipBonus = config.Bind("Naga", "AirSkipChanceBonus", 15f, "Percentage chance to skip air requirement while swimming/fishing"); ElfWoodcuttingDamageBonus = config.Bind("Elf", "WoodcuttingDamageBonus", 15f, "Percentage bonus to tree damage"); HumanCommunityTokenBonus = config.Bind("Human", "CommunityTokenBonus", 10f, "Percentage bonus to daily community token gain"); DemonTripleGoldBonus = config.Bind("Demon", "TripleGoldChanceBonus", 10f, "Percentage chance for triple gold drops"); DemonMiningDamageBonus = config.Bind("Demon", "MiningDamageBonus", 15f, "Percentage bonus to mining damage"); AngelSpellAttackSpeedBonus = config.Bind("Angel", "SpellAttackSpeedBonus", 15f, "Percentage bonus to spell casting speed"); WaterElementalFishingMinigameBonus = config.Bind("Water Elemental", "FishingMinigameSpeedBonus", 20f, "Percentage bonus to fishing minigame speed"); WaterElementalAirSkipBonus = config.Bind("Water Elemental", "AirSkipChanceBonus", 20f, "Percentage chance to skip air requirement while swimming/fishing"); AmariAquaticFishingMinigameBonus = config.Bind("Amari Aquatic", "FishingMinigameSpeedBonus", 15f, "Percentage bonus to fishing minigame speed"); AmariAquaticAirSkipBonus = config.Bind("Amari Aquatic", "AirSkipChanceBonus", 15f, "Percentage chance to skip air requirement while swimming/fishing"); Plugin.Log.LogInfo((object)"Configuration initialized"); } } internal static class StatFrameCache { private static float _cachedHPRatio = 1f; private static string _cachedSeason; private static bool _cachedIsDaytime = true; private static bool _cachedIsInMine; private static bool _cachedIsSpring; private static bool _cachedIsSummer; private static bool _cachedIsWinter; private static float _cachedQuickLearnerBonus; private static bool _cacheValid; private static string _lastSceneName; private static bool _lastIsInMine; public static float CachedHPRatio => _cachedHPRatio; public static string CachedSeason => _cachedSeason; public static bool CachedIsDaytime => _cachedIsDaytime; public static bool CachedIsInMine => _cachedIsInMine; public static bool CachedIsSpring => _cachedIsSpring; public static bool CachedIsSummer => _cachedIsSummer; public static bool CachedIsWinter => _cachedIsWinter; public static float CachedQuickLearnerBonus => _cachedQuickLearnerBonus; public static bool IsCacheValid => _cacheValid; public static void Reset() { _cacheValid = false; _cachedHPRatio = 1f; _cachedSeason = null; _cachedIsDaytime = true; _cachedIsInMine = false; _cachedIsSpring = false; _cachedIsSummer = false; _cachedIsWinter = false; _cachedQuickLearnerBonus = 0f; _lastSceneName = null; _lastIsInMine = false; } public static void Update(Race race, Type dayCycleType) { try { _cachedIsInMine = ComputeIsInMine(); if (dayCycleType != null) { object singletonInstance = ReflectionHelper.GetSingletonInstance(dayCycleType); if (singletonInstance != null) { float num = ReflectionHelper.TryGetValue(singletonInstance, "Hour", -1f); if (num < 0f) { num = ReflectionHelper.TryGetValue(singletonInstance, "CurrentHour", -1f); } if (num < 0f) { num = ReflectionHelper.TryGetValue(singletonInstance, "currentHour", -1f); } if (num < 0f) { int num2 = ReflectionHelper.TryGetValue(singletonInstance, "Hour", -1); if (num2 >= 0) { num = num2; } } _cachedIsDaytime = num < 0f || (num >= 6f && num < 18f); object instanceValue = ReflectionHelper.GetInstanceValue(singletonInstance, "Season"); if (instanceValue == null) { instanceValue = ReflectionHelper.GetInstanceValue(singletonInstance, "season"); } if (instanceValue == null) { instanceValue = ReflectionHelper.GetInstanceValue(singletonInstance, "CurrentSeason"); } _cachedSeason = instanceValue?.ToString(); _cachedIsSpring = _cachedSeason != null && _cachedSeason.IndexOf("Spring", StringComparison.OrdinalIgnoreCase) >= 0; _cachedIsSummer = _cachedSeason != null && _cachedSeason.IndexOf("Summer", StringComparison.OrdinalIgnoreCase) >= 0; _cachedIsWinter = _cachedSeason != null && _cachedSeason.IndexOf("Winter", StringComparison.OrdinalIgnoreCase) >= 0; } } Player instance = Player.Instance; if ((Object)(object)instance != (Object)null) { float maxHealth = instance.MaxHealth; if (maxHealth > 0f) { _cachedHPRatio = ReflectionHelper.TryGetValue(instance, "health", maxHealth) / maxHealth; } else { _cachedHPRatio = 1f; } if (race == Race.Human && AbilityConfig.EnableQuickLearner != null && AbilityConfig.EnableQuickLearner.Value) { _cachedQuickLearnerBonus = CalculateQuickLearnerBonus(instance); } else { _cachedQuickLearnerBonus = 0f; } } _cacheValid = true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[StatFrameCache] Update error: " + ex.Message)); _cacheValid = true; } } private static bool ComputeIsInMine() { string currentSceneName = SceneHelpers.GetCurrentSceneName(); if (currentSceneName == _lastSceneName) { return _lastIsInMine; } _lastSceneName = currentSceneName; string text = currentSceneName.ToLowerInvariant(); _lastIsInMine = text.Contains("mine") || text.Contains("dungeon") || text.Contains("cave") || text.Contains("underground") || text.Contains("nelvari") || text.Contains("withergate"); return _lastIsInMine; } private static float CalculateQuickLearnerBonus(Player player) { try { int value = AbilityConfig.QuickLearnerSkillThreshold.Value; int num = 0; float num2 = ReflectionHelper.TryGetValue(player, "FarmingSkillLevel", 0f); float num3 = ReflectionHelper.TryGetValue(player, "MiningSkillLevel", 0f); float num4 = ReflectionHelper.TryGetValue(player, "FishingSkillLevel", 0f); float num5 = ReflectionHelper.TryGetValue(player, "ExplorationSkillLevel", 0f); if (num2 >= (float)value) { num++; } if (num3 >= (float)value) { num++; } if (num4 >= (float)value) { num++; } if (num5 >= (float)value) { num++; } try { object instanceValue = ReflectionHelper.GetInstanceValue(player, "Professions"); if (instanceValue != null) { Type type = ReflectionHelper.FindWishType("ProfessionType"); if (type != null) { object key = Enum.Parse(type, "Crafting"); if (instanceValue is IDictionary dictionary && dictionary.Contains(key) && ReflectionHelper.TryGetValue(dictionary[key], "level", 0) >= value) { num++; } } } } catch { } return Mathf.Min((float)num * AbilityConfig.QuickLearnerBonusPerSkill.Value, AbilityConfig.QuickLearnerMaxBonus.Value); } catch { return 0f; } } } } namespace HavensBirthright.Session { internal static class BirthrightGameSaveContext { public static int LastLoadedSlot { get; private set; } = -1; public static string LastLoadedCharacterName { get; private set; } public static void Reset() { LastLoadedSlot = -1; LastLoadedCharacterName = null; } public static void RecordLoadCharacter(int characterNumber) { LastLoadedSlot = characterNumber; LastLoadedCharacterName = null; try { GameSave instance = SingletonBehaviour.Instance; if (((instance != null) ? instance.Saves : null) != null && characterNumber >= 0 && characterNumber < instance.Saves.Count) { GameSaveData obj = instance.Saves[characterNumber]; object lastLoadedCharacterName; if (obj == null) { lastLoadedCharacterName = null; } else { CharacterData characterData = obj.characterData; lastLoadedCharacterName = ((characterData != null) ? characterData.characterName : null); } LastLoadedCharacterName = (string)lastLoadedCharacterName; } } catch { } } public static bool TryGetAuthoritativeCharacterData(out CharacterData data) { data = null; try { GameSave instance = SingletonBehaviour.Instance; if ((Object)(object)instance == (Object)null || LastLoadedSlot < 0 || instance.Saves == null || LastLoadedSlot >= instance.Saves.Count) { return false; } if (instance.CharacterIndex != LastLoadedSlot) { return false; } GameSaveData obj = instance.Saves[LastLoadedSlot]; data = ((obj != null) ? obj.characterData : null); return data != null; } catch { return false; } } } internal static class CharacterFingerprint { internal const byte LegacySwitchFingerprintCharIndex = byte.MaxValue; internal const int LegacySwitchFingerprintListSlot = -2; internal static CharacterData GetAuthoritativeCharacterData() { CharacterData val = GameSave.CurrentCharacter; if (BirthrightGameSaveContext.TryGetAuthoritativeCharacterData(out CharacterData data) && (val == null || !string.Equals(val.characterName, data.characterName, StringComparison.Ordinal))) { val = data; } return val; } internal static string GetCurrentBodyStyleName() { try { CharacterData authoritativeCharacterData = GetAuthoritativeCharacterData(); if (((authoritativeCharacterData != null) ? authoritativeCharacterData.StyleData : null) == null || authoritativeCharacterData.StyleData.Count == 0) { return null; } authoritativeCharacterData.StyleData.TryGetValue(14, out var value); return value; } catch { return null; } } internal static string GetCurrentCharacterSwitchId() { try { CharacterData authoritativeCharacterData = GetAuthoritativeCharacterData(); if (authoritativeCharacterData == null) { return null; } string text = authoritativeCharacterData.characterName ?? ""; string value = ""; authoritativeCharacterData.StyleData?.TryGetValue(14, out value); int num = -1; try { GameSave instance = SingletonBehaviour.Instance; if ((Object)(object)instance != (Object)null) { num = instance.CharacterIndex; } } catch { } return string.Format("{0}|{1}|{2}|{3}|{4}", text, authoritativeCharacterData.race, authoritativeCharacterData.characterIndex, num, value ?? ""); } catch { return null; } } internal static bool TryParseCharacterSwitchFingerprint(string id, out string name, out string raceByte, out byte charDataIndex, out int listSlot, out string bodyStyle) { name = ""; raceByte = ""; bodyStyle = ""; charDataIndex = 0; listSlot = -2; if (string.IsNullOrEmpty(id)) { return false; } string[] array = id.Split(new char[1] { '|' }); if (array.Length >= 5 && byte.TryParse(array[2], out charDataIndex) && int.TryParse(array[3], out listSlot)) { name = array[0]; raceByte = array[1]; bodyStyle = ((array.Length == 5) ? (array[4] ?? "") : string.Join("|", array, 4, array.Length - 4)); return true; } if (array.Length == 3) { name = array[0]; raceByte = array[1]; bodyStyle = array[2] ?? ""; charDataIndex = byte.MaxValue; listSlot = -2; return true; } return false; } } internal static class CharacterSessionController { private static string _trackedCharacterId; internal static void ClearTrackedCharacterId() { _trackedCharacterId = null; } internal static void SyncTrackedCharacterIdFromSave() { RacialBonusManager racialBonusManager = Plugin.GetRacialBonusManager(); if (racialBonusManager != null && racialBonusManager.GetPlayerRace().HasValue) { _trackedCharacterId = CharacterFingerprint.GetCurrentCharacterSwitchId(); } } internal static void OnUpdateIdentityChecks() { CheckCharacterIdentityChanged(); InvalidateStaleElementalVariant(); } private static void CheckCharacterIdentityChanged() { try { string currentCharacterSwitchId = CharacterFingerprint.GetCurrentCharacterSwitchId(); if (string.IsNullOrEmpty(currentCharacterSwitchId)) { return; } if (_trackedCharacterId == null) { RacialBonusManager racialBonusManager = Plugin.GetRacialBonusManager(); if (racialBonusManager == null || !racialBonusManager.GetPlayerRace().HasValue) { RaceDetectionService.RetryRaceDetection(); } SyncTrackedCharacterIdFromSave(); } else if (!string.Equals(currentCharacterSwitchId, _trackedCharacterId, StringComparison.Ordinal)) { if (IsTransientBodyOnlyIdentityChange(_trackedCharacterId, currentCharacterSwitchId)) { _trackedCharacterId = currentCharacterSwitchId; return; } Plugin.Log.LogInfo((object)("[CharacterSession] Identity changed (" + _trackedCharacterId + " -> " + currentCharacterSwitchId + "), resetting state for new character")); _trackedCharacterId = null; BirthrightRunner.ResetAllStateForNewSave(); RaceDetectionService.RetryRaceDetection(); SyncTrackedCharacterIdFromSave(); } } catch (Exception ex) { Plugin.Log.LogDebug((object)("[CharacterSession] CheckCharacterIdentityChanged: " + ex.Message)); } } private static bool IsTransientBodyOnlyIdentityChange(string oldId, string newId) { if (string.IsNullOrEmpty(oldId) || string.IsNullOrEmpty(newId)) { return false; } if (!CharacterFingerprint.TryParseCharacterSwitchFingerprint(oldId, out string name, out string raceByte, out byte charDataIndex, out int listSlot, out string bodyStyle)) { return false; } if (!CharacterFingerprint.TryParseCharacterSwitchFingerprint(newId, out string name2, out string raceByte2, out byte charDataIndex2, out int listSlot2, out string bodyStyle2)) { return false; } if (charDataIndex == byte.MaxValue || charDataIndex2 == byte.MaxValue || listSlot == -2 || listSlot2 == -2) { return false; } if (!string.Equals(name, name2, StringComparison.Ordinal)) { return false; } if (!string.Equals(raceByte, raceByte2, StringComparison.Ordinal)) { return false; } if (charDataIndex != charDataIndex2) { return false; } if (listSlot != listSlot2) { return false; } if (string.IsNullOrEmpty(bodyStyle)) { return !string.IsNullOrEmpty(bodyStyle2); } return false; } private static void InvalidateStaleElementalVariant() { try { Race? race = Plugin.GetRacialBonusManager()?.GetPlayerRace(); if (!race.HasValue) { return; } Race value = race.Value; if (value != Race.WaterElemental && value != Race.FireElemental && value != Race.Elemental) { return; } string currentBodyStyleName = CharacterFingerprint.GetCurrentBodyStyleName(); if (string.IsNullOrEmpty(currentBodyStyleName)) { return; } Race race2 = ElementalVariantResolver.ResolveElementalFromBodyStyleName(currentBodyStyleName); if (race2 == Race.WaterElemental || race2 == Race.FireElemental) { if (value == Race.Elemental) { Plugin.Log.LogInfo((object)$"[CharacterSession] Cached generic Elemental but body resolves to {race2}; resetting."); BirthrightRunner.ResetAllStateForNewSave(); RaceDetectionService.RetryRaceDetection(); SyncTrackedCharacterIdFromSave(); } else if (value != race2) { Plugin.Log.LogInfo((object)$"[CharacterSession] Cached elemental ({value}) mismatches body ({race2}); resetting."); BirthrightRunner.ResetAllStateForNewSave(); RaceDetectionService.RetryRaceDetection(); SyncTrackedCharacterIdFromSave(); } } } catch (Exception ex) { Plugin.Log.LogDebug((object)("[CharacterSession] InvalidateStaleElementalVariant: " + ex.Message)); } } } } namespace HavensBirthright.Patches { public static class AbilityPatches { internal static readonly Dictionary OreToBarMap = new Dictionary { { 1100, 1200 }, { 1101, 1201 }, { 1102, 1202 }, { 1103, 1203 }, { 1104, 1204 }, { 1105, 1205 }, { 1107, 1206 }, { 1108, 1207 } }; internal static readonly Dictionary OreManaCostMap = new Dictionary { { 1100, 1f }, { 1101, 2f }, { 1105, 3f }, { 1102, 4f }, { 1103, 5f }, { 1104, 6f }, { 1107, 7f }, { 1108, 8f } }; private static bool _isSmeltingItem; private static bool _genericElementalWarningLogged; private static bool IsVerbose { get { if (AbilityConfig.InfernalForgeVerboseLogging != null) { return AbilityConfig.InfernalForgeVerboseLogging.Value; } return false; } } internal static void SendGameNotification(string text, int id = 0, int amount = 0, bool unique = false, bool error = false) { GameApis.SendGameNotification(text, id, amount, unique, error); } internal static MethodInfo GetAddItemIntMethod(object inventory) { return GameApis.GetAddItemIntMethod(inventory); } internal static bool InvokeAddItem(MethodInfo addMethod, object inventory, int itemId, int amount, bool notify) { return GameApis.InvokeAddItem(addMethod, inventory, itemId, amount, notify); } internal static void ResetNotificationCache() { GameApis.ResetItemAndNotificationCaches(); } internal static void ResetReflectionCache() { _genericElementalWarningLogged = false; } private static int GetItemId(object item) { return GameApis.GetItemId(item); } public static bool OnPlayerAddManaPrefix() { if (!RacialConfig.EnableRacialBonuses.Value) { return true; } if (!AbilityConfig.EnableInfernalForge.Value) { return true; } RacialBonusManager racialBonusManager = Plugin.GetRacialBonusManager(); if (racialBonusManager == null) { return true; } Race? playerRace = racialBonusManager.GetPlayerRace(); if (!playerRace.HasValue) { return true; } Race race = ElementalVariantResolver.ResolveElementalAbilityRace(playerRace.Value); if (race != Race.FireElemental && race != Race.Elemental) { return true; } if (!ActiveAbilityManager.IsRuntimeEnabled("InfernalForge")) { return true; } return false; } public static bool OnInventoryAddItemPrefix(object __instance, object item, int amount, int slot, bool sendNotification, bool specialItem, bool superSecretCheck) { if (_isSmeltingItem) { return true; } if (!RacialConfig.EnableRacialBonuses.Value) { return true; } if (AbilityConfig.EnableActiveAbilities == null || !AbilityConfig.EnableActiveAbilities.Value) { return true; } if (!AbilityConfig.EnableInfernalForge.Value) { return true; } if (!ActiveAbilityManager.IsRuntimeEnabled("InfernalForge")) { return true; } RacialBonusManager racialBonusManager = Plugin.GetRacialBonusManager(); if (racialBonusManager == null) { return true; } Race? playerRace = racialBonusManager.GetPlayerRace(); if (!playerRace.HasValue) { return true; } Race num = ElementalVariantResolver.ResolveElementalAbilityRace(playerRace.Value); bool flag = num == Race.FireElemental; bool flag2 = num == Race.Elemental; if (!flag && !flag2) { return true; } if (flag2 && !_genericElementalWarningLogged) { _genericElementalWarningLogged = true; ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)"[InfernalForge] Race is generic Elemental (variant detection may have failed). Allowing Infernal Forge as fallback. Check body style name in logs."); } } try { int itemId = GetItemId(item); if (itemId < 0) { if (IsVerbose) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogInfo((object)$"[InfernalForge] Could not determine item ID (returned {itemId})"); } } return true; } if (!OreToBarMap.TryGetValue(itemId, out var value)) { if (IsVerbose) { ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogInfo((object)$"[InfernalForge] Item {itemId} is not an ore"); } } return true; } if (IsVerbose) { ManualLogSource log4 = Plugin.Log; if (log4 != null) { log4.LogInfo((object)$"[InfernalForge] Detected ore {itemId} (amount: {amount}), bar would be {value}"); } } int num2 = AbilityConfig.InfernalForgeOrePerBar.Value; if (num2 < 1) { num2 = 3; } int num3 = amount / num2; int num4 = amount % num2; if (num3 <= 0) { if (IsVerbose) { ManualLogSource log5 = Plugin.Log; if (log5 != null) { log5.LogInfo((object)$"[InfernalForge] Not enough ore ({amount}) for 1 bar (need {num2})"); } } return true; } Player instance = Player.Instance; if ((Object)(object)instance == (Object)null) { if (IsVerbose) { ManualLogSource log6 = Plugin.Log; if (log6 != null) { log6.LogInfo((object)"[InfernalForge] Player.Instance is null"); } } return true; } float maxMana = instance.MaxMana; float num5 = ReflectionHelper.TryGetValue(instance, "mana", 0f); float num6 = num5 / maxMana * 100f; float value2; float num7 = (OreManaCostMap.TryGetValue(itemId, out value2) ? value2 : 3f); float num8 = maxMana * (num7 / 100f); float num9 = num8 * (float)num3; if ((num5 - num9) / maxMana * 100f < AbilityConfig.InfernalForgeManaThreshold.Value) { int num10 = 0; for (int num11 = num3; num11 >= 1; num11--) { float num12 = num8 * (float)num11; if ((num5 - num12) / maxMana * 100f >= AbilityConfig.InfernalForgeManaThreshold.Value) { num10 = num11; break; } } if (num10 <= 0) { if (IsVerbose) { ManualLogSource log7 = Plugin.Log; if (log7 != null) { log7.LogInfo((object)$"[InfernalForge] Insufficient mana ({num6:F0}%) to smelt any bars from {amount}x ore {itemId}"); } } return true; } num4 = amount - num10 * num2; num3 = num10; num9 = num8 * (float)num3; } MethodInfo addItemIntMethod = GetAddItemIntMethod(__instance); if (addItemIntMethod == null) { if (IsVerbose) { ManualLogSource log8 = Plugin.Log; if (log8 != null) { log8.LogInfo((object)"[InfernalForge] Could not find AddItem method on inventory"); } } return true; } ReflectionHelper.SetInstanceValue(instance, "mana", num5 - num9); bool flag3 = false; bool flag4 = true; _isSmeltingItem = true; try { flag3 = InvokeAddItem(addItemIntMethod, __instance, value, num3, sendNotification); if (num4 > 0) { flag4 = InvokeAddItem(addItemIntMethod, __instance, itemId, num4, sendNotification); } } finally { _isSmeltingItem = false; } if (!flag3) { ManualLogSource log9 = Plugin.Log; if (log9 != null) { log9.LogError((object)"[InfernalForge] Bar addition failed - reverting to normal ore pickup"); } ReflectionHelper.SetInstanceValue(instance, "mana", num5); return true; } if (!flag4) { ManualLogSource log10 = Plugin.Log; if (log10 != null) { log10.LogWarning((object)$"[InfernalForge] Leftover ore addition failed for {num4}x ore {itemId}"); } } SendGameNotification((num4 > 0) ? $"Infernal Forge: {num3} bar(s) from {num3 * num2} ore, {num4} ore left over (-{num9:F0} Mana)" : $"Infernal Forge: {num3} bar(s) from {amount} ore (-{num9:F0} Mana)", value, num3); ManualLogSource log11 = Plugin.Log; if (log11 != null) { log11.LogInfo((object)$"[InfernalForge] Smelted {num3 * num2}x ore {itemId} → {num3}x bar {value} (leftover: {num4}), cost {num9:F0} mana"); } return false; } catch (Exception ex) { ManualLogSource log12 = Plugin.Log; if (log12 != null) { log12.LogWarning((object)("[InfernalForge] Error: " + ex.Message)); } ManualLogSource log13 = Plugin.Log; if (log13 != null) { log13.LogWarning((object)("[InfernalForge] Stack: " + ex.StackTrace)); } _isSmeltingItem = false; return true; } } } public static class CombatPatches { private static bool _nineLivesProcced; private static float _nineLivesHealToHP; public static void ModifyDamageReceived(ref DamageInfo damageInfo) { if (Plugin.CriticalBirthrightHarmonyIncomplete || !RacialConfig.EnableRacialBonuses.Value) { return; } RacialBonusManager racialBonusManager = Plugin.GetRacialBonusManager(); if (racialBonusManager == null) { return; } Race? playerRace = racialBonusManager.GetPlayerRace(); if (!playerRace.HasValue) { return; } if (racialBonusManager.HasBonus(BonusType.Defense)) { float bonusValue = racialBonusManager.GetBonusValue(BonusType.Defense); DamageInfo obj = damageInfo; obj.damage *= 1f - bonusValue / 100f; } if (playerRace.Value == Race.AmariCat && AbilityConfig.EnableNineLives != null && AbilityConfig.EnableNineLives.Value) { TryNineLives(ref damageInfo); } if (AbilityConfig.EnableActiveAbilities != null && AbilityConfig.EnableActiveAbilities.Value) { if (playerRace.Value == Race.Angel && AbilityConfig.EnableDivineWard.Value) { ApplyDivineWard(ref damageInfo); } if (playerRace.Value == Race.AmariReptile && AbilityConfig.EnableHardenedScales.Value) { ActiveAbilityManager.AddStack("HardenedScales", AbilityConfig.HardenedScalesMaxStacks.Value, AbilityConfig.HardenedScalesDecayTime.Value); } } } public static void ResetNineLives() { _nineLivesProcced = false; _nineLivesHealToHP = 0f; } private static void TryNineLives(ref DamageInfo damageInfo) { try { Player instance = Player.Instance; if ((Object)(object)instance == (Object)null) { return; } float maxHealth = instance.MaxHealth; float num = ReflectionHelper.TryGetValue(instance, "health", maxHealth); if (!(num <= 0f) && !(maxHealth <= 0f) && !(num - damageInfo.damage > 0f)) { float num2 = Mathf.Min((AbilityConfig.NineLivesChance != null) ? (AbilityConfig.NineLivesChance.Value / 100f) : 0.1f, 0.6f); if (!(Random.value >= num2)) { damageInfo.damage = 0f; float num3 = ((AbilityConfig.NineLivesHealPercent != null) ? (AbilityConfig.NineLivesHealPercent.Value / 100f) : 0.5f); _nineLivesHealToHP = maxHealth * num3; _nineLivesProcced = true; } } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[NineLives] Error: " + ex.Message)); } } } public static void OnDamageReceivedPostfix(Player __instance) { if (Plugin.CriticalBirthrightHarmonyIncomplete) { return; } if (_nineLivesProcced) { try { ReflectionHelper.SetInstanceValue(__instance, "health", _nineLivesHealToHP); ReflectionHelper.SetInstanceValue(__instance, "Health", _nineLivesHealToHP); int num = ((AbilityConfig.NineLivesHealPercent != null) ? ((int)AbilityConfig.NineLivesHealPercent.Value) : 50); Type type = ReflectionHelper.FindWishType("NotificationStack"); if (type != null) { object singletonInstance = ReflectionHelper.GetSingletonInstance(type); if (singletonInstance != null) { ReflectionHelper.InvokeMethod(singletonInstance, "SendNotification", $"Nine Lives! Survived with {num}% HP!"); } } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[NineLives] Postfix error: " + ex.Message)); } } _nineLivesProcced = false; _nineLivesHealToHP = 0f; } if (!RacialConfig.EnableRacialBonuses.Value || AbilityConfig.EnableActiveAbilities == null || !AbilityConfig.EnableActiveAbilities.Value) { return; } RacialBonusManager racialBonusManager = Plugin.GetRacialBonusManager(); if (racialBonusManager != null) { Race? playerRace = racialBonusManager.GetPlayerRace(); if (playerRace.HasValue && playerRace.Value == Race.Angel && AbilityConfig.EnableDivineWard.Value) { CheckDivineWardTrigger(__instance); } } } private static void ApplyDivineWard(ref DamageInfo damageInfo) { if (ActiveAbilityManager.IsAbilityActive("DivineWard")) { float num = AbilityConfig.DivineWardDamageReduction.Value / 100f; DamageInfo obj = damageInfo; obj.damage *= 1f - num; } } private static void CheckDivineWardTrigger(Player player) { try { if (ActiveAbilityManager.IsAbilityActive("DivineWard") || ActiveAbilityManager.IsOnCooldown("DivineWard")) { return; } float maxHealth = player.MaxHealth; if (!(ReflectionHelper.TryGetValue(player, "health", maxHealth) / maxHealth * 100f <= AbilityConfig.DivineWardHPTrigger.Value)) { return; } float maxMana = player.MaxMana; float num = ReflectionHelper.TryGetValue(player, "mana", 0f); float num2 = maxMana * (AbilityConfig.DivineWardManaCostPercent.Value / 100f); if (!(num >= num2)) { return; } ReflectionHelper.SetInstanceValue(player, "mana", num - num2); ActiveAbilityManager.ActivateAbility("DivineWard", AbilityConfig.DivineWardDuration.Value, AbilityConfig.DivineWardCooldown.Value); try { Type type = ReflectionHelper.FindWishType("NotificationStack"); if (type != null) { object singletonInstance = ReflectionHelper.GetSingletonInstance(type); if (singletonInstance != null) { ReflectionHelper.InvokeMethod(singletonInstance, "SendNotification", $"Divine Ward activated! (-{num2:F0} Mana)"); } } } catch { } Plugin.Log.LogDebug((object)$"[DivineWard] Activated! Duration: {AbilityConfig.DivineWardDuration.Value}s, Mana cost: {num2:F0}"); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[DivineWard] Error: " + ex.Message)); } } } public static class EconomyPatches { public static void ModifyRelationshipGain(ref float increase) { if (!RacialConfig.EnableRacialBonuses.Value || increase <= 0f) { return; } RacialBonusManager racialBonusManager = Plugin.GetRacialBonusManager(); if (racialBonusManager == null) { return; } float num = increase; if (racialBonusManager.HasBonus(BonusType.RelationshipGain)) { float bonusValue = racialBonusManager.GetBonusValue(BonusType.RelationshipGain); increase *= 1f + bonusValue / 100f; } if (AbilityConfig.EnableRacialDrawbacks != null && AbilityConfig.EnableRacialDrawbacks.Value) { Race? playerRace = racialBonusManager.GetPlayerRace(); if (playerRace.HasValue && playerRace.Value == Race.Demon) { float value = AbilityConfig.DemonDistrustedRelationshipPenalty.Value; increase *= 1f - value / 100f; } } if (Mathf.Abs(increase - num) > 0.001f) { Plugin.Log.LogDebug((object)$"RelationshipGain modified: {num} -> {increase}"); } } public static void ModifyBuyPrice(ref int price) { if (!RacialConfig.EnableRacialBonuses.Value) { return; } RacialBonusManager racialBonusManager = Plugin.GetRacialBonusManager(); if (racialBonusManager != null && racialBonusManager.HasBonus(BonusType.ShopDiscount)) { float bonusValue = racialBonusManager.GetBonusValue(BonusType.ShopDiscount); int num = price; price = Mathf.RoundToInt((float)price * (1f - bonusValue / 100f)); if (price < 1) { price = 1; } Plugin.Log.LogDebug((object)$"ShopDiscount applied: {num} -> {price}"); } } public static void OnBeforeShopBuyItem(object __0, int __1) { ApplyShopDiscountToItemInfo(__0); } public static void OnBeforeShopBuyItemSingle(object __0) { ApplyShopDiscountToItemInfo(__0); } private static void ApplyShopDiscountToItemInfo(object itemInfo) { if (itemInfo != null) { FieldInfo fieldInfo = AccessTools.Field(itemInfo.GetType(), "price"); if (!(fieldInfo == null)) { int price = (int)fieldInfo.GetValue(itemInfo); ModifyBuyPrice(ref price); fieldInfo.SetValue(itemInfo, price); } } } } public static class FarmingPatches { } public static class PlayerPatches { public static void OnPlayerInitialized(Player __instance, bool host) { Plugin.EnsureRunner(); BirthrightRunner.ResetAllStateForNewSave(); RaceDetectionService.DetectFromPlayerInitialized(); } public static void OnPlayerInitialize(Player __instance) { RaceDetectionService.DetectIfNeeded(); } public static void ResetRaceDetection() { RaceDetectionService.ResetRaceDetection(); } public static void RetryRaceDetection() { RaceDetectionService.RetryRaceDetection(); } public static void OnGameSaveLoadCharacter(int characterNumber) { try { Plugin.Log.LogInfo((object)$"[RaceDetection] GameSave.LoadCharacter({characterNumber}) — snapshot + resetting Birthright state"); BirthrightGameSaveContext.RecordLoadCharacter(characterNumber); BirthrightRunner.ResetAllStateForNewSave(); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[RaceDetection] OnGameSaveLoadCharacter: " + ex.Message)); } } } public static class RegenPatches { } public static class SoulHarvestPatches { public static void OnEnemyDiePostfix(bool fromLocalPlayer) { if (!fromLocalPlayer || !RacialConfig.EnableRacialBonuses.Value || AbilityConfig.EnableActiveAbilities == null || !AbilityConfig.EnableActiveAbilities.Value || AbilityConfig.EnableSoulHarvest == null || !AbilityConfig.EnableSoulHarvest.Value || !ActiveAbilityManager.IsRuntimeEnabled("SoulHarvest")) { return; } RacialBonusManager racialBonusManager = Plugin.GetRacialBonusManager(); if (racialBonusManager == null) { return; } Race? playerRace = racialBonusManager.GetPlayerRace(); if (!playerRace.HasValue || playerRace.Value != Race.Demon) { return; } try { Player instance = Player.Instance; if ((Object)(object)instance == (Object)null) { return; } int num = AbilityConfig.SoulHarvestGoldPerKill?.Value ?? 15; float num2 = AbilityConfig.SoulHarvestHPCostPercent?.Value ?? 1f; if (num > 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] { num, true, false, true }); } } if (num2 > 0f) { float maxHealth = instance.MaxHealth; float num3 = ReflectionHelper.TryGetValue(instance, "health", -1f); if (num3 < 0f) { num3 = ReflectionHelper.TryGetValue(instance, "Health", maxHealth); } float num4 = maxHealth * (num2 / 100f); float num5 = Mathf.Max(1f, num3 - num4); ReflectionHelper.SetInstanceValue(instance, "Health", num5); } AbilityPatches.SendGameNotification($"Soul Harvest: +{num} gold" + ((num2 > 0f) ? $" (-{num2}% HP)" : "")); } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[SoulHarvest] OnEnemyDiePostfix: " + ex.Message)); } } } } public static class StatPatches { private static bool _isApplyingBonuses = false; private static int _cachedFrame = -1; private static Dictionary _perFrameStatCache; private const string FasterRacesGuid = "com.azraelgodking.fasterraces"; private static int _fasterRacesStateFrame = -1; private static bool _fasterRacesSpeedActive; private static bool IsAttackSpeedStat(StatType stat) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Invalid comparison between Unknown and I4 //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Invalid comparison between Unknown and I4 if ((int)stat != 3) { return (int)stat == 56; } return true; } private static bool IsFasterRacesSpeedBonusActive() { if (_fasterRacesStateFrame == Time.frameCount) { return _fasterRacesSpeedActive; } _fasterRacesStateFrame = Time.frameCount; _fasterRacesSpeedActive = false; if (Chainloader.PluginInfos == null || !Chainloader.PluginInfos.TryGetValue("com.azraelgodking.fasterraces", out var value)) { return false; } _fasterRacesSpeedActive = false; BaseUnityPlugin val = ((value != null) ? value.Instance : null); if ((Object)(object)val == (Object)null) { return _fasterRacesSpeedActive; } Type type = ((object)val).GetType(); try { PropertyInfo property = type.GetProperty("IsSpeedBonusActive", BindingFlags.Static | BindingFlags.Public); if (property != null && property.PropertyType == typeof(bool)) { _fasterRacesSpeedActive = (bool)property.GetValue(null, null); return _fasterRacesSpeedActive; } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[StatPatches] FasterRaces reflection failed; defaulting IsSpeedBonusActive=false. " + ex.Message)); } } return _fasterRacesSpeedActive; } [HarmonyPriority(100)] public static void ModifyGetStat(StatType stat, ref float __result) { //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_00bd: 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_00d8: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_012e: Unknown result type (might be due to invalid IL or missing references) //IL_00fa: Unknown result type (might be due to invalid IL or missing references) //IL_011b: Unknown result type (might be due to invalid IL or missing references) if (Plugin.CriticalBirthrightHarmonyIncomplete || _isApplyingBonuses || !StatFrameCache.IsCacheValid) { return; } int frameCount = Time.frameCount; if (frameCount != _cachedFrame) { _cachedFrame = frameCount; _perFrameStatCache = _perFrameStatCache ?? new Dictionary(); _perFrameStatCache.Clear(); } if (_perFrameStatCache.TryGetValue(stat, out var value)) { __result = value; } else { if (!RacialConfig.EnableRacialBonuses.Value) { return; } RacialBonusManager racialBonusManager = Plugin.GetRacialBonusManager(); if (racialBonusManager == null) { return; } Race? playerRace = racialBonusManager.GetPlayerRace(); if (!playerRace.HasValue) { return; } if (playerRace.Value != Race.AmariCat || RacialConfig.AmariCatReduceCombatStutter == null || !RacialConfig.AmariCatReduceCombatStutter.Value || !IsAttackSpeedStat(stat)) { _isApplyingBonuses = true; try { ApplyBaseBonuses(stat, ref __result, racialBonusManager); if (AbilityConfig.EnableActiveAbilities != null && AbilityConfig.EnableActiveAbilities.Value) { ApplyActiveAbilities(stat, ref __result, racialBonusManager, playerRace.Value); } if (AbilityConfig.EnableRacialDrawbacks != null && AbilityConfig.EnableRacialDrawbacks.Value) { ApplyDrawbacks(stat, ref __result, playerRace.Value); } if (AbilityConfig.EnableConditionalSynergies != null && AbilityConfig.EnableConditionalSynergies.Value) { ApplyConditionalSynergies(stat, ref __result, playerRace.Value); } _perFrameStatCache[stat] = __result; return; } finally { _isApplyingBonuses = false; } } _perFrameStatCache[stat] = __result; } } private static void ApplyBaseBonuses(StatType stat, ref float __result, RacialBonusManager manager) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Invalid comparison between Unknown and I4 //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00be: Invalid comparison between Unknown and I4 //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Expected I4, but got Unknown //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: Invalid comparison between Unknown and I4 //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Invalid comparison between Unknown and I4 //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Invalid comparison between Unknown and I4 if ((int)stat <= 45) { switch ((int)stat) { default: if ((int)stat == 45 && manager.HasBonus(BonusType.DodgeChance)) { float bonusValue6 = manager.GetBonusValue(BonusType.DodgeChance); __result += bonusValue6 / 100f; } return; case 2: if (manager.HasBonus(BonusType.MeleeStrength)) { __result = manager.ApplyBonus(__result, BonusType.MeleeStrength); } return; case 8: if (manager.HasBonus(BonusType.MagicPower)) { __result = manager.ApplyBonus(__result, BonusType.MagicPower); } return; case 14: if (manager.HasBonus(BonusType.CriticalChance)) { float bonusValue2 = manager.GetBonusValue(BonusType.CriticalChance); __result += bonusValue2 / 100f; } return; case 23: if (manager.HasBonus(BonusType.Defense)) { __result = manager.ApplyBonus(__result, BonusType.Defense); } return; case 3: break; case 6: if (!IsFasterRacesSpeedBonusActive() && manager.HasBonus(BonusType.MovementSpeed)) { __result = manager.ApplyBonus(__result, BonusType.MovementSpeed); } return; case 35: if (manager.HasBonus(BonusType.FarmingSpeed)) { __result = manager.ApplyBonus(__result, BonusType.FarmingSpeed); } return; case 21: if (manager.HasBonus(BonusType.MiningSpeed)) { __result = manager.ApplyBonus(__result, BonusType.MiningSpeed); } return; case 37: if (manager.HasBonus(BonusType.MiningYield)) { float bonusValue5 = manager.GetBonusValue(BonusType.MiningYield); __result += bonusValue5 / 100f; } return; case 38: if (manager.HasBonus(BonusType.WoodcuttingSpeed)) { __result = manager.ApplyBonus(__result, BonusType.WoodcuttingSpeed); } if (manager.HasBonus(BonusType.WoodcuttingYield)) { float bonusValue4 = manager.GetBonusValue(BonusType.WoodcuttingYield); __result += bonusValue4 / 100f; } return; case 20: if (manager.HasBonus(BonusType.FishingSpeed)) { __result = manager.ApplyBonus(__result, BonusType.FishingSpeed); } if (manager.HasBonus(BonusType.FishingLuck)) { __result = manager.ApplyBonus(__result, BonusType.FishingLuck); } return; case 22: if (manager.HasBonus(BonusType.ForagingChance)) { __result = manager.ApplyBonus(__result, BonusType.ForagingChance); } return; case 39: if (manager.HasBonus(BonusType.CraftingSpeed)) { __result = manager.ApplyBonus(__result, BonusType.CraftingSpeed); } return; case 0: if (manager.HasBonus(BonusType.MaxHealth)) { __result = manager.ApplyBonus(__result, BonusType.MaxHealth); } return; case 1: if (manager.HasBonus(BonusType.MaxMana)) { __result = manager.ApplyBonus(__result, BonusType.MaxMana); } return; case 4: if (manager.HasBonus(BonusType.HealthRegen)) { __result = manager.ApplyBonus(__result, BonusType.HealthRegen); } return; case 5: if (manager.HasBonus(BonusType.ManaRegen)) { __result = manager.ApplyBonus(__result, BonusType.ManaRegen); } return; case 16: if (manager.HasBonus(BonusType.GoldFind)) { float bonusValue3 = manager.GetBonusValue(BonusType.GoldFind); __result += bonusValue3 / 100f; } return; case 28: case 32: case 40: if (manager.HasBonus(BonusType.ExperienceGain)) { float bonusValue = manager.GetBonusValue(BonusType.ExperienceGain); __result += bonusValue / 100f; } return; case 7: case 9: case 10: case 11: case 12: case 13: case 15: case 17: case 18: case 19: case 24: case 25: case 26: case 27: case 29: case 30: case 31: case 33: case 34: case 36: return; } } else { if ((int)stat == 50) { if (manager.HasBonus(BonusType.ForagingChance)) { float bonusValue7 = manager.GetBonusValue(BonusType.ForagingChance); __result += bonusValue7 / 100f; } return; } if ((int)stat != 56) { if ((int)stat == 63 && manager.HasBonus(BonusType.CropQuality)) { float bonusValue8 = manager.GetBonusValue(BonusType.CropQuality); __result += bonusValue8 / 100f; } return; } } if (manager.HasBonus(BonusType.AttackSpeed)) { __result = manager.ApplyBonus(__result, BonusType.AttackSpeed); } } private static void ApplyActiveAbilities(StatType stat, ref float __result, RacialBonusManager manager, Race race) { //IL_0233: Unknown result type (might be due to invalid IL or missing references) //IL_0235: Invalid comparison between Unknown and I4 //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Invalid comparison between Unknown and I4 //IL_017c: Unknown result type (might be due to invalid IL or missing references) //IL_017f: Invalid comparison between Unknown and I4 //IL_01d5: Unknown result type (might be due to invalid IL or missing references) //IL_01d8: Invalid comparison between Unknown and I4 //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Invalid comparison between Unknown and I4 //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Invalid comparison between Unknown and I4 //IL_0273: Unknown result type (might be due to invalid IL or missing references) //IL_0276: Invalid comparison between Unknown and I4 //IL_013d: Unknown result type (might be due to invalid IL or missing references) //IL_013f: Invalid comparison between Unknown and I4 //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Invalid comparison between Unknown and I4 //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Invalid comparison between Unknown and I4 //IL_0295: Unknown result type (might be due to invalid IL or missing references) //IL_0298: Invalid comparison between Unknown and I4 //IL_02d1: Unknown result type (might be due to invalid IL or missing references) //IL_02d3: Invalid comparison between Unknown and I4 switch (race) { case Race.Demon: { if (!AbilityConfig.EnableBloodRage.Value) { break; } float cachedHPRatio = StatFrameCache.CachedHPRatio; float num4 = AbilityConfig.BloodRageHPThreshold.Value / 100f; if (cachedHPRatio <= num4) { if ((int)stat == 2) { __result *= 1f + AbilityConfig.BloodRageMeleeBonus.Value / 100f; } else if ((int)stat == 3) { __result *= 1f + AbilityConfig.BloodRageAttackSpeedBonus.Value / 100f; } } break; } case Race.Human: if (AbilityConfig.EnableQuickLearner.Value && ((int)stat == 40 || (int)stat == 32 || (int)stat == 28)) { float cachedQuickLearnerBonus = StatFrameCache.CachedQuickLearnerBonus; if (cachedQuickLearnerBonus > 0f) { __result += cachedQuickLearnerBonus / 100f; } } break; case Race.AmariDog: if (AbilityConfig.EnableLoyaltyAura.Value) { float bonusValue = ActiveAbilityManager.GetBonusValue("LoyaltyAura"); if (bonusValue > 0f) { __result *= 1f + bonusValue / 100f; } } break; case Race.AmariBird: if (!IsFasterRacesSpeedBonusActive() && AbilityConfig.EnableTailwind.Value && (int)stat == 6) { float bonusValue2 = ActiveAbilityManager.GetBonusValue("TailwindOutdoor"); if (bonusValue2 > 0f) { __result *= 1f + bonusValue2 / 100f; } } break; case Race.AmariReptile: if (AbilityConfig.EnableHardenedScales.Value && (int)stat == 23) { int stacks = ActiveAbilityManager.GetStacks("HardenedScales", AbilityConfig.HardenedScalesDecayTime.Value); if (stacks > 0) { float num = (float)stacks * AbilityConfig.HardenedScalesDefensePerStack.Value; __result *= 1f + num / 100f; } } break; case Race.Naga: if (AbilityConfig.EnableSerpentGrace.Value && (int)stat == 20 && StatFrameCache.CachedIsWinter) { float value = AbilityConfig.SerpentGraceFishingMultiplier.Value; float num2 = manager.GetBonusValue(BonusType.FishingSpeed) + manager.GetBonusValue(BonusType.FishingLuck); if (num2 > 0f) { float num3 = num2 * (value - 1f) / 100f; __result *= 1f + num3; } } break; case Race.FireElemental: if ((int)stat == 5 && AbilityConfig.EnableInfernalForge.Value && ActiveAbilityManager.IsRuntimeEnabled("InfernalForge")) { __result = 0f; } break; case Race.Elemental: if (AbilityConfig.EnableElementalResonance.Value && StatFrameCache.CachedIsInMine) { if ((int)stat == 21) { __result *= 1f + AbilityConfig.ElementalResonanceMiningSpeedBonus.Value / 100f; } else if ((int)stat == 37) { __result *= 1f + AbilityConfig.ElementalResonanceMiningDamageBonus.Value / 100f; } } if (AbilityConfig.EnableInfernalForge != null && AbilityConfig.EnableInfernalForge.Value && ElementalVariantResolver.ResolveElementalAbilityRace(race) != Race.WaterElemental && (int)stat == 5 && AbilityConfig.EnableInfernalForge.Value && ActiveAbilityManager.IsRuntimeEnabled("InfernalForge")) { __result = 0f; } break; case Race.Elf: case Race.Angel: case Race.WaterElemental: case Race.Amari: case Race.AmariCat: case Race.AmariAquatic: break; } } private static void ApplyDrawbacks(StatType stat, ref float __result, Race race) { //IL_00ac: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Invalid comparison between Unknown and I4 //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Invalid comparison between Unknown and I4 //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Unknown result type (might be due to invalid IL or missing references) //IL_011b: Unknown result type (might be due to invalid IL or missing references) //IL_011d: Invalid comparison between Unknown and I4 //IL_0142: 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: Invalid comparison between Unknown and I4 //IL_01b4: Unknown result type (might be due to invalid IL or missing references) //IL_01b6: Invalid comparison between Unknown and I4 //IL_01d8: Unknown result type (might be due to invalid IL or missing references) //IL_01da: Invalid comparison between Unknown and I4 //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00d0: Invalid comparison between Unknown and I4 //IL_0165: Unknown result type (might be due to invalid IL or missing references) //IL_0168: Invalid comparison between Unknown and I4 //IL_01fc: Unknown result type (might be due to invalid IL or missing references) //IL_01ff: Invalid comparison between Unknown and I4 switch (race) { case Race.FireElemental: if ((int)stat == 20) { float value8 = AbilityConfig.FireHydrophobicFishingPenalty.Value; __result *= 1f - value8 / 100f; } break; case Race.WaterElemental: if ((int)stat == 0) { float value13 = AbilityConfig.WaterFragileFormHPPenalty.Value; __result *= 1f - value13 / 100f; } break; case Race.Angel: if ((int)stat == 2) { float value5 = AbilityConfig.AngelPacifistMeleePenalty.Value; __result *= 1f - value5 / 100f; } break; case Race.Elf: if ((int)stat == 0) { float value3 = AbilityConfig.ElfFragileHPPenalty.Value; __result *= 1f - value3 / 100f; } else if ((int)stat == 23) { float value4 = AbilityConfig.ElfFragileDefensePenalty.Value; __result *= 1f - value4 / 100f; } break; case Race.AmariCat: if ((int)stat == 0) { float value10 = AbilityConfig.CatGlassCannonHPPenalty.Value; __result *= 1f - value10 / 100f; } break; case Race.AmariDog: if ((int)stat == 6) { float value6 = AbilityConfig.DogSlowStarterSpeedPenalty.Value; __result *= 1f - value6 / 100f; } break; case Race.AmariBird: if ((int)stat == 0) { float value11 = AbilityConfig.BirdHollowBonesHPPenalty.Value; __result *= 1f - value11 / 100f; } else if ((int)stat == 23) { float value12 = AbilityConfig.BirdHollowBonesDefensePenalty.Value; __result *= 1f - value12 / 100f; } break; case Race.AmariAquatic: if ((int)stat == 6) { float value9 = AbilityConfig.AquaticLandSlugSpeedPenalty.Value; __result *= 1f - value9 / 100f; } break; case Race.AmariReptile: if ((int)stat == 3) { float value7 = AbilityConfig.ReptileColdBloodedAttackSpeedPenalty.Value; __result *= 1f - value7 / 100f; } break; case Race.Naga: if ((int)stat == 6) { float value = AbilityConfig.NagaLandlockedSpeedPenalty.Value; __result *= 1f - value / 100f; } else if ((int)stat == 35) { float value2 = AbilityConfig.NagaLandlockedFarmingPenalty.Value; __result *= 1f - value2 / 100f; } break; case Race.Demon: case Race.Elemental: case Race.Amari: break; } } private static void ApplyConditionalSynergies(StatType stat, ref float __result, Race race) { //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Invalid comparison between Unknown and I4 //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Invalid comparison between Unknown and I4 //IL_0108: Unknown result type (might be due to invalid IL or missing references) //IL_010b: Invalid comparison between Unknown and I4 //IL_013f: Unknown result type (might be due to invalid IL or missing references) //IL_0141: Invalid comparison between Unknown and I4 //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Invalid comparison between Unknown and I4 //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Invalid comparison between Unknown and I4 //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Invalid comparison between Unknown and I4 //IL_00d5: Unknown result type (might be due to invalid IL or missing references) //IL_00d8: Invalid comparison between Unknown and I4 //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Invalid comparison between Unknown and I4 //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Invalid comparison between Unknown and I4 //IL_00da: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Invalid comparison between Unknown and I4 //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Invalid comparison between Unknown and I4 //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_00e0: Invalid comparison between Unknown and I4 //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Invalid comparison between Unknown and I4 if (race == Race.Angel && (int)stat == 8 && StatFrameCache.CachedIsDaytime) { __result *= 1f + AbilityConfig.AngelSolarPowerBonus.Value / 100f; } if (race == Race.Demon && (int)stat == 2 && !StatFrameCache.CachedIsDaytime) { __result *= 1f + AbilityConfig.DemonNightStalkerBonus.Value / 100f; } if (StatFrameCache.CachedSeason != null) { if (race == Race.Elf && StatFrameCache.CachedIsSpring && ((int)stat == 35 || (int)stat == 63 || (int)stat == 50)) { __result *= 1f + AbilityConfig.ElfSpringAwakeningBonus.Value / 100f; } if (race == Race.FireElemental && StatFrameCache.CachedIsSummer && ((int)stat == 2 || (int)stat == 8 || (int)stat == 3 || (int)stat == 14)) { __result *= 1f + AbilityConfig.FireSummerFuryBonus.Value / 100f; } if (race == Race.WaterElemental && StatFrameCache.CachedIsWinter && ((int)stat == 23 || (int)stat == 4 || (int)stat == 5)) { __result *= 1f + AbilityConfig.WaterWinterEmbraceBonus.Value / 100f; } } float cachedHPRatio = StatFrameCache.CachedHPRatio; if (race == Race.AmariReptile && (int)stat == 23 && cachedHPRatio <= AbilityConfig.ReptileLastStandHPThreshold.Value / 100f) { __result *= 1f + AbilityConfig.ReptileLastStandDefenseBonus.Value / 100f; } if (race == Race.Angel && (int)stat == 4 && cachedHPRatio <= AbilityConfig.AngelMartyrHPThreshold.Value / 100f) { __result *= AbilityConfig.AngelMartyrRegenMultiplier.Value; } } } } namespace HavensBirthright.Abilities { public static class AbilityConfig { public static ConfigEntry EnableActiveAbilities; public static ConfigEntry EnableRacialDrawbacks; public static ConfigEntry EnableConditionalSynergies; public static ConfigEntry ActiveAbilityToggleKey; public static ConfigEntry InfernalForgeVerboseLogging; public static ConfigEntry EnableTidalBlessing; public static ConfigEntry TidalBlessingHPCostPercent; public static ConfigEntry TidalBlessingHPThreshold; public static ConfigEntry TidalBlessingCooldown; public static ConfigEntry EnableInfernalForge; public static ConfigEntry InfernalForgeOrePerBar; public static ConfigEntry InfernalForgeManaThreshold; public static ConfigEntry InfernalForgeScanInterval; public static ConfigEntry EnableDivineWard; public static ConfigEntry DivineWardHPTrigger; public static ConfigEntry DivineWardDamageReduction; public static ConfigEntry DivineWardDuration; public static ConfigEntry DivineWardManaCostPercent; public static ConfigEntry DivineWardCooldown; public static ConfigEntry EnableFontOfLight; public static ConfigEntry FontOfLightInterval; public static ConfigEntry FontOfLightManaPercent; public static ConfigEntry FontOfLightManaThreshold; public static ConfigEntry FontOfLightGoldCost; public static ConfigEntry EnableBloodRage; public static ConfigEntry BloodRageHPThreshold; public static ConfigEntry BloodRageMeleeBonus; public static ConfigEntry BloodRageAttackSpeedBonus; public static ConfigEntry EnableSoulHarvest; public static ConfigEntry SoulHarvestGoldPerKill; public static ConfigEntry SoulHarvestHPCostPercent; public static ConfigEntry EnableNaturesBounty; public static ConfigEntry NaturesBountyDoubleChance; public static ConfigEntry EnableQuickLearner; public static ConfigEntry QuickLearnerSkillThreshold; public static ConfigEntry QuickLearnerBonusPerSkill; public static ConfigEntry QuickLearnerMaxBonus; public static ConfigEntry EnableNineLives; public static ConfigEntry NineLivesChance; public static ConfigEntry NineLivesHealPercent; public static ConfigEntry EnableLoyaltyAura; public static ConfigEntry LoyaltyAuraBonusPerNPC; public static ConfigEntry LoyaltyAuraMaxBonus; public static ConfigEntry EnableTailwind; public static ConfigEntry TailwindBonusPerMinute; public static ConfigEntry TailwindMaxBonus; public static ConfigEntry EnableWatersGift; public static ConfigEntry WatersGiftBonusFishChance; public static ConfigEntry EnableHardenedScales; public static ConfigEntry HardenedScalesDefensePerStack; public static ConfigEntry HardenedScalesMaxStacks; public static ConfigEntry HardenedScalesDecayTime; public static ConfigEntry EnableSerpentGrace; public static ConfigEntry SerpentGraceFishingMultiplier; public static ConfigEntry EnableElementalResonance; public static ConfigEntry ElementalResonanceMiningDamageBonus; public static ConfigEntry ElementalResonanceMiningSpeedBonus; public static ConfigEntry FireHydrophobicFishingPenalty; public static ConfigEntry WaterFragileFormHPPenalty; public static ConfigEntry AngelPacifistMeleePenalty; public static ConfigEntry DemonDistrustedRelationshipPenalty; public static ConfigEntry ElfFragileHPPenalty; public static ConfigEntry ElfFragileDefensePenalty; public static ConfigEntry CatGlassCannonHPPenalty; public static ConfigEntry DogSlowStarterSpeedPenalty; public static ConfigEntry BirdHollowBonesHPPenalty; public static ConfigEntry BirdHollowBonesDefensePenalty; public static ConfigEntry AquaticLandSlugSpeedPenalty; public static ConfigEntry ReptileColdBloodedAttackSpeedPenalty; public static ConfigEntry NagaLandlockedSpeedPenalty; public static ConfigEntry NagaLandlockedFarmingPenalty; public static ConfigEntry AngelSolarPowerBonus; public static ConfigEntry DemonNightStalkerBonus; public static ConfigEntry ElfSpringAwakeningBonus; public static ConfigEntry FireSummerFuryBonus; public static ConfigEntry WaterWinterEmbraceBonus; public static ConfigEntry ReptileLastStandDefenseBonus; public static ConfigEntry ReptileLastStandHPThreshold; public static ConfigEntry AngelMartyrRegenMultiplier; public static ConfigEntry AngelMartyrHPThreshold; public static void Initialize(ConfigFile config) { EnableActiveAbilities = config.Bind("Active Abilities", "EnableActiveAbilities", true, "Master toggle for all active racial abilities"); EnableRacialDrawbacks = config.Bind("Drawbacks", "EnableRacialDrawbacks", false, "Master toggle for racial drawbacks (penalties). Disabled by default."); EnableConditionalSynergies = config.Bind("Synergies", "EnableConditionalSynergies", true, "Master toggle for conditional synergies (time-of-day, season, HP threshold)"); ActiveAbilityToggleKey = config.Bind("Active Abilities", "ToggleKey", (KeyCode)290, "Key to turn your race's F9-toggleable ability ON or OFF (press any time; flip as often as you like). Those abilities start OFF each new session/character until you press this key the first time."); InfernalForgeVerboseLogging = config.Bind("Debug", "InfernalForgeVerboseLogging", false, "Enable verbose logging for Infernal Forge to debug issues. Logs every decision point when ore is picked up."); EnableTidalBlessing = config.Bind("Active Abilities - Water Elemental", "EnableTidalBlessing", true, "Automatically water nearby tilled plots at the cost of HP"); TidalBlessingHPCostPercent = config.Bind("Active Abilities - Water Elemental", "HPCostPercent", 3f, "Percentage of max HP consumed per plot watered"); TidalBlessingHPThreshold = config.Bind("Active Abilities - Water Elemental", "HPSafetyThreshold", 20f, "Won't activate below this HP percentage"); TidalBlessingCooldown = config.Bind("Active Abilities - Water Elemental", "Cooldown", 2f, "Seconds between activations"); EnableInfernalForge = config.Bind("Active Abilities - Fire Elemental", "EnableInfernalForge", true, "Automatically smelt raw ore into bars when picked up"); InfernalForgeOrePerBar = config.Bind("Active Abilities - Fire Elemental", "OrePerBar", 3, "Number of ore required to produce 1 bar (game default is 3)"); InfernalForgeManaThreshold = config.Bind("Active Abilities - Fire Elemental", "ManaSafetyThreshold", 10f, "Won't activate below this mana percentage"); InfernalForgeScanInterval = config.Bind("Active Abilities - Fire Elemental", "ScanIntervalSeconds", 60f, "Seconds between inventory scans for ore to smelt into bars. Default 60s (1 minute) to simulate actual smelting time."); EnableDivineWard = config.Bind("Active Abilities - Angel", "EnableDivineWard", true, "Auto-activate damage shield when HP drops low"); DivineWardHPTrigger = config.Bind("Active Abilities - Angel", "HPTriggerPercent", 25f, "HP percentage that triggers the ward"); DivineWardDamageReduction = config.Bind("Active Abilities - Angel", "DamageReduction", 50f, "Percentage of damage reduced while ward is active"); DivineWardDuration = config.Bind("Active Abilities - Angel", "Duration", 8f, "Duration of the ward in seconds"); DivineWardManaCostPercent = config.Bind("Active Abilities - Angel", "ManaCostPercent", 30f, "Percentage of max mana consumed on activation"); DivineWardCooldown = config.Bind("Active Abilities - Angel", "Cooldown", 120f, "Cooldown in seconds"); EnableFontOfLight = config.Bind("Active Abilities - Angel", "EnableFontOfLight", true, "Periodic mana restore at the cost of gold (Celestial ability). Only affects Angels."); FontOfLightInterval = config.Bind("Active Abilities - Angel", "FontOfLightInterval", 45f, "Seconds between Font of Light triggers"); FontOfLightManaPercent = config.Bind("Active Abilities - Angel", "FontOfLightManaPercent", 5f, "Percentage of max mana restored each trigger"); FontOfLightManaThreshold = config.Bind("Active Abilities - Angel", "FontOfLightManaThreshold", 80f, "Only trigger when current mana is below this percentage"); FontOfLightGoldCost = config.Bind("Active Abilities - Angel", "FontOfLightGoldCost", 10, "Gold deducted from the player each time Font of Light triggers"); EnableBloodRage = config.Bind("Active Abilities - Demon", "EnableBloodRage", true, "Gain combat bonuses when HP is low"); BloodRageHPThreshold = config.Bind("Active Abilities - Demon", "HPThreshold", 30f, "HP percentage threshold to activate Blood Rage"); BloodRageMeleeBonus = config.Bind("Active Abilities - Demon", "MeleeDamageBonus", 25f, "Percentage bonus to melee damage while active"); BloodRageAttackSpeedBonus = config.Bind("Active Abilities - Demon", "AttackSpeedBonus", 15f, "Percentage bonus to attack speed while active"); EnableSoulHarvest = config.Bind("Active Abilities - Demon", "EnableSoulHarvest", true, "Bonus gold when defeating enemies, at the cost of HP per kill (Celestial ability). Only affects Demons."); SoulHarvestGoldPerKill = config.Bind("Active Abilities - Demon", "SoulHarvestGoldPerKill", 15, "Bonus gold granted when you defeat an enemy"); SoulHarvestHPCostPercent = config.Bind("Active Abilities - Demon", "SoulHarvestHPCostPercent", 1f, "Percentage of max HP lost per enemy kill (cost)"); EnableNaturesBounty = config.Bind("Active Abilities - Elf", "EnableNaturesBounty", true, "Chance for double crop harvest"); NaturesBountyDoubleChance = config.Bind("Active Abilities - Elf", "DoubleHarvestChance", 15f, "Percentage chance for double crop yield"); EnableQuickLearner = config.Bind("Active Abilities - Human", "EnableQuickLearner", true, "XP bonus scales with leveled skills"); QuickLearnerSkillThreshold = config.Bind("Active Abilities - Human", "SkillLevelThreshold", 5, "Minimum skill level to count as 'leveled'"); QuickLearnerBonusPerSkill = config.Bind("Active Abilities - Human", "BonusPerSkill", 5f, "Percentage XP bonus per qualifying skill"); QuickLearnerMaxBonus = config.Bind("Active Abilities - Human", "MaxBonus", 25f, "Maximum XP bonus from Quick Learner"); EnableNineLives = config.Bind("Amari Cat", "EnableNineLives", true, "Passive: When taking lethal damage, chance to survive and heal to a percentage of max HP instead of dying"); NineLivesChance = config.Bind("Amari Cat", "NineLivesChance", 10f, "Passive: Percent chance to trigger Nine Lives on lethal damage (max 60%)"); NineLivesHealPercent = config.Bind("Amari Cat", "NineLivesHealPercent", 50f, "Passive: When Nine Lives triggers, heal to this percentage of max HP"); EnableLoyaltyAura = config.Bind("Active Abilities - Amari Dog", "EnableLoyaltyAura", true, "Stat bonus based on max-friendship NPCs"); LoyaltyAuraBonusPerNPC = config.Bind("Active Abilities - Amari Dog", "BonusPerNPC", 3f, "Percentage stat bonus per max-friendship NPC"); LoyaltyAuraMaxBonus = config.Bind("Active Abilities - Amari Dog", "MaxBonus", 15f, "Maximum stat bonus from Loyalty Aura"); EnableTailwind = config.Bind("Active Abilities - Amari Bird", "EnableTailwind", true, "Movement speed increases while outdoors"); TailwindBonusPerMinute = config.Bind("Active Abilities - Amari Bird", "BonusPerMinute", 2f, "Percentage movement speed bonus per minute outdoors"); TailwindMaxBonus = config.Bind("Active Abilities - Amari Bird", "MaxBonus", 20f, "Maximum movement speed bonus from Tailwind"); EnableWatersGift = config.Bind("Active Abilities - Amari Aquatic", "EnableWatersGift", true, "Chance for bonus fish when catching"); WatersGiftBonusFishChance = config.Bind("Active Abilities - Amari Aquatic", "BonusFishChance", 10f, "Percentage chance to catch a bonus fish"); EnableHardenedScales = config.Bind("Active Abilities - Amari Reptile", "EnableHardenedScales", true, "Taking hits grants stacking defense"); HardenedScalesDefensePerStack = config.Bind("Active Abilities - Amari Reptile", "DefensePerStack", 2f, "Percentage defense bonus per stack"); HardenedScalesMaxStacks = config.Bind("Active Abilities - Amari Reptile", "MaxStacks", 10, "Maximum number of stacks"); HardenedScalesDecayTime = config.Bind("Active Abilities - Amari Reptile", "DecayTime", 10f, "Seconds before stacks reset if not hit"); EnableSerpentGrace = config.Bind("Active Abilities - Naga", "EnableSerpentGrace", true, "Double fishing bonuses during rain or winter"); SerpentGraceFishingMultiplier = config.Bind("Active Abilities - Naga", "FishingMultiplier", 2f, "Multiplier for fishing bonuses during rain/winter"); EnableElementalResonance = config.Bind("Active Abilities - Elemental", "EnableElementalResonance", true, "Mining bonuses while in underground/mine scenes"); ElementalResonanceMiningDamageBonus = config.Bind("Active Abilities - Elemental", "MiningDamageBonus", 20f, "Percentage mining damage bonus in mines"); ElementalResonanceMiningSpeedBonus = config.Bind("Active Abilities - Elemental", "MiningSpeedBonus", 15f, "Percentage mining speed bonus in mines"); FireHydrophobicFishingPenalty = config.Bind("Drawbacks - Fire Elemental", "FishingPenalty", 20f, "Percentage reduction to fishing speed and luck"); WaterFragileFormHPPenalty = config.Bind("Drawbacks - Water Elemental", "MaxHPPenalty", 10f, "Percentage reduction to maximum HP"); AngelPacifistMeleePenalty = config.Bind("Drawbacks - Angel", "MeleeDamagePenalty", 10f, "Percentage reduction to melee damage"); DemonDistrustedRelationshipPenalty = config.Bind("Drawbacks - Demon", "RelationshipPenalty", 15f, "Percentage reduction to relationship gain"); ElfFragileHPPenalty = config.Bind("Drawbacks - Elf", "MaxHPPenalty", 10f, "Percentage reduction to maximum HP"); ElfFragileDefensePenalty = config.Bind("Drawbacks - Elf", "DefensePenalty", 5f, "Percentage reduction to defense"); CatGlassCannonHPPenalty = config.Bind("Drawbacks - Amari Cat", "MaxHPPenalty", 15f, "Percentage reduction to maximum HP"); DogSlowStarterSpeedPenalty = config.Bind("Drawbacks - Amari Dog", "MovementSpeedPenalty", 10f, "Percentage reduction to movement speed"); BirdHollowBonesHPPenalty = config.Bind("Drawbacks - Amari Bird", "MaxHPPenalty", 15f, "Percentage reduction to maximum HP"); BirdHollowBonesDefensePenalty = config.Bind("Drawbacks - Amari Bird", "DefensePenalty", 10f, "Percentage reduction to defense"); AquaticLandSlugSpeedPenalty = config.Bind("Drawbacks - Amari Aquatic", "MovementSpeedPenalty", 10f, "Percentage reduction to movement speed on land"); ReptileColdBloodedAttackSpeedPenalty = config.Bind("Drawbacks - Amari Reptile", "AttackSpeedPenalty", 10f, "Percentage reduction to attack speed"); NagaLandlockedSpeedPenalty = config.Bind("Drawbacks - Naga", "MovementSpeedPenalty", 10f, "Percentage reduction to movement speed"); NagaLandlockedFarmingPenalty = config.Bind("Drawbacks - Naga", "FarmingPenalty", 10f, "Percentage reduction to farming speed"); AngelSolarPowerBonus = config.Bind("Synergies - Time of Day", "AngelSolarPowerBonus", 10f, "Percentage magic damage bonus for Angels during daytime"); DemonNightStalkerBonus = config.Bind("Synergies - Time of Day", "DemonNightStalkerBonus", 10f, "Percentage melee damage bonus for Demons during nighttime"); ElfSpringAwakeningBonus = config.Bind("Synergies - Season", "ElfSpringAwakeningBonus", 15f, "Percentage farming bonus for Elves during Spring"); FireSummerFuryBonus = config.Bind("Synergies - Season", "FireSummerFuryBonus", 10f, "Percentage combat bonus for Fire Elementals during Summer"); WaterWinterEmbraceBonus = config.Bind("Synergies - Season", "WaterWinterEmbraceBonus", 15f, "Percentage defense/regen bonus for Water Elementals during Winter"); ReptileLastStandDefenseBonus = config.Bind("Synergies - Health Threshold", "ReptileLastStandDefense", 25f, "Percentage defense bonus for Amari Reptile when HP is low"); ReptileLastStandHPThreshold = config.Bind("Synergies - Health Threshold", "ReptileLastStandThreshold", 40f, "HP percentage threshold for Reptile Last Stand"); AngelMartyrRegenMultiplier = config.Bind("Synergies - Health Threshold", "AngelMartyrRegenMultiplier", 3f, "Health regen multiplier for Angels when HP is very low"); AngelMartyrHPThreshold = config.Bind("Synergies - Health Threshold", "AngelMartyrThreshold", 20f, "HP percentage threshold for Angel Martyr's Light"); } } public static class ActiveAbilityManager { public class AbilityState { public bool IsActive { get; set; } public float ExpiryTime { get; set; } public float CooldownUntil { get; set; } public int Stacks { get; set; } public float LastStackTime { get; set; } public float BonusValue { get; set; } } private static readonly Dictionary _abilities = new Dictionary(); private static readonly Dictionary _runtimeToggles = new Dictionary(); public const string DivineWard = "DivineWard"; public const string BloodRage = "BloodRage"; public const string HardenedScales = "HardenedScales"; public const string TailwindOutdoor = "TailwindOutdoor"; public const string TidalBlessing = "TidalBlessing"; public const string InfernalForge = "InfernalForge"; public const string QuickLearner = "QuickLearner"; public const string SerpentGrace = "SerpentGrace"; public const string ElementalResonance = "ElementalResonance"; public const string LoyaltyAura = "LoyaltyAura"; public const string FontOfLight = "FontOfLight"; public const string SoulHarvest = "SoulHarvest"; public static AbilityState GetState(string abilityKey) { if (!_abilities.TryGetValue(abilityKey, out AbilityState value)) { value = new AbilityState(); _abilities[abilityKey] = value; } return value; } public static bool ActivateAbility(string abilityKey, float duration, float cooldown) { AbilityState state = GetState(abilityKey); if (Time.time < state.CooldownUntil) { return false; } state.IsActive = true; state.ExpiryTime = Time.time + duration; state.CooldownUntil = Time.time + cooldown; return true; } public static bool IsAbilityActive(string abilityKey) { AbilityState state = GetState(abilityKey); if (state.IsActive && Time.time >= state.ExpiryTime) { state.IsActive = false; } return state.IsActive; } public static bool IsOnCooldown(string abilityKey) { AbilityState state = GetState(abilityKey); return Time.time < state.CooldownUntil; } public static int AddStack(string abilityKey, int maxStacks, float stackDuration) { AbilityState state = GetState(abilityKey); if (state.Stacks > 0 && Time.time - state.LastStackTime > stackDuration) { state.Stacks = 0; } if (state.Stacks < maxStacks) { state.Stacks++; } state.LastStackTime = Time.time; return state.Stacks; } public static int GetStacks(string abilityKey, float stackDuration) { AbilityState state = GetState(abilityKey); if (state.Stacks > 0 && Time.time - state.LastStackTime > stackDuration) { state.Stacks = 0; } return state.Stacks; } public static void SetBonusValue(string abilityKey, float value) { GetState(abilityKey).BonusValue = value; } public static float GetBonusValue(string abilityKey) { return GetState(abilityKey).BonusValue; } public static bool IsRuntimeEnabled(string abilityKey) { bool value; return _runtimeToggles.TryGetValue(abilityKey, out value) && value; } public static bool ToggleRuntime(string abilityKey) { bool flag = IsRuntimeEnabled(abilityKey); _runtimeToggles[abilityKey] = !flag; return !flag; } public static void ResetAll() { _abilities.Clear(); _runtimeToggles.Clear(); } } }