using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using SunhavenMods.Shared; using UnityEngine; using UnityEngine.SceneManagement; 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("FasterRaces")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("2.1.1.0")] [assembly: AssemblyInformationalVersion("2.1.1+79057c8571b97b27a9323b455b8f88a51829b688")] [assembly: AssemblyProduct("FasterRaces")] [assembly: AssemblyTitle("FasterRaces")] [assembly: AssemblyVersion("2.1.1.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace SunhavenMods.Shared { public static class ConfigFileHelper { public static ConfigFile CreateNamedConfig(string pluginGuid, string configFileName, Action 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 SharedCodeRevision { public const string Value = "2026.07.28"; } public sealed class ModHealthReport { public string PluginGuid { get; set; } public string DisplayName { get; set; } public string PluginVersion { get; set; } public string SharedCodeRevision { get; set; } public string IntegrationSummary { get; set; } public string Mode { get; set; } public string CharacterName { get; set; } public bool? DataLoaded { get; set; } public string LastPersistenceOutcome { get; set; } public string LastError { get; set; } public DateTime ReportedUtc { get; set; } public ModHealthReport Clone() { return new ModHealthReport { PluginGuid = PluginGuid, DisplayName = DisplayName, PluginVersion = PluginVersion, SharedCodeRevision = SharedCodeRevision, IntegrationSummary = IntegrationSummary, Mode = Mode, CharacterName = CharacterName, DataLoaded = DataLoaded, LastPersistenceOutcome = LastPersistenceOutcome, LastError = LastError, ReportedUtc = ReportedUtc }; } public void AppendDiagnosticDump(StringBuilder sb) { if (sb != null) { sb.AppendLine("guid: " + (PluginGuid ?? "—")); sb.AppendLine("name: " + (DisplayName ?? "—")); sb.AppendLine("version: " + (PluginVersion ?? "—")); sb.AppendLine("shared: " + (SharedCodeRevision ?? "—")); sb.AppendLine("integrations: " + (IntegrationSummary ?? "—")); sb.AppendLine("mode: " + (Mode ?? "—")); sb.AppendLine("character: " + (string.IsNullOrEmpty(CharacterName) ? "—" : CharacterName)); sb.AppendLine("data loaded: " + (DataLoaded.HasValue ? DataLoaded.Value.ToString() : "—")); sb.AppendLine("last save: " + (LastPersistenceOutcome ?? "—")); sb.AppendLine("last error: " + (LastError ?? "—")); sb.AppendLine("reported: " + ((ReportedUtc == default(DateTime)) ? "—" : ReportedUtc.ToLocalTime().ToString("u"))); } } } public static class ModDiagnostics { private static readonly Dictionary ReportsByPluginGuid = new Dictionary(StringComparer.OrdinalIgnoreCase); private static readonly object Lock = new object(); public static void ReportStartup(ModHealthReport report) { if (report == null || string.IsNullOrWhiteSpace(report.PluginGuid)) { return; } report.ReportedUtc = DateTime.UtcNow; if (string.IsNullOrEmpty(report.SharedCodeRevision)) { report.SharedCodeRevision = "2026.07.28"; } lock (Lock) { ReportsByPluginGuid[report.PluginGuid] = report.Clone(); } } public static void ReportRuntime(string pluginGuid, Action update) { if (string.IsNullOrWhiteSpace(pluginGuid) || update == null) { return; } lock (Lock) { if (!ReportsByPluginGuid.TryGetValue(pluginGuid, out ModHealthReport value)) { value = new ModHealthReport { PluginGuid = pluginGuid }; ReportsByPluginGuid[pluginGuid] = value; } update(value); value.ReportedUtc = DateTime.UtcNow; } } public static ModHealthReport GetReport(string pluginGuid) { if (string.IsNullOrWhiteSpace(pluginGuid)) { return null; } lock (Lock) { ModHealthReport value; return ReportsByPluginGuid.TryGetValue(pluginGuid, out value) ? value.Clone() : null; } } public static IReadOnlyList GetAllReports() { lock (Lock) { return ReportsByPluginGuid.Values.Select((ModHealthReport r) => r.Clone()).ToList(); } } public static string FormatHealthLogLine(ModHealthReport report) { if (report == null) { return "[Health] (empty report)"; } string text = (string.IsNullOrEmpty(report.DisplayName) ? report.PluginGuid : report.DisplayName); string text2 = (string.IsNullOrEmpty(report.PluginVersion) ? "?" : report.PluginVersion); string text3 = (string.IsNullOrEmpty(report.SharedCodeRevision) ? "?" : report.SharedCodeRevision); string text4 = (string.IsNullOrEmpty(report.IntegrationSummary) ? "—" : report.IntegrationSummary); string text5 = (string.IsNullOrEmpty(report.Mode) ? "—" : report.Mode); string text6 = (string.IsNullOrEmpty(report.CharacterName) ? "—" : report.CharacterName); string text7 = ((!report.DataLoaded.HasValue) ? "—" : (report.DataLoaded.Value ? "loaded" : "none")); string text8 = (string.IsNullOrEmpty(report.LastPersistenceOutcome) ? "—" : report.LastPersistenceOutcome); return "[Health] " + text + " v" + text2 + " | shared " + text3 + " | integrations: " + text4 + " | mode: " + text5 + " | character: " + text6 + " | data: " + text7 + " | save: " + text8; } public static void LogStartupHealth(ManualLogSource log, ModHealthReport report) { ReportStartup(report); if (log != null) { log.LogInfo((object)FormatHealthLogLine(report)); } } public static void LogModStartup(ManualLogSource log, string pluginGuid, string displayName, string pluginVersion, string integrationSummary, string mode = "startup", bool? dataLoaded = false, string lastPersistenceOutcome = "—") { LogStartupHealth(log, new ModHealthReport { PluginGuid = pluginGuid, DisplayName = displayName, PluginVersion = pluginVersion, SharedCodeRevision = "2026.07.28", IntegrationSummary = integrationSummary, Mode = mode, DataLoaded = dataLoaded, LastPersistenceOutcome = lastPersistenceOutcome }); } } public static class SuitePluginGuids { public const string DevTools = "com.azraelgodking.havendevtools"; public const string SenpaisChest = "com.azraelgodking.senpaischest"; public const string BirthdayReminder = "com.azraelgodking.squirrelsbirthdayreminder"; public const string HavensBirthright = "com.azraelgodking.havensbirthright"; public const string Smut = "com.azraelgodking.sunhavenmuseumutilitytracker"; public const string SunhavenTodo = "com.azraelgodking.sunhaventodo"; public const string TheVault = "com.azraelgodking.thevault"; public const string HavensAlmanac = "com.azraelgodking.havensalmanac"; public const string FasterRaces = "com.azraelgodking.fasterraces"; public const string TrinketFortune = "com.azraelgodking.trinketfortune"; public const string CropOptimizer = "com.azraelgodking.cropoptimizer"; public const string HavensRespec = "com.azraelgodking.havensrespec"; public const string GiftingAssistant = "com.azraelgodking.giftingassistant"; } public static class ModHealthIntegrationSummary { public static string Build(params (string label, string pluginGuid)[] integrations) { if (integrations == null || integrations.Length == 0) { return "standalone"; } Dictionary pluginInfos = Chainloader.PluginInfos; if (pluginInfos == null) { return "standalone"; } List list = new List(integrations.Length); for (int i = 0; i < integrations.Length; i++) { var (text, text2) = integrations[i]; if (!string.IsNullOrEmpty(text) && !string.IsNullOrEmpty(text2) && pluginInfos.ContainsKey(text2)) { list.Add(text); } } if (list.Count != 0) { return string.Join(", ", list); } return "standalone"; } } } namespace FasterRaces { [BepInPlugin("com.azraelgodking.fasterraces", "Faster Races", "2.1.1")] public class Plugin : BaseUnityPlugin { public static class SpeedPatch { private static PropertyInfo _subRaceProp; private static bool _subRaceChecked; private static bool _loggedRaceResolveFailure; [HarmonyPriority(300)] public static void Postfix(StatType stat, ref float __result) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Invalid comparison between Unknown and I4 if ((int)stat == 6 && EnableMod != null && EnableMod.Value) { float num = Mathf.Clamp(GetEffectiveSpeedBonus(GetCurrentRaceName()), 0f, 300f); if (!(num <= 0f)) { __result *= 1f + num / 100f; } } } internal static string GetCurrentRaceName() { try { Player instance = Player.Instance; if ((Object)(object)instance == (Object)null) { return null; } if (!_subRaceChecked) { _subRaceChecked = true; _subRaceProp = ((object)instance).GetType().GetProperty("SubRace", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } return (_subRaceProp?.GetValue(instance))?.ToString(); } catch (Exception ex) { if (!_loggedRaceResolveFailure) { _loggedRaceResolveFailure = true; ManualLogSource log = Log; if (log != null) { log.LogDebug((object)("[FasterRaces] GetCurrentRaceName failed: " + ex.Message)); } } return null; } } } internal static ConfigEntry EnableMod; internal static ConfigEntry SpeedBonusPercent; private const float MinSpeedBonusPercent = 0f; private const float MaxSpeedBonusPercent = 300f; internal static readonly Dictionary> RaceSpeedOverrides = new Dictionary>(); private static readonly string[] KnownRaces = new string[8] { "Human", "Elf", "Angel", "Demon", "FireElemental", "WaterElemental", "MagmaElemental", "Shade" }; private Harmony _harmony; private bool _applicationQuitting; public static ManualLogSource Log { get; private set; } public static bool IsSpeedBonusActive { get { if (EnableMod == null || !EnableMod.Value) { return false; } string currentRaceName = SpeedPatch.GetCurrentRaceName(); if (!string.IsNullOrEmpty(currentRaceName)) { return GetEffectiveSpeedBonus(currentRaceName) > 0f; } if (SpeedBonusPercent != null && SpeedBonusPercent.Value > 0f) { return true; } foreach (ConfigEntry value in RaceSpeedOverrides.Values) { if (value.Value > 0f) { return true; } } return false; } } private void Awake() { //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Expected O, but got Unknown //IL_00c9: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Expected O, but got Unknown //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_0100: Expected O, but got Unknown //IL_015b: Unknown result type (might be due to invalid IL or missing references) //IL_0168: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; ConfigFile val = CreateNamedConfig(); ConfigFileHelper.ReplacePluginConfig((BaseUnityPlugin)(object)this, val, (Action)Log.LogWarning); EnableMod = val.Bind("General", "Enabled", true, "Enable Faster Races movement speed bonus. When enabled, Haven's Birthright will not apply its own movement speed bonuses to avoid double speed."); SpeedBonusPercent = val.Bind("General", "SpeedBonusPercent", 25f, new ConfigDescription("Global percentage bonus to movement speed (e.g. 25 = +25%). Used for any race that does not have a per-race override set.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 300f), Array.Empty())); string[] knownRaces = KnownRaces; foreach (string text in knownRaces) { ConfigEntry value = val.Bind("PerRace", text + "SpeedBonusPercent", -1f, new ConfigDescription("Speed bonus % for " + text + ". Set to -1 to use the global SpeedBonusPercent.", (AcceptableValueBase)(object)new AcceptableValueRange(-1f, 300f), Array.Empty())); RaceSpeedOverrides[text] = value; } _harmony = new Harmony("com.azraelgodking.fasterraces"); MethodInfo methodInfo = AccessTools.Method(typeof(Player), "GetStat", new Type[1] { typeof(StatType) }, (Type[])null); MethodInfo methodInfo2 = AccessTools.Method(typeof(SpeedPatch), "Postfix", (Type[])null, (Type[])null); if (methodInfo != null && methodInfo2 != null) { _harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Log.LogInfo((object)"Patched Player.GetStat for movement speed"); } else { if (methodInfo == null) { Log.LogWarning((object)"[Reflect] Player.GetStat(StatType) not found - speed bonus inactive. Check game version."); } if (methodInfo2 == null) { Log.LogWarning((object)"[Reflect] SpeedPatch.Postfix not found - internal error."); } } Log.LogInfo((object)string.Format("{0} v{1} loaded. Global speed bonus: {2}%", "Faster Races", "2.1.1", SpeedBonusPercent.Value)); ModDiagnostics.LogModStartup(Log, "com.azraelgodking.fasterraces", "Faster Races", "2.1.1", ModHealthIntegrationSummary.Build(("DevTools", "com.azraelgodking.havendevtools"), ("Birthright", "com.azraelgodking.havensbirthright")), EnableMod.Value ? "startup" : "disabled", false); } internal static string NormalizeRaceName(string rawRaceName) { if (string.IsNullOrEmpty(rawRaceName)) { return rawRaceName; } if (RaceSpeedOverrides.ContainsKey(rawRaceName)) { return rawRaceName; } string text = rawRaceName.Replace(" ", ""); if (text != null) { int length = text.Length; if (length <= 5) { if (length != 4) { if (length == 5) { char c = text[0]; if (c != 'M') { if (c == 'W' && text == "Water") { goto IL_00f9; } } else if (text == "Magma") { goto IL_00ff; } } } else if (text == "Fire") { goto IL_00f3; } } else if (length != 11) { if (length == 12) { char c = text[0]; if (c != 'M') { if (c != 'S') { if (c == 'W' && text == "WaterElement") { goto IL_00f9; } } else if (text == "ShadeElement") { return "Shade"; } } else if (text == "MagmaElement") { goto IL_00ff; } } } else if (text == "FireElement") { goto IL_00f3; } } return rawRaceName; IL_00f3: return "FireElemental"; IL_00ff: return "MagmaElemental"; IL_00f9: return "WaterElemental"; } internal static float GetEffectiveSpeedBonus(string raceName) { raceName = NormalizeRaceName(raceName); if (!string.IsNullOrEmpty(raceName) && RaceSpeedOverrides.TryGetValue(raceName, out ConfigEntry value) && value.Value >= 0f) { return value.Value; } return SpeedBonusPercent?.Value ?? 0f; } 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, "FasterRaces.cfg"); string text2 = Path.Combine(Paths.ConfigPath, "com.azraelgodking.fasterraces.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 FasterRaces.cfg failed: " + ex.Message)); } } return new ConfigFile(text, true); } private void OnApplicationQuit() { _applicationQuitting = true; } private void OnDestroy() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) Scene activeScene = SceneManager.GetActiveScene(); string text = ((Scene)(ref activeScene)).name ?? string.Empty; string text2 = text.ToLowerInvariant(); if (_applicationQuitting || !Application.isPlaying || text2.Contains("menu") || text2.Contains("title")) { ManualLogSource log = Log; if (log != null) { log.LogInfo((object)("[Lifecycle] Plugin OnDestroy during expected teardown (scene: " + text + ")")); } } else { ManualLogSource log2 = Log; if (log2 != null) { log2.LogWarning((object)("[Lifecycle] Plugin OnDestroy outside expected teardown (scene: " + text + ")")); } } Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } public static class PluginInfo { public const string PLUGIN_GUID = "com.azraelgodking.fasterraces"; public const string PLUGIN_NAME = "Faster Races"; public const string PLUGIN_VERSION = "2.1.1"; } }