Decompiled source of ModSync v1.0.1

BepInEx\plugins\ModSync\ModSync.dll

Decompiled a month ago
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;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using ExitGames.Client.Photon;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using ModSync.Networking;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.7.1", FrameworkDisplayName = ".NET Framework 4.7.1")]
[assembly: AssemblyCompany("ModSync")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+fc6bc7fe0b23e6eb94f7536cdd35c7e2d3cd2055")]
[assembly: AssemblyProduct("ModSync")]
[assembly: AssemblyTitle("ModSync")]
[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 ModSync
{
	internal static class ConfigSync
	{
		private static readonly ManualLogSource Log = Plugin.Logger;

		internal static Dictionary<string, string> GetLocalConfigs()
		{
			Dictionary<string, string> dictionary = new Dictionary<string, string>();
			string configPath = Paths.ConfigPath;
			foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos)
			{
				if (pluginInfo.Key == "com.Jaz.modsync")
				{
					continue;
				}
				string path = Path.Combine(configPath, pluginInfo.Key + ".cfg");
				if (File.Exists(path))
				{
					try
					{
						dictionary[Path.GetFileName(path)] = File.ReadAllText(path);
					}
					catch (Exception ex)
					{
						Log.LogWarning((object)("[ConfigSync] Could not read config for " + pluginInfo.Key + ": " + ex.Message));
					}
				}
			}
			return dictionary;
		}

		internal static byte[] Serialize(Dictionary<string, string> configs)
		{
			using MemoryStream memoryStream = new MemoryStream();
			using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8);
			binaryWriter.Write(configs.Count);
			foreach (KeyValuePair<string, string> config in configs)
			{
				binaryWriter.Write(config.Key);
				binaryWriter.Write(config.Value);
			}
			return memoryStream.ToArray();
		}

		internal static Dictionary<string, string> Deserialize(byte[] data)
		{
			Dictionary<string, string> dictionary = new Dictionary<string, string>();
			using MemoryStream input = new MemoryStream(data);
			using BinaryReader binaryReader = new BinaryReader(input, Encoding.UTF8);
			int num = binaryReader.ReadInt32();
			for (int i = 0; i < num; i++)
			{
				string key = binaryReader.ReadString();
				string value = binaryReader.ReadString();
				dictionary[key] = value;
			}
			return dictionary;
		}

		internal static int ApplyInMemory(Dictionary<string, string> receivedConfigs)
		{
			int num = 0;
			foreach (KeyValuePair<string, string> receivedConfig in receivedConfigs)
			{
				string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(receivedConfig.Key);
				if (!Chainloader.PluginInfos.TryGetValue(fileNameWithoutExtension, out var value))
				{
					Log.LogDebug((object)("[ConfigSync] Skipping config for uninstalled mod: " + fileNameWithoutExtension));
					continue;
				}
				ConfigFile config = value.Instance.Config;
				Dictionary<string, Dictionary<string, string>> dictionary = ParseIni(receivedConfig.Value);
				bool saveOnConfigSet = config.SaveOnConfigSet;
				config.SaveOnConfigSet = false;
				try
				{
					foreach (ConfigDefinition key in config.Keys)
					{
						if (dictionary.TryGetValue(key.Section, out var value2) && value2.TryGetValue(key.Key, out var value3))
						{
							ConfigEntryBase val = config[key];
							try
							{
								val.BoxedValue = TomlTypeConverter.ConvertToValue(value3, val.SettingType);
								num++;
							}
							catch (Exception ex)
							{
								Log.LogWarning((object)("[ConfigSync] Could not apply " + fileNameWithoutExtension + "/" + key.Section + "/" + key.Key + ": " + ex.Message));
							}
						}
					}
				}
				finally
				{
					config.SaveOnConfigSet = saveOnConfigSet;
				}
				Log.LogInfo((object)("[ConfigSync] Applied in-memory config for: " + fileNameWithoutExtension));
			}
			return num;
		}

		private static Dictionary<string, Dictionary<string, string>> ParseIni(string content)
		{
			Dictionary<string, Dictionary<string, string>> dictionary = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
			Dictionary<string, string> value = null;
			string[] array = content.Split(new char[1] { '\n' });
			for (int i = 0; i < array.Length; i++)
			{
				string text = array[i].Trim();
				if (string.IsNullOrEmpty(text) || text.StartsWith("#"))
				{
					continue;
				}
				if (text.StartsWith("[") && text.EndsWith("]"))
				{
					string key = text.Substring(1, text.Length - 2).Trim();
					if (!dictionary.TryGetValue(key, out value))
					{
						value = (dictionary[key] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase));
					}
					continue;
				}
				int num = text.IndexOf('=');
				if (num > 0 && value != null)
				{
					string key2 = text.Substring(0, num).Trim();
					string value2 = text.Substring(num + 1).Trim();
					value[key2] = value2;
				}
			}
			return dictionary;
		}
	}
	internal static class ModChecker
	{
		internal static string[] GetLocalModList()
		{
			return (from kvp in Chainloader.PluginInfos
				where kvp.Key != "com.Jaz.modsync"
				select $"{kvp.Key}|{kvp.Value.Metadata.Version}" into s
				orderby s
				select s).ToArray();
		}

		internal static MismatchReport Compare(string[] localMods, string[] remoteMods)
		{
			HashSet<string> hashSet = new HashSet<string>(localMods);
			HashSet<string> hashSet2 = new HashSet<string>(remoteMods);
			string[] missingFromLocal = hashSet2.Except(hashSet).ToArray();
			string[] missingFromRemote = hashSet.Except(hashSet2).ToArray();
			return new MismatchReport(missingFromLocal, missingFromRemote);
		}
	}
	internal class MismatchReport
	{
		internal string[] MissingFromLocal { get; }

		internal string[] MissingFromRemote { get; }

		internal bool HasMismatches
		{
			get
			{
				if (MissingFromLocal.Length == 0)
				{
					return MissingFromRemote.Length != 0;
				}
				return true;
			}
		}

		internal MismatchReport(string[] missingFromLocal, string[] missingFromRemote)
		{
			MissingFromLocal = missingFromLocal;
			MissingFromRemote = missingFromRemote;
		}

		internal string FormatForDisplay()
		{
			List<string> list = new List<string>();
			if (MissingFromLocal.Length != 0)
			{
				list.Add("[ModSync] Mods you're missing:");
				string[] missingFromLocal = MissingFromLocal;
				foreach (string entry in missingFromLocal)
				{
					list.Add("  - " + FormatEntry(entry));
				}
			}
			if (MissingFromRemote.Length != 0)
			{
				list.Add("[ModSync] Mods the host doesn't have:");
				string[] missingFromLocal = MissingFromRemote;
				foreach (string entry2 in missingFromLocal)
				{
					list.Add("  + " + FormatEntry(entry2));
				}
			}
			return string.Join("\n", list);
		}

		private static string FormatEntry(string entry)
		{
			string[] array = entry.Split(new char[1] { '|' });
			if (array.Length != 2)
			{
				return entry;
			}
			return array[0] + " (v" + array[1] + ")";
		}
	}
	internal static class MyPluginInfo
	{
		internal const string PLUGIN_GUID = "com.Jaz.modsync";

		internal const string PLUGIN_NAME = "ModSync";

		internal const string PLUGIN_VERSION = "1.0.0";
	}
	[BepInPlugin("com.Jaz.modsync", "ModSync", "1.0.0")]
	public class Plugin : BaseUnityPlugin
	{
		private readonly Harmony _harmony = new Harmony("com.Jaz.modsync");

		internal static Plugin Instance { get; private set; }

		internal static ManualLogSource Logger { get; private set; }

		internal static ConfigEntry<bool> EnableModCheck { get; private set; }

		internal static ConfigEntry<bool> EnableConfigSync { get; private set; }

		internal static ConfigEntry<bool> NotifyOnMismatch { get; private set; }

		internal static ConfigEntry<bool> BlockJoinOnMismatch { get; private set; }

		private void Awake()
		{
			Instance = this;
			Logger = ((BaseUnityPlugin)this).Logger;
			EnableModCheck = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "EnableModCheck", true, "Check if players joining your room have the same mods installed.");
			EnableConfigSync = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "EnableConfigSync", true, "Sync host's mod configs to clients when they join the room.");
			NotifyOnMismatch = ((BaseUnityPlugin)this).Config.Bind<bool>("ModCheck", "NotifyOnMismatch", true, "Show a notification when a mod mismatch is detected.");
			BlockJoinOnMismatch = ((BaseUnityPlugin)this).Config.Bind<bool>("ModCheck", "BlockJoinOnMismatch", false, "Kick clients that have different mods than the host. (Host only)");
			_harmony.PatchAll();
			((Component)this).gameObject.AddComponent<ModSyncNetwork>();
			Logger.LogInfo((object)"ModSync v1.0.0 loaded.");
		}

		private void OnDestroy()
		{
			_harmony.UnpatchSelf();
		}
	}
}
namespace ModSync.Networking
{
	public class ModSyncNetwork : MonoBehaviour, IInRoomCallbacks, IMatchmakingCallbacks, IOnEventCallback
	{
		[CompilerGenerated]
		private sealed class <ShowNotification>d__23 : IEnumerator<object>, IDisposable, IEnumerator
		{
			private int <>1__state;

