Decompiled source of ModlistGuard v0.1.0

plugins/ModlistGuard.dll

Decompiled 4 days ago
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.Security;
using System.Security.Permissions;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: IgnoresAccessChecksTo("assembly_utils")]
[assembly: IgnoresAccessChecksTo("assembly_valheim")]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyCompany("ModlistGuard")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.1.0.0")]
[assembly: AssemblyInformationalVersion("0.1.0")]
[assembly: AssemblyProduct("ModlistGuard")]
[assembly: AssemblyTitle("ModlistGuard")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.1.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace ModlistGuard
{
	[BepInPlugin("com.noodle.modlistguard", "ModlistGuard", "0.1.0")]
	internal class ModlistGuardPlugin : BaseUnityPlugin
	{
		public const string PluginGuid = "com.noodle.modlistguard";

		public const string PluginName = "ModlistGuard";

		public const string PluginVersion = "0.1.0";

		internal static ManualLogSource Log;

		internal static ConfigEntry<bool> IgnoreVersionChanges;

		internal static ConfigEntry<string> IgnoredMods;

		private void Awake()
		{
			//IL_0054: Unknown result type (might be due to invalid IL or missing references)
			Log = ((BaseUnityPlugin)this).Logger;
			IgnoreVersionChanges = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "IgnoreVersionChanges", false, "Only warn when a mod was added or removed, not when one was updated.");
			IgnoredMods = ((BaseUnityPlugin)this).Config.Bind<string>("General", "IgnoredMods", "", "Comma-separated plugin GUIDs that never count as a mismatch — client-only mods like minimap or UI tweaks that cannot affect a save.");
			new Harmony("com.noodle.modlistguard").PatchAll(typeof(WorldStartPatch));
		}
	}
	internal readonly struct ModEntry
	{
		public readonly string Guid;

		public readonly string Name;

		public readonly string Version;

		public ModEntry(string guid, string name, string version)
		{
			Guid = guid;
			Name = name;
			Version = version;
		}
	}
	internal static class ModlistStore
	{
		private static string Dir => Path.Combine(Utils.GetSaveDataPath((FileSource)2), "modlist_guard");

		private static string PathFor(World world)
		{
			return Path.Combine(Dir, world.m_uid + ".txt");
		}

		public static List<ModEntry> Current()
		{
			return (from p in Chainloader.PluginInfos.Values
				where p.Metadata.GUID != "com.noodle.modlistguard"
				select new ModEntry(p.Metadata.GUID, p.Metadata.Name, p.Metadata.Version.ToString())).OrderBy<ModEntry, string>((ModEntry m) => m.Guid, StringComparer.Ordinal).ToList();
		}

		public static List<ModEntry> Load(World world)
		{
			string text = PathFor(world);
			if (!File.Exists(text))
			{
				return null;
			}
			try
			{
				return (from line in File.ReadAllLines(text)
					select line.Split(new char[1] { '\t' }) into f
					where f.Length == 3
					select new ModEntry(f[0], f[2], f[1])).ToList();
			}
			catch (Exception ex)
			{
				ModlistGuardPlugin.Log.LogWarning((object)("Could not read " + text + ": " + ex.Message));
				return null;
			}
		}

		public static void Save(World world, List<ModEntry> mods)
		{
			try
			{
				Directory.CreateDirectory(Dir);
				File.WriteAllLines(PathFor(world), mods.Select((ModEntry m) => m.Guid + "\t" + m.Version + "\t" + m.Name));
			}
			catch (Exception ex)
			{
				ModlistGuardPlugin.Log.LogWarning((object)("Could not record mods for " + world.m_name + ": " + ex.Message));
			}
		}
	}
	internal class ModlistDiff
	{
		public readonly List<ModEntry> Missing = new List<ModEntry>();

		public readonly List<ModEntry> Added = new List<ModEntry>();

		public readonly List<(ModEntry From, ModEntry To)> Updated = new List<(ModEntry, ModEntry)>();

		private const int MaxNames = 10;

		public bool IsEmpty
		{
			get
			{
				if (Missing.Count == 0 && Added.Count == 0)
				{
					return Updated.Count == 0;
				}
				return false;
			}
		}

		public ModlistDiff(List<ModEntry> saved, List<ModEntry> current)
		{
			HashSet<string> ignored = new HashSet<string>(from s in ModlistGuardPlugin.IgnoredMods.Value.Split(new char[1] { ',' })
				select s.Trim() into s
				where s.Length > 0
				select s);
			Dictionary<string, ModEntry> before = saved.Where((ModEntry m) => !ignored.Contains(m.Guid)).ToDictionary((ModEntry m) => m.Guid);
			Dictionary<string, ModEntry> after = current.Where((ModEntry m) => !ignored.Contains(m.Guid)).ToDictionary((ModEntry m) => m.Guid);
			Missing.AddRange(before.Values.Where((ModEntry m) => !after.ContainsKey(m.Guid)));
			Added.AddRange(after.Values.Where((ModEntry m) => !before.ContainsKey(m.Guid)));
			if (ModlistGuardPlugin.IgnoreVersionChanges.Value)
			{
				return;
			}
			foreach (ModEntry value2 in after.Values)
			{
				if (before.TryGetValue(value2.Guid, out var value) && value.Version != value2.Version)
				{
					Updated.Add((value, value2));
				}
			}
		}

		public string Describe()
		{
			List<string> list = new List<string>();
			if (Missing.Count > 0)
			{
				list.Add("<color=#ff7070>Missing:</color> " + Join(Missing.Select((ModEntry m) => m.Name)));
			}
			if (Added.Count > 0)
			{
				list.Add("<color=#80e080>New:</color> " + Join(Added.Select((ModEntry m) => m.Name)));
			}
			if (Updated.Count > 0)
			{
				list.Add("<color=#f0d060>Updated:</color> " + Join(Updated.Select(((ModEntry From, ModEntry To) u) => u.To.Name + " " + u.From.Version + "→" + u.To.Version)));
			}
			return string.Join("\n", list);
		}

		private static string Join(IEnumerable<string> names)
		{
			List<string> list = names.ToList();
			string text = string.Join(", ", list.Take(10));
			if (list.Count <= 10)
			{
				return text;
			}
			return $"{text} and {list.Count - 10} more";
		}
	}
	[HarmonyPatch(typeof(FejdStartup), "OnWorldStart")]
	internal static class WorldStartPatch
	{
		[CompilerGenerated]
		private static class <>O
		{
			public static PopupButtonCallback <0>__Pop;
		}

		private static long? s_acknowledged;

		private static bool Prefix(FejdStartup __instance, out bool __state)
		{
			//IL_0037: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f4: Unknown result type (might be due to invalid IL or missing references)
			//IL_011b: Expected O, but got Unknown
			//IL_0116: Unknown result type (might be due to invalid IL or missing references)
			//IL_0120: Expected O, but got Unknown
			//IL_0109: Unknown result type (might be due to invalid IL or missing references)
			//IL_010e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0114: Expected O, but got Unknown
			__state = __instance.m_startingWorld;
			World world = __instance.m_world;
			if (world == null || __instance.m_startingWorld || (int)world.m_dataError != 0 || s_acknowledged == world.m_uid)
			{
				return true;
			}
			List<ModEntry> list = ModlistStore.Current();
			List<ModEntry> list2 = ModlistStore.Load(world);
			string text;
			if (list2 == null)
			{
				if (list.Count > 0)
				{
					return true;
				}
				text = "No mods are loaded, and ModlistGuard has no record of this world.\n\nIf it was played with mods, go back and launch through r2modman.\n\nLoad anyway?";
			}
			else
			{
				ModlistDiff modlistDiff = new ModlistDiff(list2, list);
				if (modlistDiff.IsEmpty)
				{
					return true;
				}
				text = "This world was last played with different mods.\n\n" + modlistDiff.Describe();
				if (modlistDiff.Missing.Count > 0)
				{
					text += "\n\nLoading without them permanently deletes what they added.";
				}
				text += "\n\nLoad anyway?";
			}
			long uid = world.m_uid;
			string text2 = text;
			PopupButtonCallback val = delegate
			{
				UnifiedPopup.Pop();
				s_acknowledged = uid;
				__instance.OnWorldStart();
			};
			object obj = <>O.<0>__Pop;
			if (obj == null)
			{
				PopupButtonCallback val2 = UnifiedPopup.Pop;
				<>O.<0>__Pop = val2;
				obj = (object)val2;
			}
			UnifiedPopup.Push((PopupBase)new YesNoPopup("Mods changed", text2, val, (PopupButtonCallback)obj, false, false));
			return false;
		}

		private static void Postfix(FejdStartup __instance, bool __state)
		{
			if (!__state && __instance.m_startingWorld && __instance.m_world != null)
			{
				ModlistStore.Save(__instance.m_world, ModlistStore.Current());
				s_acknowledged = null;
			}
		}
	}
}
namespace System.Runtime.CompilerServices
{
	[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
	internal sealed class IgnoresAccessChecksToAttribute : Attribute
	{
		public IgnoresAccessChecksToAttribute(string assemblyName)
		{
		}
	}
}