Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of AchievementEnabler v0.2.0
patchers/AchievementEnabler/AchievementEnabler.Preloader.dll
Decompiled 7 hours agousing System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Collections.Generic; [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("AchievementEnabler.Preloader")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("BepInEx patcher that rewrites Valheim's modded-flag reads so achievement gating sees an unmodded game.")] [assembly: AssemblyFileVersion("0.2.0.0")] [assembly: AssemblyInformationalVersion("0.2.0+ef9107718d26a9f886780340b03d86579d13929a")] [assembly: AssemblyProduct("AchievementEnabler.Preloader")] [assembly: AssemblyTitle("AchievementEnabler.Preloader")] [assembly: AssemblyVersion("0.2.0.0")] namespace AchievementEnabler.Preloader; internal sealed class RewriteResult { internal List<string> SummaryLines = new List<string>(); } internal static class FieldReadRewriter { internal static RewriteResult Rewrite(ModuleDefinition module, PatcherConfig config, ManualLogSource log) { //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Unknown result type (might be due to invalid IL or missing references) //IL_0140: Unknown result type (might be due to invalid IL or missing references) //IL_0145: Unknown result type (might be due to invalid IL or missing references) //IL_01a4: Unknown result type (might be due to invalid IL or missing references) //IL_01a9: Unknown result type (might be due to invalid IL or missing references) //IL_0120: Unknown result type (might be due to invalid IL or missing references) //IL_0119: Unknown result type (might be due to invalid IL or missing references) //IL_01db: Unknown result type (might be due to invalid IL or missing references) //IL_01b7: Unknown result type (might be due to invalid IL or missing references) //IL_01bc: Unknown result type (might be due to invalid IL or missing references) //IL_0167: Unknown result type (might be due to invalid IL or missing references) //IL_0200: Unknown result type (might be due to invalid IL or missing references) //IL_018a: Unknown result type (might be due to invalid IL or missing references) //IL_0183: Unknown result type (might be due to invalid IL or missing references) RewriteResult rewriteResult = new RewriteResult(); if (config.Fields.Count == 0) { log.LogWarning((object)"No fields configured; nothing to do."); rewriteResult.SummaryLines.Add("no fields configured"); return rewriteResult; } int num = 0; int num2 = 0; int num3 = 0; List<string> list = new List<string>(); foreach (TypeDefinition type in module.GetTypes()) { Enumerator<MethodDefinition> enumerator2 = type.Methods.GetEnumerator(); try { while (enumerator2.MoveNext()) { MethodDefinition current = enumerator2.Current; if (!current.HasBody) { continue; } bool flag = false; bool flag2 = IsAllowlisted(current, config.Allowlist); ILProcessor val = null; Instruction[] array = (Instruction[])(object)new Instruction[current.Body.Instructions.Count]; current.Body.Instructions.CopyTo(array, 0); Instruction[] array2 = array; foreach (Instruction val2 in array2) { object operand = val2.Operand; FieldReference val3 = (FieldReference)((operand is FieldReference) ? operand : null); if (val3 != null && config.Fields.Contains(Key(val3))) { num++; if (val2.OpCode == OpCodes.Ldsfld) { val2.OpCode = (flag2 ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0); val2.Operand = null; num2++; flag = true; } else if (val2.OpCode == OpCodes.Ldfld) { val = val ?? current.Body.GetILProcessor(); val2.OpCode = OpCodes.Pop; val2.Operand = null; val.InsertAfter(val2, val.Create(flag2 ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0)); num2++; flag = true; } else if (val2.OpCode == OpCodes.Ldsflda || val2.OpCode == OpCodes.Ldflda) { num3++; log.LogInfo((object)$" skipped {Describe(current)} - {val2.OpCode} (address-of cannot be folded)"); } else { log.LogDebug((object)$" ignored {Describe(current)} - {val2.OpCode}"); } } } if (flag) { list.Add(" " + Describe(current) + " -> " + (flag2 ? "true" : "false")); } } } finally { ((IDisposable)enumerator2/*cast due to .constrained prefix*/).Dispose(); } } string text = string.Join(", ", ToArray(config.Fields)); log.LogInfo((object)$"{num} read(s) of [{text}] across the module; {num2} folded, {num3} skipped."); foreach (string item in list) { log.LogInfo((object)item); } rewriteResult.SummaryLines.Add($"{num} read(s) of [{text}]: {num2} folded, {num3} skipped (address-of)"); rewriteResult.SummaryLines.AddRange(list); return rewriteResult; } private static string Key(FieldReference field) { return ((MemberReference)((MemberReference)field).DeclaringType).FullName + "::" + ((MemberReference)field).Name; } private static string Describe(MethodDefinition method) { return ((MemberReference)method.DeclaringType).FullName + "::" + ((MemberReference)method).Name; } private static bool IsAllowlisted(MethodDefinition method, HashSet<string> allowlist) { if (allowlist.Count == 0) { return false; } if (allowlist.Contains(Describe(method))) { return true; } TypeDefinition declaringType = method.DeclaringType; if (declaringType.DeclaringType == null) { return false; } string fullName = ((MemberReference)declaringType.DeclaringType).FullName; foreach (string item in allowlist) { int num = item.IndexOf("::", StringComparison.Ordinal); if (num > 0 && string.Equals(item.Substring(0, num), fullName, StringComparison.Ordinal)) { string text = item.Substring(num + 2); if (((MemberReference)method).Name.IndexOf("<" + text + ">", StringComparison.Ordinal) >= 0) { return true; } } } return false; } private static string[] ToArray(HashSet<string> set) { string[] array = new string[set.Count]; set.CopyTo(array); return array; } } public static class ModdedFlagPatcher { internal const string PatcherName = "AchievementEnabler.Preloader"; internal const string SummaryKey = "AchievementEnabler.Preloader.Summary.v1"; internal static readonly ManualLogSource Log = Logger.CreateLogSource("AchievementEnabler.Preloader"); public static IEnumerable<string> TargetDLLs => new string[1] { "assembly_valheim.dll" }; public static void Patch(AssemblyDefinition assembly) { List<string> list = new List<string>(); try { PatcherConfig patcherConfig = PatcherConfig.Load(); if (!patcherConfig.Enabled) { Log.LogInfo((object)"Disabled in config; assembly_valheim left untouched."); list.Add("disabled in AchievementEnabler.Preloader.cfg"); return; } if (patcherConfig.DumpSymbols) { SymbolDump.Run(assembly.MainModule, patcherConfig, Log); } RewriteResult rewriteResult = FieldReadRewriter.Rewrite(assembly.MainModule, patcherConfig, Log); list.AddRange(rewriteResult.SummaryLines); } catch (Exception ex) { Log.LogError((object)("Patching failed, leaving assembly_valheim untouched: " + ex)); list.Add("FAILED: " + ex.Message); } finally { string[] lines = list.ToArray(); AppDomain.CurrentDomain.SetData("AchievementEnabler.Preloader.Summary.v1", (Func<string[]>)(() => lines)); } } } internal sealed class PatcherConfig { internal const string FileName = "AchievementEnabler.Preloader.cfg"; internal bool Enabled; internal HashSet<string> Fields; internal HashSet<string> Allowlist; internal bool DumpSymbols; internal string[] Keywords; internal static PatcherConfig Load() { //IL_0010: 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_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) ConfigFile val = new ConfigFile(Path.Combine(Paths.ConfigPath, "AchievementEnabler.Preloader.cfg"), true); ConfigEntry<bool> val2 = val.Bind<bool>("General", "Enabled", true, "Master switch for the IL rewrite. Turn this off to run the game with the patcher installed but inert."); ConfigEntry<string> val3 = val.Bind<string>("Spoof", "Fields", "Game::isModded", "Comma-separated bool fields, written as Type::field, whose reads are folded to a constant.\nType is Cecil's full name, so a type in the global namespace is bare (Game::isModded) and a\nnamespaced one is qualified (Some.Namespace.Type::field)."); ConfigEntry<string> val4 = val.Bind<string>("Spoof", "Allowlist", "FejdStartup::SetupGui, Game::Awake", "Comma-separated methods, written as Type::Method, that keep seeing TRUE. Every other reader\nsees FALSE. These are the places that only *report* the flag rather than gate on it - the\nmain-menu 'modded' label and the startup log line - so the game stays honest about being modded."); ConfigEntry<bool> val5 = val.Bind<bool>("Diagnostics", "DumpSymbols", false, "Log every type, method and field in assembly_valheim whose name matches Keywords, plus every\nmethod that reads one of the Fields above. Use this after a game update to find what the new\nachievement code actually checks."); ConfigEntry<string> val6 = val.Bind<string>("Diagnostics", "Keywords", "achiev,unlock,steamuserstats,trophy,milestone", "Comma-separated, case-insensitive substrings that DumpSymbols matches names against."); return new PatcherConfig { Enabled = val2.Value, Fields = ToSet(val3.Value), Allowlist = ToSet(val4.Value), DumpSymbols = val5.Value, Keywords = ToArray(val6.Value) }; } private static HashSet<string> ToSet(string csv) { HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); string[] array = ToArray(csv); foreach (string item in array) { hashSet.Add(item); } return hashSet; } private static string[] ToArray(string csv) { if (string.IsNullOrEmpty(csv)) { return new string[0]; } string[] array = csv.Split(new char[1] { ',' }); List<string> list = new List<string>(array.Length); string[] array2 = array; for (int i = 0; i < array2.Length; i++) { string text = array2[i].Trim(); if (text.Length > 0) { list.Add(text); } } return list.ToArray(); } } internal static class SymbolDump { private const int MaxHitsPerSection = 200; internal static void Run(ModuleDefinition module, PatcherConfig config, ManualLogSource log) { //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_0149: Unknown result type (might be due to invalid IL or missing references) //IL_014e: Unknown result type (might be due to invalid IL or missing references) //IL_01dc: Unknown result type (might be due to invalid IL or missing references) //IL_01e1: Unknown result type (might be due to invalid IL or missing references) if (config.Keywords.Length == 0) { log.LogWarning((object)"DumpSymbols is on but Keywords is empty; nothing to look for."); return; } log.LogInfo((object)("=== symbol dump: " + ((ModuleReference)module).Name + ", keywords [" + string.Join(", ", config.Keywords) + "] ===")); List<string> list = new List<string>(); List<string> list2 = new List<string>(); List<string> list3 = new List<string>(); foreach (TypeDefinition type in module.GetTypes()) { if (Matches(((MemberReference)type).Name, config.Keywords)) { list.Add(" type " + ((MemberReference)type).FullName); } Enumerator<FieldDefinition> enumerator2 = type.Fields.GetEnumerator(); try { while (enumerator2.MoveNext()) { FieldDefinition current2 = enumerator2.Current; if (Matches(((MemberReference)current2).Name, config.Keywords)) { list2.Add(" field " + ((MemberReference)type).FullName + "::" + ((MemberReference)current2).Name + " : " + ((MemberReference)((FieldReference)current2).FieldType).Name); } } } finally { ((IDisposable)enumerator2/*cast due to .constrained prefix*/).Dispose(); } Enumerator<MethodDefinition> enumerator3 = type.Methods.GetEnumerator(); try { while (enumerator3.MoveNext()) { MethodDefinition current3 = enumerator3.Current; if (Matches(((MemberReference)current3).Name, config.Keywords)) { list2.Add(" method " + ((MemberReference)type).FullName + "::" + ((MemberReference)current3).Name + " : " + ((MemberReference)((MethodReference)current3).ReturnType).Name); } if (!current3.HasBody) { continue; } bool flag = false; bool flag2 = false; Enumerator<Instruction> enumerator4 = current3.Body.Instructions.GetEnumerator(); try { while (enumerator4.MoveNext()) { Instruction current4 = enumerator4.Current; object operand = current4.Operand; FieldReference val = (FieldReference)((operand is FieldReference) ? operand : null); if (val != null && config.Fields.Contains(((MemberReference)((MemberReference)val).DeclaringType).FullName + "::" + ((MemberReference)val).Name)) { flag = true; continue; } object operand2 = current4.Operand; MethodReference val2 = (MethodReference)((operand2 is MethodReference) ? operand2 : null); if (val2 != null && (Matches(((MemberReference)val2).Name, config.Keywords) || Matches(((MemberReference)((MemberReference)val2).DeclaringType).Name, config.Keywords))) { flag2 = true; } } } finally { ((IDisposable)enumerator4/*cast due to .constrained prefix*/).Dispose(); } if (flag) { list3.Add(" reads " + ((MemberReference)type).FullName + "::" + ((MemberReference)current3).Name); } if (flag2) { list2.Add(" calls " + ((MemberReference)type).FullName + "::" + ((MemberReference)current3).Name + " -> keyword-matching member"); } } } finally { ((IDisposable)enumerator3/*cast due to .constrained prefix*/).Dispose(); } } Report(log, "types matching keywords", list); Report(log, "members matching keywords", list2); Report(log, "methods reading a targeted field", list3); log.LogInfo((object)"=== end symbol dump ==="); } private static void Report(ManualLogSource log, string heading, List<string> hits) { if (hits.Count == 0) { log.LogInfo((object)("-- " + heading + ": none")); return; } log.LogInfo((object)$"-- {heading}: {hits.Count}"); for (int i = 0; i < hits.Count && i < 200; i++) { log.LogInfo((object)hits[i]); } if (hits.Count > 200) { log.LogInfo((object)$" ... {hits.Count - 200} more (narrow Keywords to see them)"); } } private static bool Matches(string name, string[] keywords) { foreach (string value in keywords) { if (name.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0) { return true; } } return false; } }
plugins/AchievementEnabler.dll
Decompiled 7 hours agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("AchievementEnabler")] [assembly: AssemblyDescription("Lets a modded Valheim earn achievements. The modded flag stays true; the checks that read it see false.")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("MidnightMods")] [assembly: AssemblyProduct("AchievementEnabler")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("6f2a1c74-58d0-4a3e-9b21-7c4e0d8f5a13")] [assembly: AssemblyFileVersion("0.2.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyVersion("0.2.0.0")] namespace AchievementEnabler; [BepInPlugin("MidnightsFX.AchievementEnabler", "AchievementEnabler", "0.2.0")] public class AchievementEnabler : BaseUnityPlugin { public const string PluginGUID = "MidnightsFX.AchievementEnabler"; public const string PluginName = "AchievementEnabler"; public const string PluginVersion = "0.2.0"; internal static ManualLogSource Log; private void Awake() { Log = ((BaseUnityPlugin)this).Logger; ValConfig.Init(((BaseUnityPlugin)this).Config); if (!ValConfig.Enabled.Value) { Logger.LogInfo("Disabled in config; nothing patched."); return; } ModdedFlag.Resolve(ValConfig.FlagMember.Value); if (ValConfig.KeepFlagTrue.Value && ModdedFlag.Available) { ModdedFlag.Value = true; } Logger.LogInfo($"Modded flag: {ModdedFlag.Description} = {ModdedFlag.Value}"); PatcherReport.LogSummary(); int num = ScopedFlagSwap.Apply(ValConfig.Split(ValConfig.ScopedFalseMethods.Value)); int num2 = MethodOverrides.Apply(ValConfig.Split(ValConfig.ForceFalseMethods.Value)); if (num == 0 && num2 == 0) { Logger.LogWarning("No gate methods are in play. Since Valheim 1.0 there is a real one - Achievements::IsCheatedAtAll - so unless the preloader patcher above reported folding the flag read, achievements are still blocked. Restore that value in ScopedFalseMethods, or if the game has since renamed it, set DumpSymbols in the patcher config, restart, and put what the symbol dump finds there instead."); } else { Logger.LogInfo($"Ready: {num} scoped swap(s), {num2} forced result(s)."); } } } internal class Logger { public static LogLevel Level = (LogLevel)16; public static void EnableDebugLogging(object sender, EventArgs e) { CheckEnableDebugLogging(); } public static void CheckEnableDebugLogging() { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) if (ValConfig.EnableDebugMode.Value) { Level = (LogLevel)32; } else { Level = (LogLevel)16; } } public static void SetDebugLogging(bool state) { //IL_000d: 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) if (state) { Level = (LogLevel)32; } else { Level = (LogLevel)16; } } public static void LogDebug(string message) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Invalid comparison between Unknown and I4 if ((int)Level >= 32) { AchievementEnabler.Log.LogInfo((object)("[DEBUG]" + message)); } } public static void LogInfo(string message) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Invalid comparison between Unknown and I4 if ((int)Level >= 16) { AchievementEnabler.Log.LogInfo((object)message); } } public static void LogWarning(string message) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Invalid comparison between Unknown and I4 if ((int)Level >= 4) { AchievementEnabler.Log.LogWarning((object)message); } } public static void LogError(string message) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Invalid comparison between Unknown and I4 if ((int)Level >= 2) { AchievementEnabler.Log.LogError((object)message); } } } internal class ValConfig { internal const string DefaultScopedFalseMethods = "Achievements::IsCheatedAtAll"; private static ConfigFile cfg; public static ConfigEntry<bool> Enabled; public static ConfigEntry<bool> KeepFlagTrue; public static ConfigEntry<string> FlagMember; public static ConfigEntry<bool> EnableDebugMode; public static ConfigEntry<string> ScopedFalseMethods; public static ConfigEntry<string> ForceFalseMethods; public static void Init(ConfigFile config) { cfg = config; cfg.SaveOnConfigSet = false; Enabled = cfg.Bind<bool>("General", "Enabled", true, "Master switch. Off means no patching at all; the flag is left exactly as other mods set it."); KeepFlagTrue = cfg.Bind<bool>("General", "KeepFlagTrue", true, "Set the modded flag to true at startup, so the main menu shows the 'modded' label and the\nstartup log stays honest. Leave this on: the whole point is that the flag is true and only\nthe gates are lied to. Mod loaders like Jotunn already set it, but not every setup has one."); FlagMember = cfg.Bind<string>("General", "FlagMember", "Game::isModded", "Where the modded flag lives, as Type::member. Resolved by name at runtime, so it copes with\nthe flag being a field or a property. Change this only if the game renames or moves it."); EnableDebugMode = cfg.Bind<bool>("General", "EnableDebugMode", false, "Verbose logging."); ScopedFalseMethods = cfg.Bind<string>("Spoof", "ScopedFalseMethods", "Achievements::IsCheatedAtAll", "Comma-separated methods, as Type::Method, that should see the modded flag as FALSE.\nThe flag is set false for the duration of each call and restored afterwards, so it stays true\neverywhere else. This is the main mechanism.\nThe default is Valheim 1.0's achievement gate. Achievements::IsCheatedAtAll is the only place\nin the game that reads the modded flag to decide anything, and everything achievement-related\nreaches it through Achievements::CanGetAchievements. Swapping the flag just for that call\nleaves the real cheat checks it also performs - console cheats, cheated items, cheated world\nmodifiers - completely intact, so cheating still costs you achievements.\nClear this to turn the scoped swap off."); ForceFalseMethods = cfg.Bind<string>("Spoof", "ForceFalseMethods", "", "Comma-separated bool-returning methods, as Type::Method, whose result is forced to false.\nUse this when the gate is a method rather than a flag read - e.g. a Game::IsModded() helper.\nEmpty by default: Valheim 1.0's gate is a flag read, which the scoped swap above covers."); cfg.Save(); cfg.SaveOnConfigSet = true; Logger.CheckEnableDebugLogging(); EnableDebugMode.SettingChanged += Logger.EnableDebugLogging; } public static string[] Split(string csv) { if (string.IsNullOrEmpty(csv)) { return new string[0]; } List<string> list = new List<string>(); string[] array = csv.Split(new char[1] { ',' }); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length > 0) { list.Add(text); } } return list.ToArray(); } } internal static class MethodOverrides { private static readonly Harmony Harmony = new Harmony("MidnightsFX.AchievementEnabler.overrides"); internal static int Apply(string[] entries) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Expected O, but got Unknown if (entries.Length == 0) { return 0; } HarmonyMethod val = new HarmonyMethod(AccessTools.Method(typeof(MethodOverrides), "ForceFalse", (Type[])null, (Type[])null)); int num = 0; foreach (string text in entries) { MethodBase methodBase = MemberRef.ResolveMethod(text); if (methodBase == null) { continue; } MethodInfo methodInfo = methodBase as MethodInfo; if (methodInfo == null || methodInfo.ReturnType != typeof(bool)) { Logger.LogWarning("'" + text + "' does not return bool; skipped. ForceFalseMethods only applies to bool methods."); continue; } try { Harmony.Patch(methodBase, (HarmonyMethod)null, val, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Logger.LogInfo("Forcing " + text + " to return false."); num++; } catch (Exception ex) { Logger.LogWarning("Could not patch '" + text + "': " + ex.Message); } } return num; } private static void ForceFalse(ref bool __result) { __result = false; } } internal static class ModdedFlag { private static FieldInfo _field; private static PropertyInfo _property; private static string _description = "unresolved"; internal static bool Available { get { if (!(_field != null)) { return _property != null; } return true; } } internal static string Description => _description; internal static bool Value { get { try { if (_field != null) { return (bool)_field.GetValue(null); } if (_property != null) { return (bool)_property.GetValue(null, null); } } catch (Exception ex) { Logger.LogWarning("Could not read the modded flag: " + ex.Message); } return false; } set { try { if (_field != null) { _field.SetValue(null, value); } else if (_property != null) { _property.SetValue(null, value, null); } } catch (Exception ex) { Logger.LogWarning("Could not write the modded flag: " + ex.Message); } } } internal static void Resolve(string member) { _field = null; _property = null; _description = "unresolved"; if (!MemberRef.TryParse(member, out var typeName, out var memberName)) { Logger.LogWarning("FlagMember '" + member + "' is not in Type::member form; the modded flag will not be touched."); return; } Type type = AccessTools.TypeByName(typeName); if (type == null) { Logger.LogWarning("Type '" + typeName + "' not found; the modded flag will not be touched."); return; } _field = AccessTools.Field(type, memberName); if (_field != null && _field.FieldType == typeof(bool)) { _description = "field " + type.FullName + "::" + memberName; return; } _field = null; _property = AccessTools.Property(type, memberName); if (_property != null && _property.PropertyType == typeof(bool) && _property.CanRead && _property.CanWrite) { _description = "property " + type.FullName + "::" + memberName; return; } _property = null; Logger.LogWarning("'" + member + "' resolved to no writable bool field or property. If the game renamed it, set General.FlagMember in the config; DumpSymbols in the patcher config will tell you the new name."); } } internal static class MemberRef { internal static bool TryParse(string entry, out string typeName, out string memberName) { typeName = null; memberName = null; if (string.IsNullOrEmpty(entry)) { return false; } int num = entry.IndexOf("::", StringComparison.Ordinal); if (num <= 0 || num + 2 >= entry.Length) { return false; } typeName = entry.Substring(0, num).Trim().Replace('/', '+'); memberName = entry.Substring(num + 2).Trim(); if (typeName.Length > 0) { return memberName.Length > 0; } return false; } internal static MethodBase ResolveMethod(string entry) { if (!TryParse(entry, out var typeName, out var memberName)) { Logger.LogWarning("'" + entry + "' is not in Type::Method form; skipped."); return null; } Type type = AccessTools.TypeByName(typeName); if (type == null) { Logger.LogWarning("'" + entry + "': type '" + typeName + "' not found; skipped."); return null; } int num = 0; foreach (MethodInfo declaredMethod in AccessTools.GetDeclaredMethods(type)) { if (declaredMethod.Name == memberName) { num++; } } if (num > 1) { Logger.LogWarning($"'{entry}' has {num} overloads; patching the first one AccessTools returns. " + "If that is the wrong one, this shorthand cannot say which - open an issue."); } MethodBase methodBase = AccessTools.Method(type, memberName, (Type[])null, (Type[])null); if (methodBase == null) { Logger.LogWarning("'" + entry + "': method '" + memberName + "' not found on " + type.FullName + "; skipped."); return null; } if (methodBase.IsAbstract || methodBase.ContainsGenericParameters || methodBase.GetMethodBody() == null) { Logger.LogWarning("'" + entry + "' has no patchable body (abstract, generic or extern); skipped."); return null; } return methodBase; } } internal static class PatcherReport { private const string SummaryKey = "AchievementEnabler.Preloader.Summary.v1"; internal static void LogSummary() { if (!(AppDomain.CurrentDomain.GetData("AchievementEnabler.Preloader.Summary.v1") is Func<string[]> func)) { Logger.LogInfo("Preloader patcher not present. The scoped swap below is the only mechanism in play, which is fine unless you need a flag read that no method wraps."); return; } string[] array; try { array = func(); } catch (Exception ex) { Logger.LogWarning("Preloader patcher summary could not be read: " + ex.Message); return; } if (array == null || array.Length == 0) { Logger.LogInfo("Preloader patcher ran but reported nothing."); return; } Logger.LogInfo("Preloader patcher:"); string[] array2 = array; foreach (string text in array2) { Logger.LogInfo(" " + text); } } } internal static class ScopedFlagSwap { private static readonly Harmony Harmony = new Harmony("MidnightsFX.AchievementEnabler.scoped"); internal static int Apply(string[] entries) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Expected O, but got Unknown //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Expected O, but got Unknown if (entries.Length == 0) { return 0; } if (!ModdedFlag.Available) { Logger.LogWarning("ScopedFalseMethods is configured but the modded flag did not resolve; nothing to swap."); return 0; } HarmonyMethod val = new HarmonyMethod(AccessTools.Method(typeof(ScopedFlagSwap), "Prefix", (Type[])null, (Type[])null)); HarmonyMethod val2 = new HarmonyMethod(AccessTools.Method(typeof(ScopedFlagSwap), "Finalizer", (Type[])null, (Type[])null)); int num = 0; foreach (string text in entries) { MethodBase methodBase = MemberRef.ResolveMethod(text); if (!(methodBase == null)) { try { Harmony.Patch(methodBase, val, (HarmonyMethod)null, (HarmonyMethod)null, val2, (HarmonyMethod)null); Logger.LogInfo("Scoped swap on " + text + " - it will see the modded flag as false."); num++; } catch (Exception ex) { Logger.LogWarning("Could not patch '" + text + "': " + ex.Message); } } } return num; } private static void Prefix(out bool __state) { __state = ModdedFlag.Value; ModdedFlag.Value = false; } private static void Finalizer(bool __state) { ModdedFlag.Value = __state; } }