			private object <>2__current;

			object IEnumerator<object>.Current
			{
				[DebuggerHidden]
				get
				{
					return <>2__current;
				}
			}

			object IEnumerator.Current
			{
				[DebuggerHidden]
				get
				{
					return <>2__current;
				}
			}

			[DebuggerHidden]
			public <ShowNotification>d__23(int <>1__state)
			{
				this.<>1__state = <>1__state;
			}

			[DebuggerHidden]
			void IDisposable.Dispose()
			{
				<>1__state = -2;
			}

			private bool MoveNext()
			{
				switch (<>1__state)
				{
				default:
					return false;
				case 0:
					<>1__state = -1;
					<>2__current = null;
					<>1__state = 1;
					return true;
				case 1:
					<>1__state = -1;
					return false;
				}
			}

			bool IEnumerator.MoveNext()
			{
				//ILSpy generated this explicit interface implementation from .override directive in MoveNext
				return this.MoveNext();
			}

			[DebuggerHidden]
			void IEnumerator.Reset()
			{
				throw new NotSupportedException();
			}
		}

		private const byte EVT_REQUEST_MOD_SYNC = 145;

		private const byte EVT_MOD_LIST_PAYLOAD = 146;

		private const byte EVT_CONFIG_PAYLOAD = 147;

