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 GetLocalConfigs() { Dictionary dictionary = new Dictionary(); string configPath = Paths.ConfigPath; foreach (KeyValuePair 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 configs) { using MemoryStream memoryStream = new MemoryStream(); using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8); binaryWriter.Write(configs.Count); foreach (KeyValuePair config in configs) { binaryWriter.Write(config.Key); binaryWriter.Write(config.Value); } return memoryStream.ToArray(); } internal static Dictionary Deserialize(byte[] data) { Dictionary dictionary = new Dictionary(); 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 receivedConfigs) { int num = 0; foreach (KeyValuePair 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> 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> ParseIni(string content) { Dictionary> dictionary = new Dictionary>(StringComparer.OrdinalIgnoreCase); Dictionary 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(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 hashSet = new HashSet(localMods); HashSet hashSet2 = new HashSet(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 list = new List(); 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 EnableModCheck { get; private set; } internal static ConfigEntry EnableConfigSync { get; private set; } internal static ConfigEntry NotifyOnMismatch { get; private set; } internal static ConfigEntry BlockJoinOnMismatch { get; private set; } private void Awake() { Instance = this; Logger = ((BaseUnityPlugin)this).Logger; EnableModCheck = ((BaseUnityPlugin)this).Config.Bind("General", "EnableModCheck", true, "Check if players joining your room have the same mods installed."); EnableConfigSync = ((BaseUnityPlugin)this).Config.Bind("General", "EnableConfigSync", true, "Sync host's mod configs to clients when they join the room."); NotifyOnMismatch = ((BaseUnityPlugin)this).Config.Bind("ModCheck", "NotifyOnMismatch", true, "Show a notification when a mod mismatch is detected."); BlockJoinOnMismatch = ((BaseUnityPlugin)this).Config.Bind("ModCheck", "BlockJoinOnMismatch", false, "Kick clients that have different mods than the host. (Host only)"); _harmony.PatchAll(); ((Component)this).gameObject.AddComponent(); 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 d__23 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public 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 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 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(d__23))] private IEnumerator ShowNotification(string message) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__23(0); } } }