using 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 SummaryLines = new List(); } 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 list = new List(); foreach (TypeDefinition type in module.GetTypes()) { Enumerator 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 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 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 TargetDLLs => new string[1] { "assembly_valheim.dll" }; public static void Patch(AssemblyDefinition assembly) { List list = new List(); 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)(() => lines)); } } } internal sealed class PatcherConfig { internal const string FileName = "AchievementEnabler.Preloader.cfg"; internal bool Enabled; internal HashSet Fields; internal HashSet 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 val2 = val.Bind("General", "Enabled", true, "Master switch for the IL rewrite. Turn this off to run the game with the patcher installed but inert."); ConfigEntry val3 = val.Bind("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 val4 = val.Bind("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 val5 = val.Bind("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 val6 = val.Bind("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 ToSet(string csv) { HashSet hashSet = new HashSet(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 list = new List(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 list = new List(); List list2 = new List(); List list3 = new List(); foreach (TypeDefinition type in module.GetTypes()) { if (Matches(((MemberReference)type).Name, config.Keywords)) { list.Add(" type " + ((MemberReference)type).FullName); } Enumerator 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 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 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 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; } }