		private void OnEnable()
		{
			PhotonNetwork.AddCallbackTarget((object)this);
		}

		private void OnDisable()
		{
			PhotonNetwork.RemoveCallbackTarget((object)this);
		}

		void IInRoomCallbacks.OnPlayerEnteredRoom(Player newPlayer)
		{
			if (PhotonNetwork.IsMasterClient)
			{
				Plugin.Logger.LogInfo((object)("[ModSync] " + newPlayer.NickName + " joined — pushing mod list."));
				PushModListTo(newPlayer);
				if (Plugin.EnableConfigSync.Value)
				{
					PushConfigsTo(newPlayer);
				}
			}
		}

		void IInRoomCallbacks.OnPlayerLeftRoom(Player otherPlayer)
		{
		}

		void IInRoomCallbacks.OnRoomPropertiesUpdate(Hashtable propertiesThatChanged)
		{
		}

		void IInRoomCallbacks.OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps)
		{
		}

		void IInRoomCallbacks.OnMasterClientSwitched(Player newMasterClient)
		{
		}

		void IMatchmakingCallbacks.OnJoinedRoom()
		{
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Expected O, but got Unknown
			//IL_003f: Unknown result type (might be due to invalid IL or missing references)
			if (!PhotonNetwork.IsMasterClient)
			{
				Plugin.Logger.LogInfo((object)"[ModSync] Joined room — requesting mod sync from host.");
				RaiseEventOptions val = new RaiseEventOptions();
				val.TargetActors = new int[1] { PhotonNetwork.MasterClient.ActorNumber };
				RaiseEventOptions val2 = val;
				PhotonNetwork.RaiseEvent((byte)145, (object)null, val2, SendOptions.SendReliable);
			}
		}

		void IMatchmakingCallbacks.OnFriendListUpdate(List<FriendInfo> friendList)
		{
		}

		void IMatchmakingCallbacks.OnCreatedRoom()
		{
		}

		void IMatchmakingCallbacks.OnCreateRoomFailed(short returnCode, string message)
		{
		}

		void IMatchmakingCallbacks.OnJoinRoomFailed(short returnCode, string message)
		{
		}

		void IMatchmakingCallbacks.OnJoinRandomFailed(short returnCode, string message)
		{
		}

		void IMatchmakingCallbacks.OnLeftRoom()
		{
		}

