Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of ServerConfigSync v0.1.3
ServerConfigSync.dll
Decompiled a week agousing 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)."); } } } }