Decompiled source of ServerConfigSync v0.1.3

ServerConfigSync.dll

Decompiled a week ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("ServerConfigSync")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.1.3.0")]
[assembly: AssemblyInformationalVersion("0.1.3+b384e504f03fd76dcc4a95951af60455ce897bbc")]
[assembly: AssemblyProduct("ServerConfigSync")]
[assembly: AssemblyTitle("ServerConfigSync")]
[assembly: InternalsVisibleTo("ServerConfigSync.Tests")]
[assembly: AssemblyVersion("0.1.3.0")]
[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 ServerConfigSync
{
	internal static class ClientSide
	{
		private sealed class Tracked
		{
			public string Guid;

			public string Local;

			public string Server;

			public string Applied;
		}

		private static readonly object Lock = new object();

		private static readonly Dictionary<ConfigEntryBase, Tracked> TrackedEntries = new Dictionary<ConfigEntryBase, Tracked>();

		private static readonly HashSet<ConfigFile> TrackedFiles = new HashSet<ConfigFile>();

		private static readonly Dictionary<Type, FieldInfo> ValueFields = new Dictionary<Type, FieldInfo>();

		[ThreadStatic]
		private static int _busy;

		[ThreadStatic]
		private static HashSet<ConfigFile> _reloading;

		private static ZNetPeer _serverPeer;

		private static ChunkAssembler _assembler;

		private static readonly List<string> PendingMessages = new List<string>();

		private static bool _loggedRefusal;

		internal static Func<string, ConfigFile> ResolveConfig = delegate(string guid)
		{
			if (!Chainloader.PluginInfos.TryGetValue(guid, out var value))
			{
				return (ConfigFile)null;
			}
			if (value == null)
			{
				return (ConfigFile)null;
			}
			BaseUnityPlugin instance = value.Instance;
			return (instance == null) ? null : instance.Config;
		};

		private static MethodInfo _messageMethod;

		public static void RegisterPeer(ZNetPeer peer)
		{
			//IL_003b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0041: Expected O, but got Unknown
			_serverPeer = peer;
			_assembler = new ChunkAssembler();
			peer.m_rpc.Register<ZPackage>("ServerConfigSync_Data", (Action<ZRpc, ZPackage>)OnData);
			ZPackage val = new ZPackage();
			val.Write(1);
			val.Write("0.1.3");
			peer.m_rpc.Invoke("ServerConfigSync_Hello", new object[1] { val });
		}

		private static void OnData(ZRpc rpc, ZPackage pkg)
		{
			try
			{
				if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer() || _serverPeer == null || rpc != _serverPeer.m_rpc)
				{
					Plugin.Log.LogWarning((object)"Ignored config data that did not come from the connected server.");
					return;
				}
				if (!Plugin.AcceptServerConfig.Value)
				{
					if (!_loggedRefusal)
					{
						Plugin.Log.LogInfo((object)"Server sent its settings, but AcceptServerConfig is off. Ignored.");
					}
					_loggedRefusal = true;
					return;
				}
				int num = pkg.ReadInt();
				if (num != 1)
				{
					Plugin.Log.LogWarning((object)$"Server uses ServerConfigSync protocol {num}, this client uses {1}. Update the mod. Nothing applied.");
					return;
				}
				int transferId = pkg.ReadInt();
				int index = pkg.ReadInt();
				int count = pkg.ReadInt();
				int num2 = pkg.ReadInt();
				if (num2 < 0 || num2 > 250000 || pkg.ReadInt() != num2)
				{
					throw new InvalidDataException("bad chunk length");
				}
				byte[] data = pkg.ReadByteArray(num2);
				byte[] array = _assembler.Add(transferId, index, count, data);
				if (array != null)
				{
					Apply(Protocol.Deserialize(array));
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("Rejected config data from the server: " + ex.Message));
			}
		}

		internal static void Apply(Snapshot snapshot)
		{
			//IL_00b9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c0: Expected O, but got Unknown
			int num = 0;
			int num2 = 0;
			int num3 = 0;
			Dictionary<string, List<ConfigEntryBase>> dictionary = new Dictionary<string, List<ConfigEntryBase>>();
			lock (Lock)
			{
				_busy++;
				try
				{
					Dictionary<ConfigEntryBase, KeyValuePair<string, string>> dictionary2 = new Dictionary<ConfigEntryBase, KeyValuePair<string, string>>();
					Dictionary<ConfigFile, Dictionary<ConfigDefinition, string>> cache = new Dictionary<ConfigFile, Dictionary<ConfigDefinition, string>>();
					foreach (KeyValuePair<string, List<SyncedValue>> plugin in snapshot.Plugins)
					{
						string key = plugin.Key;
						if (key == "Manke.ServerConfigSync")
						{
							continue;
						}
						ConfigFile val = ResolveConfig(key);
						if (val == null)
						{
							num++;
						}
						else
						{
							if (Plugin.IsCoreConfig(val))
							{
								continue;
							}
							foreach (SyncedValue item in plugin.Value)
							{
								ConfigDefinition val2;
								try
								{
									val2 = new ConfigDefinition(item.Section, item.Key);
								}
								catch
								{
									num2++;
									continue;
								}
								if (!val.ContainsKey(val2))
								{
									num2++;
								}
								else
								{
									dictionary2[val[val2]] = new KeyValuePair<string, string>(key, item.Value);
								}
							}
						}
					}
					foreach (KeyValuePair<ConfigEntryBase, Tracked> item2 in TrackedEntries.ToList())
					{
						if (!dictionary2.ContainsKey(item2.Key))
						{
							SetValue(item2.Key, item2.Value.Local);
							TrackedEntries.Remove(item2.Key);
							num3++;
						}
					}
					foreach (KeyValuePair<ConfigEntryBase, KeyValuePair<string, string>> item3 in dictionary2)
					{
						ConfigEntryBase key2 = item3.Key;
						if (!TrackedEntries.TryGetValue(key2, out var value))
						{
							value = new Tracked
							{
								Guid = item3.Value.Key,
								Local = PlayerValue(key2, cache)
							};
							TrackedEntries[key2] = value;
						}
						value.Server = item3.Value.Value;
						SetValue(key2, value.Server);
						value.Applied = key2.GetSerializedValue();
						if (value.Applied != value.Local)
						{
							if (!dictionary.TryGetValue(value.Guid, out var value2))
							{
								value2 = (dictionary[value.Guid] = new List<ConfigEntryBase>());
							}
							value2.Add(key2);
							Plugin.Verbose("  " + value.Guid + " [" + key2.Definition.Section + "] " + key2.Definition.Key + ": " + value.Local + " -> " + value.Applied);
						}
					}
					UpdateFileSubscriptions();
				}
				finally
				{
					_busy--;
				}
			}
			int num4 = dictionary.Values.Sum((List<ConfigEntryBase> l) => l.Count);
			Plugin.Log.LogInfo((object)($"Applied server settings (server mod {snapshot.ServerVersion}): {TrackedEntries.Count} entries from {snapshot.Plugins.Count} mods, " + $"{num4} differ from your own values" + ((num3 > 0) ? $", {num3} restored to yours" : "") + ((num > 0) ? $", {num} mods not installed here" : "") + ((num2 > 0) ? $", {num2} unknown entries ignored" : "") + "."));
			if (num4 > 0)
			{
				QueueMessages(dictionary, num4);
			}
		}

		private static void QueueMessages(Dictionary<string, List<ConfigEntryBase>> differing, int differingCount)
		{
			List<string> list = new List<string>();
			foreach (KeyValuePair<string, List<ConfigEntryBase>> item in differing)
			{
				if (!item.Value.Any(HasListeners))
				{
					list.Add(PluginName(item.Key));
				}
			}
			list.Sort(StringComparer.OrdinalIgnoreCase);
			if (list.Count > 0)
			{
				Plugin.Log.LogInfo((object)("These mods do not listen for setting changes; if a server value seems to have no effect, they may only read it at startup: " + string.Join(", ", list.ToArray())));
			}
			lock (PendingMessages)
			{
				PendingMessages.Clear();
				PendingMessages.Add($"Server settings applied: {differingCount} value(s) in {differing.Count} mod(s)");
				if (Plugin.ShowStartupWarning.Value && list.Count > 0)
				{
					PendingMessages.Add(Plugin.ListAffectedMods.Value ? ("Some mods may only read these settings at startup: " + string.Join(", ", list.ToArray())) : $"{list.Count} mod(s) may only read these settings at startup (see the log)");
				}
			}
		}

		private static string PlayerValue(ConfigEntryBase entry, Dictionary<ConfigFile, Dictionary<ConfigDefinition, string>> cache)
		{
			ConfigFile configFile = entry.ConfigFile;
			if (!cache.TryGetValue(configFile, out var value))
			{
				value = (cache[configFile] = ReadFile(configFile.ConfigFilePath));
			}
			if (value == null || !value.TryGetValue(entry.Definition, out var value2))
			{
				return entry.GetSerializedValue();
			}
			return value2;
		}

		private static Dictionary<ConfigDefinition, string> ReadFile(string path)
		{
			//IL_009b: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ae: Expected O, but got Unknown
			try
			{
				if (!File.Exists(path))
				{
					return null;
				}
				Dictionary<ConfigDefinition, string> dictionary = new Dictionary<ConfigDefinition, string>();
				string text = string.Empty;
				string[] array = File.ReadAllLines(path);
				for (int i = 0; i < array.Length; i++)
				{
					string text2 = array[i].Trim();
					if (text2.StartsWith("#"))
					{
						continue;
					}
					if (text2.StartsWith("[") && text2.EndsWith("]"))
					{
						text = text2.Substring(1, text2.Length - 2);
						continue;
					}
					string[] array2 = text2.Split(new char[1] { '=' }, 2);
					if (array2.Length == 2)
					{
						try
						{
							dictionary[new ConfigDefinition(text, array2[0].Trim())] = array2[1].Trim();
						}
						catch (ArgumentException)
						{
						}
					}
				}
				return dictionary;
			}
			catch (Exception ex2)
			{
				Plugin.Log.LogWarning((object)("Could not read " + Path.GetFileName(path) + ": " + ex2.Message));
				return null;
			}
		}

		public static void RestoreAll(string reason)
		{
			int count;
			lock (Lock)
			{
				count = TrackedEntries.Count;
				if (count > 0)
				{
					_busy++;
					try
					{
						foreach (KeyValuePair<ConfigEntryBase, Tracked> trackedEntry in TrackedEntries)
						{
							try
							{
								SetValue(trackedEntry.Key, trackedEntry.Value.Local);
							}
							catch (Exception ex)
							{
								Plugin.Log.LogWarning((object)$"Could not restore {trackedEntry.Key.Definition}: {ex.Message}");
							}
						}
						TrackedEntries.Clear();
						UpdateFileSubscriptions();
					}
					finally
					{
						_busy--;
					}
				}
			}
			_serverPeer = null;
			_assembler = null;
			_loggedRefusal = false;
			lock (PendingMessages)
			{
				PendingMessages.Clear();
			}
			if (count > 0)
			{
				Plugin.Log.LogInfo((object)$"Restored your own values for {count} entries ({reason}).");
			}
		}

		private static void UpdateFileSubscriptions()
		{
			HashSet<ConfigFile> needed = new HashSet<ConfigFile>(TrackedEntries.Keys.Select((ConfigEntryBase e) => e.ConfigFile));
			foreach (ConfigFile item in TrackedFiles.Where((ConfigFile f) => !needed.Contains(f)).ToList())
			{
				item.SettingChanged -= OnSettingChanged;
				TrackedFiles.Remove(item);
			}
			foreach (ConfigFile item2 in needed)
			{
				if (TrackedFiles.Add(item2))
				{
					item2.SettingChanged += OnSettingChanged;
				}
			}
		}

		private static void SetValue(ConfigEntryBase entry, string serialized)
		{
			ConfigFile configFile = entry.ConfigFile;
			bool saveOnConfigSet = configFile.SaveOnConfigSet;
			configFile.SaveOnConfigSet = false;
			try
			{
				entry.SetSerializedValue(serialized);
			}
			finally
			{
				configFile.SaveOnConfigSet = saveOnConfigSet;
			}
		}

		private static bool SetSilently(ConfigEntryBase entry, string serialized)
		{
			Type type = ((object)entry).GetType();
			if (!ValueFields.TryGetValue(type, out var value))
			{
				value = AccessTools.Field(type, "_typedValue");
				ValueFields[type] = value;
				if (value == null)
				{
					Plugin.Log.LogWarning((object)$"Unsupported BepInEx version: no value field on {type}.");
				}
			}
			if (value == null)
			{
				return false;
			}
			value.SetValue(entry, TomlTypeConverter.ConvertToValue(serialized, entry.SettingType));
			return true;
		}

		private static void OnSettingChanged(object sender, SettingChangedEventArgs e)
		{
			if (_busy > 0)
			{
				return;
			}
			ConfigEntryBase changedSetting = e.ChangedSetting;
			lock (Lock)
			{
				if (!TrackedEntries.TryGetValue(changedSetting, out var value))
				{
					return;
				}
				string serializedValue = changedSetting.GetSerializedValue();
				if (serializedValue == value.Applied)
				{
					return;
				}
				value.Local = serializedValue;
				_busy++;
				try
				{
					SetValue(changedSetting, value.Server);
					value.Applied = changedSetting.GetSerializedValue();
					if (changedSetting.ConfigFile.SaveOnConfigSet)
					{
						changedSetting.ConfigFile.Save();
					}
				}
				finally
				{
					_busy--;
				}
				Plugin.Log.LogInfo((object)("[" + changedSetting.Definition.Section + "] " + changedSetting.Definition.Key + " changed locally to " + serializedValue + ": saved as your value, the server value stays active until you disconnect."));
			}
		}

		public static bool BeforeSave(ConfigFile file, out List<KeyValuePair<ConfigEntryBase, string>> swapped)
		{
			swapped = null;
			lock (Lock)
			{
				if (!TrackedFiles.Contains(file))
				{
					return true;
				}
				if (_reloading != null && _reloading.Contains(file))
				{
					return false;
				}
				swapped = new List<KeyValuePair<ConfigEntryBase, string>>();
				foreach (KeyValuePair<ConfigEntryBase, Tracked> trackedEntry in TrackedEntries)
				{
					if (trackedEntry.Key.ConfigFile != file)
					{
						continue;
					}
					string serializedValue = trackedEntry.Key.GetSerializedValue();
					if (!(serializedValue == trackedEntry.Value.Local))
					{
						if (!SetSilently(trackedEntry.Key, trackedEntry.Value.Local))
						{
							AfterSave(swapped);
							swapped = null;
							Plugin.Log.LogWarning((object)("Skipped saving " + Path.GetFileName(file.ConfigFilePath) + " to protect your own values."));
							return false;
						}
						swapped.Add(new KeyValuePair<ConfigEntryBase, string>(trackedEntry.Key, serializedValue));
					}
				}
				return true;
			}
		}

		public static void AfterSave(List<KeyValuePair<ConfigEntryBase, string>> swapped)
		{
			if (swapped == null)
			{
				return;
			}
			lock (Lock)
			{
				foreach (KeyValuePair<ConfigEntryBase, string> item in swapped)
				{
					SetSilently(item.Key, item.Value);
				}
			}
		}

		public static bool BeforeReload(ConfigFile file)
		{
			lock (Lock)
			{
				if (!TrackedFiles.Contains(file))
				{
					return false;
				}
				foreach (KeyValuePair<ConfigEntryBase, Tracked> trackedEntry in TrackedEntries)
				{
					if (trackedEntry.Key.ConfigFile == file)
					{
						SetSilently(trackedEntry.Key, trackedEntry.Value.Local);
					}
				}
				_busy++;
				(_reloading ?? (_reloading = new HashSet<ConfigFile>())).Add(file);
				return true;
			}
		}

		public static void AfterReload(ConfigFile file)
		{
			if (_reloading == null || !_reloading.Remove(file))
			{
				return;
			}
			lock (Lock)
			{
				try
				{
					foreach (KeyValuePair<ConfigEntryBase, Tracked> trackedEntry in TrackedEntries)
					{
						if (trackedEntry.Key.ConfigFile == file)
						{
							trackedEntry.Value.Local = trackedEntry.Key.GetSerializedValue();
							SetValue(trackedEntry.Key, trackedEntry.Value.Server);
							trackedEntry.Value.Applied = trackedEntry.Key.GetSerializedValue();
						}
					}
				}
				finally
				{
					_busy--;
				}
			}
			Plugin.Verbose(Path.GetFileName(file.ConfigFilePath) + " was reloaded; server values applied again.");
		}

		public static void Update()
		{
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null || (Object)(object)MessageHud.instance == (Object)null)
			{
				return;
			}
			lock (PendingMessages)
			{
				foreach (string pendingMessage in PendingMessages)
				{
					ShowMessage(localPlayer, pendingMessage);
				}
				PendingMessages.Clear();
			}
		}

		private static void ShowMessage(Player player, string text)
		{
			try
			{
				if (_messageMethod == null)
				{
					_messageMethod = typeof(Character).GetMethods(BindingFlags.Instance | BindingFlags.Public).FirstOrDefault(delegate(MethodInfo m)
					{
						ParameterInfo[] parameters2 = m.GetParameters();
						return m.Name == "Message" && parameters2.Length >= 2 && parameters2[0].ParameterType == typeof(MessageType) && parameters2[1].ParameterType == typeof(string);
					});
				}
				if (!(_messageMethod == null))
				{
					ParameterInfo[] parameters = _messageMethod.GetParameters();
					object[] array = new object[parameters.Length];
					array[0] = (object)(MessageType)1;
					array[1] = text;
					for (int num = 2; num < parameters.Length; num++)
					{
						Type parameterType = parameters[num].ParameterType;
						array[num] = (parameters[num].HasDefaultValue ? parameters[num].DefaultValue : (parameterType.IsValueType ? Activator.CreateInstance(parameterType) : null));
					}
					_messageMethod.Invoke(player, array);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogDebug((object)("Cannot show message: " + ex.Message));
			}
		}

		private static bool HasListeners(ConfigEntryBase entry)
		{
			try
			{
				if (AccessTools.Field(((object)entry).GetType(), "SettingChanged")?.GetValue(entry) is Delegate obj && obj.GetInvocationList().Length != 0)
				{
					return true;
				}
				if (AccessTools.Field(typeof(ConfigFile), "SettingChanged")?.GetValue(entry.ConfigFile) is Delegate obj2)
				{
					Delegate[] invocationList = obj2.GetInvocationList();
					foreach (Delegate obj3 in invocationList)
					{
						if (!(obj3.Target is ConfigEntryBase) && !(obj3.Method.DeclaringType?.Assembly == typeof(ClientSide).Assembly))
						{
							return true;
						}
					}
				}
			}
			catch
			{
				return true;
			}
			return false;
		}

		private static string PluginName(string guid)
		{
			if (!Chainloader.PluginInfos.TryGetValue(guid, out var value))
			{
				return guid;
			}
			return value.Metadata.Name;
		}
	}
	internal static class EntryFilter
	{
		private static readonly Regex SecretName = new Regex("webhook|token|passw|pwd|secret|api[\\s_\\-]?key|auth|credential|private[\\s_\\-]?key|url|\\buri\\b|endpoint|connection[\\s_\\-]?string", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);

		private static readonly Regex SecretValue = new Regex("^\\s*[a-z][a-z0-9+.\\-]*://|discord(app)?\\.com/api/webhooks", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);

		private static readonly Regex KeybindName = new Regex("hotkey|keybind|key\\s*bind|shortcut|keycode|modifier\\s*key", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);

		private static readonly Dictionary<Assembly, bool> ServerSyncCache = new Dictionary<Assembly, bool>();

		private static readonly Regex OwnSyncTypeName = new Regex("ServerSync|ConfigSync", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);

		public static bool IsPluginSelected(string guid)
		{
			return Plugin.Mode.Value switch
			{
				SyncMode.Whitelist => SplitList(Plugin.PluginList.Value).Contains(guid), 
				SyncMode.Blacklist => !SplitList(Plugin.PluginList.Value).Contains(guid), 
				_ => true, 
			};
		}

		public static bool IsKeybind(ConfigEntryBase entry)
		{
			if (!(entry.SettingType == typeof(KeyboardShortcut)) && !(entry.SettingType == typeof(KeyCode)))
			{
				return KeybindName.IsMatch(entry.Definition.Key);
			}
			return true;
		}

		public static bool LooksSecret(ConfigEntryBase entry, string serializedValue)
		{
			if (!SecretName.IsMatch(entry.Definition.Key) && !SecretName.IsMatch(entry.Definition.Section))
			{
				if (!string.IsNullOrEmpty(serializedValue))
				{
					return SecretValue.IsMatch(serializedValue);
				}
				return false;
			}
			return true;
		}

		public static bool SyncsItself(PluginInfo info, out string how)
		{
			Assembly assembly = ((object)info.Instance).GetType().Assembly;
			if (UsesServerSync(assembly))
			{
				how = "ServerSync";
				return true;
			}
			how = null;
			try
			{
				Type[] typesFromAssembly = AccessTools.GetTypesFromAssembly(assembly);
				foreach (Type type in typesFromAssembly)
				{
					if (!(type.Namespace == "ServerSync") && OwnSyncTypeName.IsMatch(type.Name))
					{
						how = "own sync code";
						return true;
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogDebug((object)("Could not inspect " + info.Metadata.GUID + ": " + ex.Message));
			}
			return false;
		}

		private static bool UsesServerSync(Assembly assembly)
		{
			if (ServerSyncCache.TryGetValue(assembly, out var value))
			{
				return value;
			}
			bool flag = false;
			Type type = assembly.GetType("ServerSync.ConfigSync", throwOnError: false);
			if (type != null)
			{
				flag = true;
				try
				{
					FieldInfo fieldInfo = AccessTools.Field(type, "configSyncs");
					FieldInfo fieldInfo2 = AccessTools.Field(type, "allConfigs");
					if (fieldInfo?.GetValue(null) is IEnumerable enumerable && fieldInfo2 != null)
					{
						flag = false;
						foreach (object item in enumerable)
						{
							if (fieldInfo2.GetValue(item) is IEnumerable enumerable2 && enumerable2.GetEnumerator().MoveNext())
							{
								flag = true;
								break;
							}
						}
					}
				}
				catch (Exception ex)
				{
					Plugin.Log.LogDebug((object)("Could not inspect ServerSync in " + assembly.GetName().Name + ": " + ex.Message));
				}
			}
			ServerSyncCache[assembly] = flag;
			return flag;
		}

		public static bool IsJotunnAdminOnly(ConfigEntryBase entry)
		{
			ConfigDescription description = entry.Description;
			object[] array = ((description != null) ? description.Tags : null);
			if (array == null)
			{
				return false;
			}
			object[] array2 = array;
			bool flag = default(bool);
			foreach (object obj in array2)
			{
				if (obj == null)
				{
					continue;
				}
				Type type = obj.GetType();
				if (!(type.Name != "ConfigurationManagerAttributes") && !(type.Assembly.GetName().Name != "Jotunn"))
				{
					object obj2 = AccessTools.Property(type, "IsAdminOnly")?.GetValue(obj, null) ?? AccessTools.Field(type, "IsAdminOnly")?.GetValue(obj);
					int num;
					if (obj2 is bool)
					{
						flag = (bool)obj2;
						num = 1;
					}
					else
					{
						num = 0;
					}
					if (((uint)num & (flag ? 1u : 0u)) != 0)
					{
						return true;
					}
				}
			}
			return false;
		}

		public static HashSet<string> SplitList(string value)
		{
			HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			if (string.IsNullOrEmpty(value))
			{
				return hashSet;
			}
			string[] array = value.Split(',');
			for (int i = 0; i < array.Length; i++)
			{
				string text = array[i].Trim();
				if (text.Length > 0)
				{
					hashSet.Add(text);
				}
			}
			return hashSet;
		}
	}
	internal sealed class EntryPatterns
	{
		private readonly List<Regex[]> _patterns = new List<Regex[]>();

		private readonly string _source;

		private static readonly Dictionary<string, EntryPatterns> Cache = new Dictionary<string, EntryPatterns>();

		private EntryPatterns(string source)
		{
			_source = source ?? "";
			foreach (string item in EntryFilter.SplitList(_source))
			{
				string[] array = item.Split(new char[1] { ':' }, 3);
				Regex[] array2 = new Regex[array.Length];
				for (int i = 0; i < array.Length; i++)
				{
					string pattern = "^" + Regex.Escape(array[i].Trim()).Replace("\\*", ".*") + "$";
					array2[i] = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
				}
				_patterns.Add(array2);
			}
		}

		public static EntryPatterns Get(string source)
		{
			source = source ?? "";
			if (!Cache.TryGetValue(source, out var value))
			{
				value = new EntryPatterns(source);
				Cache[source] = value;
			}
			return value;
		}

		public bool Matches(string guid, string section, string key)
		{
			foreach (Regex[] pattern in _patterns)
			{
				if (pattern[0].IsMatch(guid) && (pattern.Length <= 1 || pattern[1].IsMatch(section)) && (pattern.Length <= 2 || pattern[2].IsMatch(key)))
				{
					return true;
				}
			}
			return false;
		}
	}
	internal static class SnapshotBuilder
	{
		public static Snapshot Build()
		{
			Snapshot snapshot = new Snapshot
			{
				ServerVersion = "0.1.3"
			};
			EntryPatterns entryPatterns = EntryPatterns.Get(Plugin.ExcludedEntries.Value);
			EntryPatterns entryPatterns2 = EntryPatterns.Get(Plugin.AllowedSecretEntries.Value);
			List<string> list = new List<string>();
			List<string> list2 = new List<string>();
			int num = 0;
			int num2 = 0;
			int num3 = 0;
			foreach (PluginInfo value2 in Chainloader.PluginInfos.Values)
			{
				if ((Object)(object)((value2 != null) ? value2.Instance : null) == (Object)null)
				{
					continue;
				}
				string gUID = value2.Metadata.GUID;
				if (gUID == "Manke.ServerConfigSync" || !EntryFilter.IsPluginSelected(gUID))
				{
					continue;
				}
				ConfigFile config = value2.Instance.Config;
				if (config == null || Plugin.IsCoreConfig(config))
				{
					continue;
				}
				if (Plugin.SkipSelfSyncingMods.Value && EntryFilter.SyncsItself(value2, out var how))
				{
					list.Add(value2.Metadata.Name + " (" + how + ")");
					continue;
				}
				List<SyncedValue> list3 = new List<SyncedValue>();
				foreach (KeyValuePair<ConfigDefinition, ConfigEntryBase> item in config)
				{
					ConfigEntryBase value = item.Value;
					string section = value.Definition.Section;
					string key = value.Definition.Key;
					if (entryPatterns.Matches(gUID, section, key))
					{
						num3++;
						continue;
					}
					if (!Plugin.SyncKeybinds.Value && EntryFilter.IsKeybind(value))
					{
						num++;
						continue;
					}
					if (Plugin.SkipSelfSyncingMods.Value && EntryFilter.IsJotunnAdminOnly(value))
					{
						num2++;
						continue;
					}
					string serializedValue;
					try
					{
						serializedValue = value.GetSerializedValue();
					}
					catch (Exception ex)
					{
						Plugin.Log.LogWarning((object)("Cannot read " + gUID + " [" + section + "] " + key + ": " + ex.Message));
						continue;
					}
					if (EntryFilter.LooksSecret(value, serializedValue) && !entryPatterns2.Matches(gUID, section, key))
					{
						list2.Add(gUID + ":" + section + ":" + key);
					}
					else
					{
						list3.Add(new SyncedValue
						{
							Section = section,
							Key = key,
							Value = serializedValue
						});
					}
				}
				if (list3.Count > 0)
				{
					snapshot.Plugins[gUID] = list3;
				}
			}
			List<string> summary = snapshot.Summary;
			summary.Add($"Prepared {snapshot.EntryCount} entries from {snapshot.Plugins.Count} mods for connecting players " + $"(mode {Plugin.Mode.Value}; skipped {num} keybinds, {num2} Jotunn admin-only, {num3} excluded).");
			if (list.Count > 0)
			{
				summary.Add("Skipped mods that sync their own settings: " + string.Join(", ", list.ToArray()));
			}
			if (list2.Count > 0)
			{
				summary.Add("Withheld entries that look like secrets (add them to AllowedSecretEntries to send them): " + string.Join(", ", list2.ToArray()));
			}
			if (Plugin.VerboseLogging.Value)
			{
				foreach (KeyValuePair<string, List<SyncedValue>> plugin in snapshot.Plugins)
				{
					summary.Add($"  {plugin.Key}: {plugin.Value.Count} entries");
				}
			}
			return snapshot;
		}
	}
	[HarmonyPatch(typeof(ZNet), "OnNewConnection")]
	internal static class ZNetOnNewConnectionPatch
	{
		private static void Postfix(ZNet __instance, ZNetPeer __0)
		{
			try
			{
				if (__instance.IsServer())
				{
					ServerSide.RegisterPeer(__0);
				}
				else
				{
					ClientSide.RegisterPeer(__0);
				}
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"OnNewConnection: {arg}");
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")]
	internal static class ZNetRpcPeerInfoPatch
	{
		[HarmonyPriority(600)]
		private static bool Prefix(ZNet __instance, ZRpc __0)
		{
			if (__instance.IsServer())
			{
				return ServerSide.AllowPeer(__0);
			}
			return true;
		}

		private static void Postfix(ZNet __instance, ZRpc __0)
		{
			try
			{
				if (__instance.IsServer())
				{
					ServerSide.OnPeerInfo(__0);
				}
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"Sending settings failed: {arg}");
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "Awake")]
	internal static class ZNetAwakePatch
	{
		private static void Postfix(ZNet __instance)
		{
			try
			{
				if (__instance.IsDedicated() && Plugin.ServerEnabled.Value)
				{
					ServerSide.Rebuild();
				}
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"Preparing settings failed: {arg}");
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "OnDestroy")]
	internal static class ZNetOnDestroyPatch
	{
		private static void Prefix(ZNet __instance)
		{
			if (__instance.IsServer())
			{
				ServerSide.Reset();
			}
			else
			{
				ClientSide.RestoreAll("disconnected");
			}
		}
	}
	[HarmonyPatch(typeof(ConfigFile), "Save")]
	internal static class ConfigFileSavePatch
	{
		private static bool Prefix(ConfigFile __instance, out List<KeyValuePair<ConfigEntryBase, string>> __state)
		{
			return ClientSide.BeforeSave(__instance, out __state);
		}

		private static Exception Finalizer(Exception __exception, List<KeyValuePair<ConfigEntryBase, string>> __state)
		{
			ClientSide.AfterSave(__state);
			return __exception;
		}
	}
	[HarmonyPatch(typeof(ConfigFile), "Reload")]
	internal static class ConfigFileReloadPatch
	{
		private static void Prefix(ConfigFile __instance)
		{
			ClientSide.BeforeReload(__instance);
		}

		private static Exception Finalizer(ConfigFile __instance, Exception __exception)
		{
			ClientSide.AfterReload(__instance);
			return __exception;
		}
	}
	[HarmonyPatch(typeof(ConfigFile), "OnConfigReloaded")]
	internal static class ConfigFileOnConfigReloadedPatch
	{
		private static void Prefix(ConfigFile __instance)
		{
			ClientSide.AfterReload(__instance);
		}
	}
	public enum SyncMode
	{
		All,
		Whitelist,
		Blacklist
	}
	[BepInPlugin("Manke.ServerConfigSync", "ServerConfigSync", "0.1.3")]
	public class Plugin : BaseUnityPlugin
	{
		public const string Guid = "Manke.ServerConfigSync";

		public const string Name = "ServerConfigSync";

		public const string Version = "0.1.3";

		private const string OldGuid = "peppeaiello.ServerConfigSync";

		internal static Plugin Instance;

		internal static ManualLogSource Log;

		internal static ConfigEntry<bool> ServerEnabled;

		internal static ConfigEntry<SyncMode> Mode;

		internal static ConfigEntry<string> PluginList;

		internal static ConfigEntry<string> ExcludedEntries;

		internal static ConfigEntry<bool> SyncKeybinds;

		internal static ConfigEntry<bool> SkipSelfSyncingMods;

		internal static ConfigEntry<string> AllowedSecretEntries;

		internal static ConfigEntry<bool> RequireClientMod;

		internal static ConfigEntry<bool> LiveUpdates;

		internal static ConfigEntry<bool> AcceptServerConfig;

		internal static ConfigEntry<bool> ShowStartupWarning;

		internal static ConfigEntry<bool> ListAffectedMods;

		internal static ConfigEntry<bool> VerboseLogging;

		private Harmony _harmony;

		private void Awake()
		{
			//IL_01c9: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d3: Expected O, but got Unknown
			Instance = this;
			Log = ((BaseUnityPlugin)this).Logger;
			MigrateOldConfig();
			ServerEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("1 - Server", "Enabled", true, "Send this server's mod settings to connecting players. Only used when this game instance is the server.");
			Mode = ((BaseUnityPlugin)this).Config.Bind<SyncMode>("1 - Server", "SyncMode", SyncMode.All, "All: sync every installed mod. Whitelist: only the mods in PluginList. Blacklist: every mod except those in PluginList.");
			PluginList = ((BaseUnityPlugin)this).Config.Bind<string>("1 - Server", "PluginList", "", "Comma-separated plugin GUIDs used by the Whitelist and Blacklist modes (example: ishid4.mods.betterarchery, torokal.afkmanager). The GUID of a mod is written at the top of its .cfg file.");
			ExcludedEntries = ((BaseUnityPlugin)this).Config.Bind<string>("1 - Server", "ExcludedEntries", "", "Comma-separated entries that are never synced. Format: GUID, GUID:Section or GUID:Section:Key. '*' works as a wildcard (example: ishid4.mods.betterarchery:Bow Zoom:*).");
			SyncKeybinds = ((BaseUnityPlugin)this).Config.Bind<bool>("1 - Server", "SyncKeybinds", false, "If false, keyboard shortcuts and hotkeys stay as each player set them.");
			SkipSelfSyncingMods = ((BaseUnityPlugin)this).Config.Bind<bool>("1 - Server", "SkipSelfSyncingMods", true, "Skip mods that already sync their own settings (with ServerSync, or with their own code when it can be recognized) and Jotunn entries marked admin-only. Mods whose sync code cannot be recognized are synced too; blacklist them if you prefer.");
			AllowedSecretEntries = ((BaseUnityPlugin)this).Config.Bind<string>("1 - Server", "AllowedSecretEntries", "", "Entries that look like secrets (webhooks, tokens, passwords, URLs...) are never sent. List here, with the same format as ExcludedEntries, the ones you really want to send.");
			RequireClientMod = ((BaseUnityPlugin)this).Config.Bind<bool>("1 - Server", "RequireClientMod", false, "If true, players without ServerConfigSync cannot join.");
			LiveUpdates = ((BaseUnityPlugin)this).Config.Bind<bool>("1 - Server", "LiveUpdates", true, "When a .cfg file in BepInEx/config changes while the server is running, reload it and send the new values to connected players.");
			AcceptServerConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("2 - Client", "AcceptServerConfig", true, "Apply the settings sent by the server while connected. Your own config files are never modified and your values come back when you disconnect.");
			ShowStartupWarning = ((BaseUnityPlugin)this).Config.Bind<bool>("2 - Client", "ShowStartupWarning", false, "Also show in game which mods might only read their settings at startup. This is a guess and often wrong (many mods read the value every time and work fine), so by default it only goes to the log.");
			ListAffectedMods = ((BaseUnityPlugin)this).Config.Bind<bool>("2 - Client", "ListAffectedMods", true, "Include the names of the affected mods in the in-game message.");
			VerboseLogging = ((BaseUnityPlugin)this).Config.Bind<bool>("3 - Debug", "VerboseLogging", false, "Log every synced entry.");
			_harmony = new Harmony("Manke.ServerConfigSync");
			_harmony.PatchAll(typeof(Plugin).Assembly);
			Log.LogInfo((object)"ServerConfigSync 0.1.3 loaded");
		}

		private void MigrateOldConfig()
		{
			try
			{
				string text = Path.Combine(Paths.ConfigPath, "peppeaiello.ServerConfigSync.cfg");
				if (!File.Exists(((BaseUnityPlugin)this).Config.ConfigFilePath) && File.Exists(text))
				{
					File.Copy(text, ((BaseUnityPlugin)this).Config.ConfigFilePath);
					((BaseUnityPlugin)this).Config.Reload();
					Log.LogInfo((object)("Settings carried over from peppeaiello.ServerConfigSync.cfg to " + Path.GetFileName(((BaseUnityPlugin)this).Config.ConfigFilePath) + "; the old file is no longer used."));
				}
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("Could not carry over the settings from peppeaiello.ServerConfigSync.cfg: " + ex.Message));
			}
		}

		private void Update()
		{
			ServerSide.Update();
			ClientSide.Update();
		}

		private void OnDestroy()
		{
			ClientSide.RestoreAll("plugin unloaded");
			ServerSide.StopWatcher();
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
		}

		internal static bool IsCoreConfig(ConfigFile file)
		{
			try
			{
				string bepInExConfigPath = Paths.BepInExConfigPath;
				if (string.IsNullOrEmpty(bepInExConfigPath))
				{
					return false;
				}
				return string.Equals(Path.GetFullPath(file.ConfigFilePath), Path.GetFullPath(bepInExConfigPath), StringComparison.OrdinalIgnoreCase);
			}
			catch
			{
				return true;
			}
		}

		internal static void Verbose(string message)
		{
			if (VerboseLogging.Value)
			{
				Log.LogInfo((object)message);
			}
		}
	}
	internal sealed class SyncedValue
	{
		public string Section;

		public string Key;

		public string Value;
	}
	internal sealed class Snapshot
	{
		public readonly Dictionary<string, List<SyncedValue>> Plugins = new Dictionary<string, List<SyncedValue>>();

		public string ServerVersion = "";

		public readonly List<string> Summary = new List<string>();

		public int EntryCount
		{
			get
			{
				int num = 0;
				foreach (List<SyncedValue> value in Plugins.Values)
				{
					num += value.Count;
				}
				return num;
			}
		}
	}
	internal static class Protocol
	{
		private sealed class LimitedReadStream : Stream
		{
			private readonly Stream _inner;

			private readonly long _limit;

			private long _read;

			public override bool CanRead => true;

			public override bool CanSeek => false;

			public override bool CanWrite => false;

			public override long Length
			{
				get
				{
					throw new NotSupportedException();
				}
			}

			public override long Position
			{
				get
				{
					return _read;
				}
				set
				{
					throw new NotSupportedException();
				}
			}

			public LimitedReadStream(Stream inner, long limit)
			{
				_inner = inner;
				_limit = limit;
			}

			public override int ReadByte()
			{
				byte[] array = new byte[1];
				if (Read(array, 0, 1) != 1)
				{
					return -1;
				}
				return array[0];
			}

			public override int Read(byte[] buffer, int offset, int count)
			{
				int num = _inner.Read(buffer, offset, count);
				_read += num;
				if (_read > _limit)
				{
					throw new InvalidDataException("payload too large");
				}
				return num;
			}

			public override void Flush()
			{
			}

			public override long Seek(long offset, SeekOrigin origin)
			{
				throw new NotSupportedException();
			}

			public override void SetLength(long value)
			{
				throw new NotSupportedException();
			}

			public override void Write(byte[] buffer, int offset, int count)
			{
				throw new NotSupportedException();
			}
		}

		public const int Version = 1;

		public const string RpcHello = "ServerConfigSync_Hello";

		public const string RpcData = "ServerConfigSync_Data";

		public const int ChunkSize = 250000;

		public const int MaxChunks = 64;

		public const int MaxDecompressedBytes = 33554432;

		public const int MaxPlugins = 2000;

		public const int MaxEntriesPerPlugin = 20000;

		public const int MaxEntries = 100000;

		public const int MaxStringLength = 65536;

		public static byte[] Serialize(Snapshot snapshot)
		{
			using MemoryStream memoryStream = new MemoryStream();
			using (GZipStream output = new GZipStream(memoryStream, CompressionMode.Compress, leaveOpen: true))
			{
				using BinaryWriter binaryWriter = new BinaryWriter(output);
				binaryWriter.Write(1);
				binaryWriter.Write(snapshot.ServerVersion ?? "");
				binaryWriter.Write(snapshot.Plugins.Count);
				foreach (KeyValuePair<string, List<SyncedValue>> plugin in snapshot.Plugins)
				{
					binaryWriter.Write(plugin.Key);
					binaryWriter.Write(plugin.Value.Count);
					foreach (SyncedValue item in plugin.Value)
					{
						binaryWriter.Write(item.Section);
						binaryWriter.Write(item.Key);
						binaryWriter.Write(item.Value ?? "");
					}
				}
			}
			return memoryStream.ToArray();
		}

		public static Snapshot Deserialize(byte[] compressed)
		{
			using MemoryStream stream = new MemoryStream(compressed, writable: false);
			using GZipStream inner = new GZipStream(stream, CompressionMode.Decompress);
			using LimitedReadStream limitedReadStream = new LimitedReadStream(inner, 33554432L);
			using BinaryReader binaryReader = new BinaryReader(limitedReadStream);
			int num = binaryReader.ReadInt32();
			if (num != 1)
			{
				throw new InvalidDataException($"unsupported protocol version {num}");
			}
			Snapshot snapshot = new Snapshot
			{
				ServerVersion = ReadString(binaryReader)
			};
			int num2 = ReadCount(binaryReader, 2000);
			int num3 = 0;
			for (int i = 0; i < num2; i++)
			{
				string key = ReadString(binaryReader);
				int num4 = ReadCount(binaryReader, 20000);
				num3 += num4;
				if (num3 > 100000)
				{
					throw new InvalidDataException("too many entries");
				}
				List<SyncedValue> list = new List<SyncedValue>(num4);
				for (int j = 0; j < num4; j++)
				{
					list.Add(new SyncedValue
					{
						Section = ReadString(binaryReader),
						Key = ReadString(binaryReader),
						Value = ReadString(binaryReader)
					});
				}
				snapshot.Plugins[key] = list;
			}
			if (limitedReadStream.ReadByte() != -1)
			{
				throw new InvalidDataException("unexpected data after the payload");
			}
			return snapshot;
		}

		private static int ReadCount(BinaryReader r, int max)
		{
			int num = r.ReadInt32();
			if (num < 0 || num > max)
			{
				throw new InvalidDataException($"count {num} out of range");
			}
			return num;
		}

		private static string ReadString(BinaryReader r)
		{
			int num = 0;
			int num2 = 0;
			while (true)
			{
				if (num2 > 28)
				{
					throw new InvalidDataException("bad string length");
				}
				byte b = r.ReadByte();
				num |= (b & 0x7F) << num2;
				if ((b & 0x80) == 0)
				{
					break;
				}
				num2 += 7;
			}
			if (num < 0 || num > 65536)
			{
				throw new InvalidDataException("string too long");
			}
			byte[] array = r.ReadBytes(num);
			if (array.Length != num)
			{
				throw new EndOfStreamException();
			}
			return Encoding.UTF8.GetString(array);
		}

		public static List<byte[]> Split(byte[] data)
		{
			List<byte[]> list = new List<byte[]>();
			for (int i = 0; i < data.Length || list.Count == 0; i += 250000)
			{
				int num = Math.Min(250000, data.Length - i);
				byte[] array = new byte[num];
				Buffer.BlockCopy(data, i, array, 0, num);
				list.Add(array);
			}
			return list;
		}
	}
	internal sealed class ChunkAssembler
	{
		private int _transferId = -1;

		private byte[][] _chunks;

		private int _received;

		public byte[] Add(int transferId, int index, int count, byte[] data)
		{
			if (count <= 0 || count > 64 || index < 0 || index >= count)
			{
				throw new InvalidDataException("bad chunk header");
			}
			if (data == null || data.Length > 250000)
			{
				throw new InvalidDataException("bad chunk size");
			}
			if (transferId != _transferId || _chunks == null || _chunks.Length != count)
			{
				_transferId = transferId;
				_chunks = new byte[count][];
				_received = 0;
			}
			if (_chunks[index] == null)
			{
				_received++;
			}
			_chunks[index] = data;
			if (_received < count)
			{
				return null;
			}
			int num = 0;
			byte[][] chunks = _chunks;
			foreach (byte[] array in chunks)
			{
				num += array.Length;
			}
			byte[] array2 = new byte[num];
			int num2 = 0;
			chunks = _chunks;
			foreach (byte[] array3 in chunks)
			{
				Buffer.BlockCopy(array3, 0, array2, num2, array3.Length);
				num2 += array3.Length;
			}
			_chunks = null;
			return array2;
		}
	}
	internal static class ServerSide
	{
		private sealed class ClientInfo
		{
			public int Protocol;

			public string Version;
		}

		private static readonly ConditionalWeakTable<ZRpc, ClientInfo> Clients = new ConditionalWeakTable<ZRpc, ClientInfo>();

		private static byte[] _payload;

		private static List<byte[]> _chunks;

		private static int _transferId;

		private static FileSystemWatcher _watcher;

		private static readonly object WatcherLock = new object();

		private static readonly HashSet<string> ChangedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

		private static DateTime _lastChange;

		public static void RegisterPeer(ZNetPeer peer)
		{
			peer.m_rpc.Register<ZPackage>("ServerConfigSync_Hello", (Action<ZRpc, ZPackage>)OnHello);
		}

		private static void OnHello(ZRpc rpc, ZPackage pkg)
		{
			ClientInfo clientInfo = new ClientInfo
			{
				Protocol = pkg.ReadInt(),
				Version = pkg.ReadString()
			};
			Clients.Remove(rpc);
			Clients.Add(rpc, clientInfo);
			Plugin.Log.LogInfo((object)$"Client {Describe(rpc)} has ServerConfigSync {clientInfo.Version} (protocol {clientInfo.Protocol})");
			StartWatcher();
		}

		public static bool AllowPeer(ZRpc rpc)
		{
			if (!Plugin.RequireClientMod.Value)
			{
				return true;
			}
			if (Clients.TryGetValue(rpc, out var value) && value.Protocol == 1)
			{
				return true;
			}
			Plugin.Log.LogWarning((object)("Rejecting " + Describe(rpc) + ": ServerConfigSync is required on this server " + ((value == null) ? "and the client does not have it." : $"but the client uses protocol {value.Protocol}.")));
			rpc.Invoke("Error", new object[1] { 3 });
			return false;
		}

		public static void OnPeerInfo(ZRpc rpc)
		{
			ZNetPeer val = FindPeer(rpc);
			if (val != null && val.IsReady())
			{
				if (!Clients.TryGetValue(rpc, out var value))
				{
					Plugin.Log.LogInfo((object)(val.m_playerName + " does not have ServerConfigSync, nothing sent."));
				}
				else if (value.Protocol != 1)
				{
					Plugin.Log.LogWarning((object)$"{val.m_playerName} uses ServerConfigSync protocol {value.Protocol}, this server uses {1}; nothing sent.");
				}
				else if (Plugin.ServerEnabled.Value)
				{
					Rebuild();
					Send(rpc);
					Plugin.Log.LogInfo((object)$"Sent settings to {val.m_playerName} ({_payload.Length} bytes, {_chunks.Count} part(s)).");
				}
			}
		}

		public static bool Rebuild()
		{
			Snapshot snapshot = SnapshotBuilder.Build();
			byte[] array = Protocol.Serialize(snapshot);
			if (_payload != null && _payload.SequenceEqual(array))
			{
				return false;
			}
			_payload = array;
			_chunks = Protocol.Split(array);
			_transferId++;
			foreach (string item in snapshot.Summary)
			{
				Plugin.Log.LogInfo((object)item);
			}
			return true;
		}

		private static void Send(ZRpc rpc)
		{
			//IL_0004: Unknown result type (might be due to invalid IL or missing references)
			//IL_000a: Expected O, but got Unknown
			for (int i = 0; i < _chunks.Count; i++)
			{
				ZPackage val = new ZPackage();
				val.Write(1);
				val.Write(_transferId);
				val.Write(i);
				val.Write(_chunks.Count);
				val.Write(_chunks[i].Length);
				val.Write(_chunks[i]);
				rpc.Invoke("ServerConfigSync_Data", new object[1] { val });
			}
		}

		private static ZNetPeer FindPeer(ZRpc rpc)
		{
			ZNet instance = ZNet.instance;
			if (instance == null)
			{
				return null;
			}
			return ((IEnumerable<ZNetPeer>)instance.GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer p) => p.m_rpc == rpc));
		}

		private static string Describe(ZRpc rpc)
		{
			try
			{
				return rpc.GetSocket().GetHostName();
			}
			catch
			{
				return "?";
			}
		}

		private static void StartWatcher()
		{
			if (_watcher != null || !Plugin.LiveUpdates.Value)
			{
				return;
			}
			try
			{
				_watcher = new FileSystemWatcher(Paths.ConfigPath, "*.cfg")
				{
					IncludeSubdirectories = false,
					NotifyFilter = (NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite)
				};
				_watcher.Changed += OnFileEvent;
				_watcher.Created += OnFileEvent;
				_watcher.Renamed += OnFileEvent;
				_watcher.EnableRaisingEvents = true;
				Plugin.Log.LogInfo((object)("Watching " + Paths.ConfigPath + " for config changes."));
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Live updates disabled, cannot watch the config folder: " + ex.Message));
				_watcher = null;
			}
		}

		public static void StopWatcher()
		{
			if (_watcher != null)
			{
				_watcher.EnableRaisingEvents = false;
				_watcher.Dispose();
				_watcher = null;
			}
		}

		public static void Reset()
		{
			StopWatcher();
			_payload = null;
			_chunks = null;
		}

		private static void OnFileEvent(object sender, FileSystemEventArgs e)
		{
			lock (WatcherLock)
			{
				ChangedFiles.Add(Path.GetFullPath(e.FullPath));
				_lastChange = DateTime.UtcNow;
			}
		}

		public static void Update()
		{
			List<string> collection;
			lock (WatcherLock)
			{
				if (ChangedFiles.Count == 0 || DateTime.UtcNow - _lastChange < TimeSpan.FromSeconds(2.0))
				{
					return;
				}
				collection = ChangedFiles.ToList();
				ChangedFiles.Clear();
			}
			if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer())
			{
				return;
			}
			HashSet<string> hashSet = new HashSet<string>(collection, StringComparer.OrdinalIgnoreCase);
			HashSet<ConfigFile> hashSet2 = new HashSet<ConfigFile>();
			foreach (PluginInfo value2 in Chainloader.PluginInfos.Values)
			{
				object obj;
				if (value2 == null)
				{
					obj = null;
				}
				else
				{
					BaseUnityPlugin instance = value2.Instance;
					obj = ((instance != null) ? instance.Config : null);
				}
				ConfigFile val = (ConfigFile)obj;
				if (val == null || hashSet2.Contains(val) || Plugin.IsCoreConfig(val))
				{
					continue;
				}
				string fullPath;
				try
				{
					fullPath = Path.GetFullPath(val.ConfigFilePath);
				}
				catch
				{
					continue;
				}
				if (hashSet.Contains(fullPath) && File.Exists(fullPath))
				{
					try
					{
						val.Reload();
						hashSet2.Add(val);
						Plugin.Log.LogInfo((object)("Reloaded " + Path.GetFileName(fullPath)));
					}
					catch (Exception ex)
					{
						Plugin.Log.LogWarning((object)("Could not reload " + Path.GetFileName(fullPath) + ": " + ex.Message));
					}
				}
			}
			if (hashSet2.Count == 0 || !Rebuild() || !Plugin.ServerEnabled.Value)
			{
				return;
			}
			int num = 0;
			foreach (ZNetPeer peer in ZNet.instance.GetPeers())
			{
				if (peer.IsReady() && Clients.TryGetValue(peer.m_rpc, out var value) && value.Protocol == 1)
				{
					Send(peer.m_rpc);
					num++;
				}
			}
			if (num > 0)
			{
				Plugin.Log.LogInfo((object)$"Pushed updated settings to {num} player(s).");
			}
		}
	}
}