		private static void PushModListTo(Player target)
		{
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			//IL_0019: Expected O, but got Unknown
			//IL_0037: Unknown result type (might be due to invalid IL or missing references)
			if (Plugin.EnableModCheck.Value)
			{
				string[] localModList = ModChecker.GetLocalModList();
				RaiseEventOptions val = new RaiseEventOptions();
				val.TargetActors = new int[1] { target.ActorNumber };
				RaiseEventOptions val2 = val;
				PhotonNetwork.RaiseEvent((byte)146, (object)localModList, val2, SendOptions.SendReliable);
			}
		}

		private static void PushConfigsTo(Player target)
		{
			//IL_0016: Unknown result type (might be due to invalid IL or missing references)
			//IL_001c: Expected O, but got Unknown
			//IL_003a: Unknown result type (might be due to invalid IL or missing references)
			Dictionary<string, string> localConfigs = ConfigSync.GetLocalConfigs();
			if (localConfigs.Count != 0)
			{
				byte[] array = ConfigSync.Serialize(localConfigs);
				RaiseEventOptions val = new RaiseEventOptions();
				val.TargetActors = new int[1] { target.ActorNumber };
				RaiseEventOptions val2 = val;
				PhotonNetwork.RaiseEvent((byte)147, (object)array, val2, SendOptions.SendReliable);
			}
		}

		void IOnEventCallback.OnEvent(EventData photonEvent)
		{
			switch (photonEvent.Code)
			{
			case 145:
				HandleModSyncRequest(photonEvent);
				break;
			case 146:
				HandleModListPayload(photonEvent);
				break;
			case 147:
				HandleConfigPayload(photonEvent);
				break;
			}
		}

		private void HandleModSyncRequest(EventData evt)
		{
			if (!PhotonNetwork.IsMasterClient)
			{
				return;
			}
			Room currentRoom = PhotonNetwork.CurrentRoom;
			if (currentRoom != null && currentRoom.Players.TryGetValue(evt.Sender, out var value))
			{
				Plugin.Logger.LogInfo((object)("[ModSync] " + value.NickName + " requested mod sync."));
				PushModListTo(value);
				if (Plugin.EnableConfigSync.Value)
				{
					PushConfigsTo(value);
				}
			}
		}

		private void HandleModListPayload(EventData evt)
		{
			if (!Plugin.EnableModCheck.Value)
			{
				return;
			}
			if (!(evt.CustomData is string[] remoteMods))
			{
				Plugin.Logger.LogWarning((object)"[ModSync] Received malformed mod list payload.");
				return;
			}
			MismatchReport mismatchReport = ModChecker.Compare(ModChecker.GetLocalModList(), remoteMods);
			if (!mismatchReport.HasMismatches)
			{
				Plugin.Logger.LogInfo((object)"[ModSync] Mod list matches host.");
				return;
			}
			string text = mismatchReport.FormatForDisplay();
			Plugin.Logger.LogWarning((object)text);
			if (Plugin.NotifyOnMismatch.Value)
			{
				((MonoBehaviour)this).StartCoroutine(ShowNotification(text));
			}
			if (Plugin.BlockJoinOnMismatch.Value && PhotonNetwork.IsMasterClient && evt.Sender != PhotonNetwork.LocalPlayer.ActorNumber)
			{
				Room currentRoom = PhotonNetwork.CurrentRoom;
				if (currentRoom != null && currentRoom.Players.TryGetValue(evt.Sender, out var value))
				{
					Plugin.Logger.LogWarning((object)("[ModSync] Kicking " + value.NickName + " due to mod mismatch."));
					PhotonNetwork.CloseConnection(value);
				}
			}
		}

		private void HandleConfigPayload(EventData evt)
		{
			if (Plugin.EnableConfigSync.Value)
			{
				if (!(evt.CustomData is byte[] data))
				{
					Plugin.Logger.LogWarning((object)"[ModSync] Received malformed config payload.");
					return;
				}
				Plugin.Logger.LogInfo((object)"[ModSync] Received config bundle from host — applying in-memory...");
				int num = ConfigSync.ApplyInMemory(ConfigSync.Deserialize(data));
				Plugin.Logger.LogInfo((object)$"[ModSync] Applied {num} setting(s) from host config.");
			}
		}

		[IteratorStateMachine(typeof(<ShowNotification>d__23))]
		private IEnumerator ShowNotification(string message)
		{
			//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
			return new <ShowNotification>d__23(0);
		}
	}
}