using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Threading; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using TMPro; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: AssemblyTitle("Conditional Config Sync")] [assembly: AssemblyDescription("Shared config synchronization and server policy library for Valheim mods.")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("shudnal")] [assembly: AssemblyProduct("Conditional Config Sync")] [assembly: AssemblyCopyright("Copyright © shudnal 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("566458c7-717b-4583-af37-a69e4cd02966")] [assembly: AssemblyFileVersion("1.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyMetadata("RepositoryUrl", "https://github.com/shudnal/ConditionalConfigSync")] [assembly: AssemblyMetadata("License", "Unlicense")] [assembly: InternalsVisibleTo("ConditionalConfigSync.Plugin")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [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 ConditionalConfigSync { [Description("Registers and synchronizes config entries and runtime values for one mod.")] public class ConditionalConfigSync { internal static class ConfigEntryGetSerializedValuePatch { internal static bool Prefix(ConfigEntryBase __instance, ref string __result) { OwnConfigEntryBase configData = GetConfigData(__instance); if (configData == null || IsWritableConfig(configData)) { return true; } __result = TomlTypeConverter.ConvertToString(configData.LocalBaseValue, __instance.SettingType); return false; } } internal static class ConfigEntrySetSerializedValuePatch { internal static bool Prefix(ConfigEntryBase __instance, string value) { OwnConfigEntryBase configData = GetConfigData(__instance); if (configData == null || !configData.HasLocalBaseValue) { return true; } try { configData.StoreLocalBaseValue(TomlTypeConverter.ConvertToValue(value, __instance.SettingType)); } catch (Exception ex) { LogSource.LogWarning((object)$"[ConfigFile] Config value of setting \"{__instance.Definition}\" could not be parsed and will be ignored. Reason: {ex.Message}"); } return false; } } [Description("Diagnostic logging verbosity for ConditionalConfigSync.")] public enum ConditionalConfigSyncDebugLevel { Basic = 1, Verbose, Trace } private readonly struct ReceivedConfigState { public readonly bool ServerControlled; public readonly bool Hidden; public ReceivedConfigState(bool serverControlled, bool hidden) { ServerControlled = serverControlled; Hidden = hidden; } } private class ParsedConfigs { public readonly Dictionary configValues = new Dictionary(); public readonly Dictionary customValues = new Dictionary(); public readonly Dictionary configStates = new Dictionary(); } private enum PackageEntryKind : byte { Config = 1, CustomValue, ServerVersion, LockExempt, ConfigState } private class PackageEntry { public PackageEntryKind kind; public string? section; public string? key; public Type? type; public object? value; public bool serverControlled; public bool hidden; public static PackageEntry ServerVersion(string version) { return new PackageEntry { kind = PackageEntryKind.ServerVersion, value = version }; } public static PackageEntry LockExempt(bool value) { return new PackageEntry { kind = PackageEntryKind.LockExempt, value = value }; } public static PackageEntry ConfigState(ConfigEntryBase config, bool serverControlled, bool hidden) { return new PackageEntry { kind = PackageEntryKind.ConfigState, section = config.Definition.Section, key = config.Definition.Key, serverControlled = serverControlled, hidden = hidden }; } } private class InvalidDeserializationTypeException : Exception { public string expected = null; public string received = null; public string field = ""; } private enum ConfigPolicyOverride { Default, ForceServerControlled, ForceClientControlled } private sealed class SyncPolicyRecord { internal readonly string DisplayText; internal readonly string? Key; internal readonly ConfigPolicyOverride PolicyOverride; internal readonly string? Error; internal SyncPolicyRecord(string displayText, string? key, ConfigPolicyOverride policyOverride, string? error) { DisplayText = displayText; Key = key; PolicyOverride = policyOverride; Error = error; } } private sealed class HiddenPolicyRecord { internal readonly string DisplayText; internal readonly string? Key; internal readonly string? Error; internal HiddenPolicyRecord(string displayText, string? key, string? error) { DisplayText = displayText; Key = key; Error = error; } } private enum PolicyTargetFailure { None, Mod, Section, Config } private sealed class PolicyTargetResolution { internal ConditionalConfigSync? ConfigSync; internal readonly List Configs = new List(); internal PolicyTargetFailure Failure; } internal static class TerminalInitPatch { internal static void Postfix() { RegisterDebugConsoleCommands(); } } internal static class ZNetUpdatePatch { internal static void Postfix() { DrainMainThreadQueue(); } } internal static class ZNetAwakePatch { internal static void Postfix(ZNet __instance) { EnsureDebugSupportInitialized(); isServer = GameReflection.IsServer(__instance); sessionActive = true; if (isServer) { EnsurePolicySupportInitialized(createIfMissing: true); } foreach (ConditionalConfigSync configSync in configSyncs) { configSync.RegisterForActiveSession(); } if (isServer) { GameReflection.StartCoroutine(WatchAdminListChanges(), __instance); } void SendAdmin(List peers, bool isAdmin) { ZPackage package = ConfigsToPackage(null, null, new PackageEntry[1] { PackageEntry.LockExempt(isAdmin) }); ConditionalConfigSync conditionalConfigSync = configSyncs.FirstOrDefault(); if (conditionalConfigSync != null) { GameReflection.StartCoroutine(conditionalConfigSync.SendZPackage(peers, package), __instance); } } IEnumerator WatchAdminListChanges() { SyncedList adminList = GameReflection.GetAdminList(__instance) ?? throw new InvalidOperationException("ZNet admin list is unavailable."); List currentList = GameReflection.GetSyncedListValues(adminList); while (true) { yield return (object)new WaitForSeconds(30f); if (!GameReflection.GetSyncedListValues(adminList).SequenceEqual(currentList)) { currentList = GameReflection.GetSyncedListValues(adminList); List peers = GameReflection.GetPeers(__instance); List adminPeer = peers.Where(IsPeerAdmin).ToList(); List nonAdminPeer = peers.Except(adminPeer).ToList(); foreach (ConditionalConfigSync sync in configSyncs) { sync.DebugLog(ConditionalConfigSyncDebugLevel.Basic, "Admin", $"Admin list changed, admins={adminPeer.Count}, nonAdmins={nonAdminPeer.Count}"); } SendAdmin(nonAdminPeer, isAdmin: false); SendAdmin(adminPeer, isAdmin: true); } } } } } internal static class ZNetOnNewConnectionPatch { internal static void Postfix(ZNet __instance, ZNetPeer peer) { if (GameReflection.IsServer(__instance)) { return; } foreach (ConditionalConfigSync configSync in configSyncs) { configSync.RegisterClientRpcHandler(peer); } } } internal static class ZNetShutdownPatch { internal static void Postfix() { ConditionalConfigSync[] array = configSyncs.ToArray(); ProcessingServerUpdate = true; lockExempt = false; try { ConditionalConfigSync[] array2 = array; foreach (ConditionalConfigSync conditionalConfigSync in array2) { try { conditionalConfigSync.DebugLog(ConditionalConfigSyncDebugLevel.Basic, "Shutdown", "Reset local values and source-of-truth state"); conditionalConfigSync.ResetConfigsFromServer(); conditionalConfigSync.IsSourceOfTruth = true; conditionalConfigSync.InitialSyncDone = false; conditionalConfigSync.pendingConfigBroadcasts.Clear(); conditionalConfigSync.pendingCustomValueBroadcasts.Clear(); conditionalConfigSync.pendingSequencedCustomValuePackages.Clear(); string[] array3 = conditionalConfigSync.configValueCache.Keys.ToArray(); foreach (string cacheKey in array3) { conditionalConfigSync.RemoveFragmentAssembly(cacheKey); } } catch (Exception arg) { conditionalConfigSync.DebugWarning("Shutdown", $"Failed to reset one synchronization instance; continuing. Error: {arg}"); } } } finally { ProcessingServerUpdate = false; } ConditionalConfigSync[] array4 = array; foreach (ConditionalConfigSync conditionalConfigSync2 in array4) { conditionalConfigSync2.InvokeEventHandlers(conditionalConfigSync2.ServerConnectionReset, "ServerConnectionReset"); } ResetNetworkSessionState(); isServer = false; } } internal static class ZNetRpcPeerInfoSyncPatch { internal class BufferingSocket : ZPlayFabSocket, ISocket { public volatile bool finished = false; public volatile int versionMatchQueued = -1; public readonly List Package = new List(); public readonly ISocket Original; internal BufferingSocket(ISocket original) { Original = original; } public bool IsConnected() { return GameReflection.SocketIsConnected(Original); } public ZPackage Recv() { return GameReflection.SocketRecv(Original); } public int GetSendQueueSize() { return GameReflection.SocketGetSendQueueSize(Original); } public int GetCurrentSendRate() { return GameReflection.SocketGetCurrentSendRate(Original); } public bool IsHost() { return GameReflection.SocketIsHost(Original); } public void Dispose() { GameReflection.SocketDispose(Original); } public bool GotNewData() { return GameReflection.SocketGotNewData(Original); } public void Close() { GameReflection.SocketClose(Original); } public string GetEndPointString() { return GameReflection.SocketGetEndPointString(Original); } public void GetAndResetStats(out int totalSent, out int totalRecv) { GameReflection.SocketGetAndResetStats(Original, out totalSent, out totalRecv); } public void GetConnectionQuality(out float localQuality, out float remoteQuality, out int ping, out float outByteSec, out float inByteSec) { GameReflection.SocketGetConnectionQuality(Original, out localQuality, out remoteQuality, out ping, out outByteSec, out inByteSec); } public ISocket Accept() { return GameReflection.SocketAccept(Original); } public int GetHostPort() { return GameReflection.SocketGetHostPort(Original); } public bool Flush() { return GameReflection.SocketFlush(Original); } public string GetHostName() { return GameReflection.SocketGetHostName(Original); } public void VersionMatch() { if (finished) { GameReflection.SocketVersionMatch(Original); } else { versionMatchQueued = Package.Count; } } public void Send(ZPackage pkg) { int position = GameReflection.PackageGetPos(pkg); GameReflection.PackageSetPos(pkg, 0); int num = GameReflection.PackageReadInt(pkg); if ((num == GameReflection.StableHash("PeerInfo") || num == GameReflection.StableHash("RoutedRPC") || num == GameReflection.StableHash("ZDOData")) && !finished) { ZPackage val = GameReflection.NewPackage(GameReflection.PackageGetArray(pkg)); GameReflection.PackageSetPos(val, position); Package.Add(val); } else { GameReflection.PackageSetPos(pkg, position); GameReflection.SocketSend(Original, pkg); } } } internal static void Prefix(ref BufferingSocket? __state, ZNet __instance, ZRpc rpc) { if (!GameReflection.IsServer(__instance)) { return; } BufferingSocket bufferingSocket = new BufferingSocket(GameReflection.GetRpcSocket(rpc) ?? throw new InvalidOperationException("RPC socket is unavailable.")); GameReflection.SetRpcSocket(rpc, (ISocket)(object)bufferingSocket); ZNetPeer peer = GameReflection.GetPeer(rpc, __instance); if (peer != null && !object.Equals(GameReflection.GetOnlineBackend(), (object)(OnlineBackendType)0)) { ISocket? peerSocket = GameReflection.GetPeerSocket(peer); ZPlayFabSocket val = (ZPlayFabSocket)(object)((peerSocket is ZPlayFabSocket) ? peerSocket : null); if (val != null) { GameReflection.SetPlayFabRemotePlayerId((ZPlayFabSocket)(object)bufferingSocket, GameReflection.GetPlayFabRemotePlayerId(val)); } GameReflection.SetPeerSocket(peer, (ISocket)(object)bufferingSocket); } __state = bufferingSocket; } internal static void Postfix(BufferingSocket? __state, ZNet __instance, ZRpc rpc) { ZNetPeer peer; if (GameReflection.IsServer(__instance) && __state != null) { peer = GameReflection.GetPeer(rpc, __instance); if (peer == null) { SendBufferedData(); } else { GameReflection.StartCoroutine(sendAsync(), __instance); } } void SendBufferedData() { BufferingSocket bufferingSocket2; if (GameReflection.GetRpcSocket(rpc) is BufferingSocket bufferingSocket) { GameReflection.SetRpcSocket(rpc, bufferingSocket.Original); ZNetPeer peer2 = GameReflection.GetPeer(rpc, __instance); if (peer2 != null) { GameReflection.SetPeerSocket(peer2, bufferingSocket.Original); } bufferingSocket2 = bufferingSocket; } else { bufferingSocket2 = __state; } if (!bufferingSocket2.finished) { bufferingSocket2.finished = true; for (int i = 0; i < bufferingSocket2.Package.Count; i++) { if (i == bufferingSocket2.versionMatchQueued) { GameReflection.SocketVersionMatch(bufferingSocket2.Original); } GameReflection.SocketSend(bufferingSocket2.Original, bufferingSocket2.Package[i]); } if (bufferingSocket2.Package.Count == bufferingSocket2.versionMatchQueued) { GameReflection.SocketVersionMatch(bufferingSocket2.Original); } } } IEnumerator sendAsync() { try { foreach (ConditionalConfigSync configSync in configSyncs) { ZPackage package = configSync.CreateFullSyncPackage(peer); configSync.DebugLog(ConditionalConfigSyncDebugLevel.Basic, "InitialSync", $"Sending full sync to {FormatPeer(peer)}, admin={IsPeerAdmin(peer)}, configs={configSync.allConfigs.Count}, custom={configSync.allCustomValues.Count}, size={GameReflection.PackageSize(package)}"); yield return GameReflection.StartCoroutine(configSync.SendZPackage(new List { peer }, package), __instance); } } finally { SendBufferedData(); } } } } [Description("True while an incoming synchronization package is being applied. Usually diagnostic only.")] public static bool ProcessingServerUpdate = false; [Description("Stable unique synchronization identifier, normally the owning mod GUID.")] public readonly string Name; [Description("Human-readable mod name used in logs.")] public string? DisplayName; [Description("Current version of the owning mod used for peer compatibility checks.")] public string? CurrentVersion; [Description("Oldest compatible remote mod version accepted by this instance.")] public string? MinimumRequiredVersion; [Description("Allows unlocked clients to publish changes, matching ServerSync unlocked-client behavior.")] public bool AllowClientConfigUpdatesWhenUnlocked = true; private bool? forceConfigLocking; private bool isSourceOfTruth = true; private static readonly HashSet configSyncs = new HashSet(); private readonly HashSet allConfigs = new HashSet(); private HashSet allCustomValues = new HashSet(); private static bool isServer; private static bool lockExempt = false; private OwnConfigEntryBase? lockedConfig = null; private bool? lastNotifiedLockState; [Description("Enables shared ConditionalConfigSync diagnostic logging programmatically.")] public static bool DebugLoggingEnabled = false; [Description("Selects diagnostic logging verbosity.")] public static ConditionalConfigSyncDebugLevel DebugLoggingLevel = ConditionalConfigSyncDebugLevel.Basic; [Description("Optional case-insensitive filter matched against mod sync names and display names.")] public static string DebugLoggingFilter = ""; private static bool debugConfigEnabled; private static ConditionalConfigSyncDebugLevel debugConfigLevel = ConditionalConfigSyncDebugLevel.Basic; private static string debugConfigFilter = ""; private static bool debugSupportInitialized; private static bool debugConfigReloadScheduled; private static bool debugCommandsRegistered; private static FileSystemWatcher? debugConfigWatcher; private static readonly object debugSupportLock = new object(); private const string SyncPolicyFileName = "ConditionalConfigSync.SyncPolicy.cfg"; private const string HiddenConfigsFileName = "ConditionalConfigSync.HiddenConfigs.cfg"; private const string PolicyDumpFileName = "ConditionalConfigSync.PolicyDump.txt"; private static readonly object policyLock = new object(); private static bool policySupportInitialized; private static bool policyReloadScheduled; private static long policyReadGeneration; private static long policyAppliedGeneration; private static FileSystemWatcher? syncPolicyWatcher; private static FileSystemWatcher? hiddenConfigsWatcher; private static Dictionary syncPolicy = new Dictionary(StringComparer.OrdinalIgnoreCase); private static HashSet hiddenConfigPolicy = new HashSet(StringComparer.OrdinalIgnoreCase); private static ManualLogSource? logSource; private static readonly object runtimeLock = new object(); private static bool runtimeInitialized; private static int mainThreadId; private static readonly object mainThreadQueueLock = new object(); private static readonly Queue mainThreadQueue = new Queue(); private const string ConfigDirectoryName = "shudnal.ConditionalConfigSync"; private const string DebugConfigFileName = "ConditionalConfigSync.Debug.cfg"; private static readonly object configDirectoryLock = new object(); private static bool configDirectoryInitialized; private const string ResyncRpcSuffix = " ConditionalConfigSync Resync"; private const int StableReadAttempts = 5; private const int StableReadDelayMilliseconds = 50; private readonly VersionCheck versionCheck; private bool serverRpcsRegistered; private readonly HashSet registeredClientRpcs = new HashSet(); private readonly HashSet lateRegisteredConfigs = new HashSet(); private readonly HashSet lateRegisteredCustomValues = new HashSet(); private bool lateRegistrationSyncScheduled; private static bool sessionActive; private static Harmony? runtimeHarmony; private static AssemblyLoadEventHandler? assemblyLoadHandler; private short sendCount; private short processingCount; private bool lastHandledPackageWasFull; private readonly HashSet pendingConfigBroadcasts = new HashSet(); private readonly HashSet pendingCustomValueBroadcasts = new HashSet(); private readonly Queue pendingSequencedCustomValuePackages = new Queue(); private bool flushingPendingBroadcasts; private readonly HashSet configsBeingApplied = new HashSet(); private readonly HashSet customValuesBeingApplied = new HashSet(); private const byte PARTIAL_CONFIGS = 1; private const byte FRAGMENTED_CONFIG = 2; private const byte COMPRESSED_CONFIG = 4; private const byte V2_PACKAGE = 8; private const int packageSliceSize = 250000; private const int maximumSendQueueSize = 20000; private const int compressMinSize = 10000; private const int maxPackageEntries = 8192; private const int maxPayloadSize = 20971520; private const int maxFragments = 128; private const int maxFragmentSize = 300000; private const int maxFragmentAssembliesPerSender = 4; private const int maxFragmentCacheBytesPerSender = 20971520; private const int maxFragmentCacheBytesGlobal = 67108864; private const int maxPendingSequencedUpdates = 100; private readonly Dictionary> configValueCache = new Dictionary>(); private readonly Dictionary configValueCacheExpectedFragments = new Dictionary(); private readonly Dictionary configValueCacheBytes = new Dictionary(); private readonly Dictionary configValueCacheSenders = new Dictionary(); private readonly List> cacheExpirations = new List>(); private static long packageCounter = 0L; [Description("Whether the owning mod must be installed and compatible on the remote peer.")] public bool ModRequired { get; set; } = false; [Description("Whether non-admin clients are currently blocked from publishing server-controlled settings.")] public bool IsLocked { get { bool? flag = forceConfigLocking; bool num; if (!flag.HasValue) { if (lockedConfig == null) { goto IL_0052; } num = ((IConvertible)lockedConfig.BaseConfig.BoxedValue).ToInt32(CultureInfo.InvariantCulture) != 0; } else { num = flag == true; } if (!num) { goto IL_0052; } int result = ((!lockExempt) ? 1 : 0); goto IL_0053; IL_0052: result = 0; goto IL_0053; IL_0053: return (byte)result != 0; } set { bool isLocked = IsLocked; forceConfigLocking = value; if (isLocked != IsLocked) { ServerLockedSettingChanged(); } } } [Description("Whether the local side is the source of truth or has a server admin exemption.")] public bool IsAdmin => lockExempt || isSourceOfTruth; [Description("Whether the local side currently owns and publishes synchronized values.")] public bool IsSourceOfTruth { get { return isSourceOfTruth; } private set { if (value != isSourceOfTruth) { isSourceOfTruth = value; InvokeEventHandlers(this.SourceOfTruthChanged, value, "SourceOfTruthChanged"); } } } [Description("Whether initial server synchronization has completed for this mod instance.")] public bool InitialSyncDone { get; private set; } = false; private static bool IsDebugActive => DebugLoggingEnabled || debugConfigEnabled; private static ConditionalConfigSyncDebugLevel EffectiveDebugLevel { get { int num = 0; if (DebugLoggingEnabled) { num = Math.Max(num, (int)DebugLoggingLevel); } if (debugConfigEnabled) { num = Math.Max(num, (int)debugConfigLevel); } return (num <= 0) ? ConditionalConfigSyncDebugLevel.Basic : ((ConditionalConfigSyncDebugLevel)num); } } private static string EffectiveDebugFilter => (!string.IsNullOrWhiteSpace(DebugLoggingFilter)) ? DebugLoggingFilter : debugConfigFilter; private static string SyncPolicyPath => Path.Combine(ConfigDirectoryPath, "ConditionalConfigSync.SyncPolicy.cfg"); private static string HiddenConfigsPath => Path.Combine(ConfigDirectoryPath, "ConditionalConfigSync.HiddenConfigs.cfg"); private static ManualLogSource LogSource => logSource ?? (logSource = Logger.CreateLogSource("ConditionalConfigSync")); private static string ConfigDirectoryPath => Path.Combine(Paths.ConfigPath, "shudnal.ConditionalConfigSync"); private static string DebugConfigPath => Path.Combine(ConfigDirectoryPath, "ConditionalConfigSync.Debug.cfg"); private static bool IsMainThread => mainThreadId == 0 || Thread.CurrentThread.ManagedThreadId == mainThreadId; private bool IsSending => sendCount > 0; private bool IsProcessing => processingCount > 0; private bool ShouldDeferOutgoingBroadcasts => ProcessingServerUpdate || IsProcessing || IsSending || flushingPendingBroadcasts; [Description("Raised when this process changes between authoritative and client-replica roles.")] public event Action? SourceOfTruthChanged; [Description("Raised after the first complete server synchronization has been applied on a client.")] public event Action? InitialSyncCompleted; [Description("Raised after server state is cleared and local fallback values are restored.")] public event Action? ServerConnectionReset; [Description("Raised for any effective policy-state transition of a registered config entry.")] public event EventHandler? PolicyStateChanged; [Description("Raised when a config entry changes between server-controlled and client-controlled.")] public event Action? ServerControlledChanged; [Description("Raised when a config entry changes between hidden and visible.")] public event Action? HiddenStateChanged; [Description("Raised when the effective configuration lock changes.")] public event Action? LockStateChanged; [Description("Raised when synchronization is rejected for permissions, safety limits, malformed data, or an exception.")] public event EventHandler? SyncRejected; private event Action? LockedConfigChanged; [Description("Creates one synchronization instance. Pass a stable mod GUID as the name.")] public ConditionalConfigSync(string name) { EnsureRuntimeReady(); Name = name; configSyncs.Add(this); versionCheck = new VersionCheck(this); if (sessionActive) { QueueMainThread(RegisterForActiveSession); } } [Description("Registers a policy-controlled config that is server-controlled by default.")] public SyncedConfigEntry AddConfigEntry(ConfigEntry configEntry) { return AddConfigEntry(configEntry, ConfigSyncMode.Conditional, serverControlledByDefault: true); } [Description("Registers a Conditional config with an explicit server/client default.")] public SyncedConfigEntry AddConfigEntry(ConfigEntry configEntry, bool synchronizedSetting) { return AddConfigEntry(configEntry, ConfigSyncMode.Conditional, synchronizedSetting); } [Description("Registers a config with an explicit synchronization mode.")] public SyncedConfigEntry AddConfigEntry(ConfigEntry configEntry, ConfigSyncMode syncMode) { return AddConfigEntry(configEntry, syncMode, serverControlledByDefault: true); } [Description("Registers a fully configured config without a temporary synchronization state.")] public SyncedConfigEntry AddConfigEntry(ConfigEntry configEntry, ConfigSyncMode syncMode, bool serverControlledByDefault) { if (!Enum.IsDefined(typeof(ConfigSyncMode), syncMode)) { throw new ArgumentOutOfRangeException("syncMode", syncMode, "Unknown config synchronization mode."); } bool flag = syncMode switch { ConfigSyncMode.AlwaysServerControlled => true, ConfigSyncMode.AlwaysClientControlled => false, _ => serverControlledByDefault, }; if (GetConfigData((ConfigEntryBase)(object)configEntry) is SyncedConfigEntry result) { return result; } ConfigDefinition definition = ((ConfigEntryBase)configEntry).Definition; if (allConfigs.Any((OwnConfigEntryBase config) => config.BaseConfig.Definition.Equals(definition))) { throw new InvalidOperationException("Config entry '" + definition.Section + " -> " + definition.Key + "' is already registered in sync '" + Name + "'."); } SyncedConfigEntry syncedEntry = new SyncedConfigEntry(configEntry) { SyncMode = syncMode, SynchronizedConfig = flag }; AccessTools.DeclaredField(typeof(ConfigDescription), "k__BackingField").SetValue(((ConfigEntryBase)configEntry).Description, new object[1] { new ConfigurationManagerAttributes() }.Concat(((ConfigEntryBase)configEntry).Description.Tags ?? Array.Empty()).Concat(new SyncedConfigEntry[1] { syncedEntry }).ToArray()); configEntry.SettingChanged += delegate { OnConfigEntryChanged((ConfigEntryBase)(object)configEntry, syncedEntry); }; allConfigs.Add(syncedEntry); bool flag2 = IsSourceOfTruth && isServer && GameReflection.HasZNet && policySupportInitialized; syncedEntry.IsServerControlled = (flag2 ? ComputeServerControlled(syncedEntry) : GetDefaultServerControlled(syncedEntry)); syncedEntry.IsHidden = flag2 && ComputeHidden(syncedEntry); syncedEntry.PolicyStateInitialized = flag2; ConfigurationManagerAttributes configAttribute = GetConfigAttribute((ConfigEntryBase)(object)configEntry); configAttribute.ReadOnly = !IsWritableConfig(syncedEntry); configAttribute.Browsable = !syncedEntry.IsHidden; DebugLog(ConditionalConfigSyncDebugLevel.Trace, "Register", string.Format("Added config {0}/{1}, type={2}, mode={3}, default={4}", definition.Section, definition.Key, ((ConfigEntryBase)configEntry).SettingType.Name, syncMode, flag ? "ServerControlled" : "ClientControlled")); ScheduleLateRegistrationSync(syncedEntry); return syncedEntry; } [Description("Binds and registers a Conditional config with an explicit server/client default.")] public SyncedConfigEntry AddConfigEntry(ConfigFile configFile, string section, string key, T defaultValue, string description, bool synchronizedSetting) { return AddConfigEntry(configFile.Bind(section, key, defaultValue, description), ConfigSyncMode.Conditional, synchronizedSetting); } [Description("Binds and registers a config in one call using a text description.")] public SyncedConfigEntry AddConfigEntry(ConfigFile configFile, string section, string key, T defaultValue, string description, ConfigSyncMode syncMode = ConfigSyncMode.Conditional, bool serverControlledByDefault = true) { return AddConfigEntry(configFile.Bind(section, key, defaultValue, description), syncMode, serverControlledByDefault); } [Description("Binds and registers a Conditional ConfigDescription entry with an explicit server/client default.")] public SyncedConfigEntry AddConfigEntry(ConfigFile configFile, string section, string key, T defaultValue, ConfigDescription description, bool synchronizedSetting) { return AddConfigEntry(configFile.Bind(section, key, defaultValue, description), ConfigSyncMode.Conditional, synchronizedSetting); } [Description("Binds and registers a config in one call using a ConfigDescription.")] public SyncedConfigEntry AddConfigEntry(ConfigFile configFile, string section, string key, T defaultValue, ConfigDescription description, ConfigSyncMode syncMode = ConfigSyncMode.Conditional, bool serverControlledByDefault = true) { return AddConfigEntry(configFile.Bind(section, key, defaultValue, description), syncMode, serverControlledByDefault); } [Description("Binds and registers a Conditional ConfigDefinition entry with a text description and explicit default.")] public SyncedConfigEntry AddConfigEntry(ConfigFile configFile, ConfigDefinition definition, T defaultValue, string description, bool synchronizedSetting) { return AddConfigEntry(configFile.Bind(definition.Section, definition.Key, defaultValue, description), ConfigSyncMode.Conditional, synchronizedSetting); } [Description("Binds and registers a ConfigDefinition entry with a text description.")] public SyncedConfigEntry AddConfigEntry(ConfigFile configFile, ConfigDefinition definition, T defaultValue, string description, ConfigSyncMode syncMode = ConfigSyncMode.Conditional, bool serverControlledByDefault = true) { return AddConfigEntry(configFile.Bind(definition.Section, definition.Key, defaultValue, description), syncMode, serverControlledByDefault); } [Description("Binds and registers a Conditional ConfigDefinition entry with an explicit server/client default.")] public SyncedConfigEntry AddConfigEntry(ConfigFile configFile, ConfigDefinition definition, T defaultValue, ConfigDescription description, bool synchronizedSetting) { return AddConfigEntry(configFile.Bind(definition.Section, definition.Key, defaultValue, description), ConfigSyncMode.Conditional, synchronizedSetting); } [Description("Binds and registers a config in one call using a ConfigDefinition.")] public SyncedConfigEntry AddConfigEntry(ConfigFile configFile, ConfigDefinition definition, T defaultValue, ConfigDescription description, ConfigSyncMode syncMode = ConfigSyncMode.Conditional, bool serverControlledByDefault = true) { return AddConfigEntry(configFile.Bind(definition.Section, definition.Key, defaultValue, description), syncMode, serverControlledByDefault); } [Description("Registers the protected server-controlled lock entry. Non-admin clients can never change it.")] public SyncedConfigEntry AddLockingConfigEntry(ConfigEntry lockingConfig) where T : IConvertible { if (lockedConfig != null) { throw new Exception("Cannot initialize locking ConfigEntry twice"); } lockedConfig = AddConfigEntry(lockingConfig, ConfigSyncMode.AlwaysServerControlled); lockedConfig.IsServerControlled = true; lockedConfig.PolicyStateInitialized = false; lockingConfig.SettingChanged += delegate { this.LockedConfigChanged?.Invoke(); }; LockedConfigChanged -= ServerLockedSettingChanged; LockedConfigChanged += ServerLockedSettingChanged; return (SyncedConfigEntry)lockedConfig; } internal void AddCustomValue(CustomSyncedValueBase customValue) { if (allCustomValues.Select((CustomSyncedValueBase v) => v.Identifier).Concat(new string[2] { "serverversion", "lockexempt" }).Contains(customValue.Identifier)) { throw new Exception("Cannot have multiple settings with the same name or with a reserved name (serverversion, lockexempt)"); } allCustomValues.Add(customValue); allCustomValues = new HashSet(from v in allCustomValues orderby v.Priority descending, v.RegistrationIndex select v); customValue.ValueChanged += delegate { OnCustomValueChanged(customValue); }; DebugLog(ConditionalConfigSyncDebugLevel.Trace, "Register", $"Added custom value {customValue.Identifier}, type={customValue.Type.Name}, priority={customValue.Priority}, sequenced={customValue.PreserveUpdateSequence}"); ScheduleLateRegistrationSync(null, customValue); } private static bool IsWritableConfig(OwnConfigEntryBase config) { ConditionalConfigSync conditionalConfigSync = configSyncs.FirstOrDefault((ConditionalConfigSync cs) => cs.allConfigs.Contains(config)); if (conditionalConfigSync == null) { return true; } return conditionalConfigSync.IsSourceOfTruth || !config.IsServerControlled || !config.HasLocalBaseValue || (!conditionalConfigSync.IsLocked && (config != conditionalConfigSync.lockedConfig || lockExempt)); } private void ServerLockedSettingChanged() { RaiseLockStateChangedIfNeeded(); DebugLog(ConditionalConfigSyncDebugLevel.Verbose, "Lock", $"Re-evaluating read-only state: locked={IsLocked}, admin={IsAdmin}, sourceOfTruth={IsSourceOfTruth}"); foreach (OwnConfigEntryBase allConfig in allConfigs) { ConfigurationManagerAttributes configAttribute = GetConfigAttribute(allConfig.BaseConfig); configAttribute.ReadOnly = !IsWritableConfig(allConfig); configAttribute.Browsable = !allConfig.IsHidden; } } private void ResetConfigsFromServer() { DebugLog(ConditionalConfigSyncDebugLevel.Verbose, "Reset", "Restoring local values after server sync/session end"); Dictionary dictionary = new Dictionary(); List list = new List(); try { foreach (OwnConfigEntryBase item in allConfigs.Where((OwnConfigEntryBase config) => config.HasLocalBaseValue)) { ConfigFile configFile = item.BaseConfig.ConfigFile; if (!dictionary.ContainsKey(configFile)) { dictionary[configFile] = configFile.SaveOnConfigSet; configFile.SaveOnConfigSet = false; } configsBeingApplied.Add(item.BaseConfig); try { item.BaseConfig.BoxedValue = item.LocalBaseValue; item.ClearLocalBaseValue(); item.ClearServerValue(); } catch (Exception arg) { ConfigDefinition definition = item.BaseConfig.Definition; DebugWarning("Reset", $"Failed to restore local config {definition.Section} -> {definition.Key}; continuing. Error: {arg}"); } finally { configsBeingApplied.Remove(item.BaseConfig); } } } finally { foreach (KeyValuePair item2 in dictionary) { item2.Key.SaveOnConfigSet = item2.Value; } } foreach (OwnConfigEntryBase allConfig in allConfigs) { bool isServerControlled = allConfig.IsServerControlled; bool isHidden = allConfig.IsHidden; bool defaultServerControlled = GetDefaultServerControlled(allConfig); allConfig.ClearServerValue(); allConfig.IsServerControlled = defaultServerControlled; allConfig.IsHidden = false; allConfig.PolicyStateInitialized = false; if (isServerControlled != defaultServerControlled || isHidden) { list.Add(new PolicyStateChangedEventArgs(allConfig, isServerControlled, defaultServerControlled, isHidden, newHidden: false, "server connection reset")); } } foreach (CustomSyncedValueBase item3 in allCustomValues.Where((CustomSyncedValueBase config) => config.HasLocalBaseValue)) { customValuesBeingApplied.Add(item3); try { item3.BoxedValue = item3.LocalBaseValue; item3.ClearLocalBaseValue(); } catch (Exception arg2) { DebugWarning("Reset", $"Failed to restore local custom value '{item3.Identifier}'; continuing. Error: {arg2}"); } finally { customValuesBeingApplied.Remove(item3); } } ServerLockedSettingChanged(); foreach (PolicyStateChangedEventArgs item4 in list) { RaisePolicyStateEvents(item4); } } private static OwnConfigEntryBase? GetConfigData(ConfigEntryBase config) { return config.Description.Tags?.OfType().SingleOrDefault(); } [Description("Returns the synchronization wrapper attached to a registered BepInEx config entry.")] public static SyncedConfigEntry? ConfigData(ConfigEntry config) { return ((ConfigEntryBase)config).Description.Tags?.OfType>().SingleOrDefault(); } private static T GetConfigAttribute(ConfigEntryBase config) { return config.Description.Tags.OfType().First(); } private static void EnsureDebugSupportInitialized() { lock (debugSupportLock) { if (!debugSupportInitialized) { debugSupportInitialized = true; LoadDebugConfig(createIfMissing: true, quiet: true); StartDebugConfigWatcher(); } } } private static void StartDebugConfigWatcher() { try { EnsureConfigDirectory(); debugConfigWatcher = new FileSystemWatcher(ConfigDirectoryPath, "ConditionalConfigSync.Debug.cfg") { NotifyFilter = (NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite | NotifyFilters.CreationTime), EnableRaisingEvents = true }; debugConfigWatcher.Changed += delegate { ScheduleDebugConfigReload(); }; debugConfigWatcher.Created += delegate { ScheduleDebugConfigReload(); }; debugConfigWatcher.Renamed += delegate { ScheduleDebugConfigReload(); }; debugConfigWatcher.Deleted += delegate { ScheduleDebugConfigReload(); }; } catch (Exception ex) { LogSource.LogWarning((object)("[DebugConfig] Could not start watcher for ConditionalConfigSync.Debug.cfg: " + ex.Message)); } } private static void ScheduleDebugConfigReload() { lock (debugSupportLock) { if (debugConfigReloadScheduled) { return; } debugConfigReloadScheduled = true; } ThreadPool.QueueUserWorkItem(delegate { Thread.Sleep(500); lock (debugSupportLock) { debugConfigReloadScheduled = false; } LoadDebugConfig(createIfMissing: false, quiet: false); }); } private static void LoadDebugConfig(bool createIfMissing, bool quiet) { try { EnsureConfigDirectory(); if (!File.Exists(DebugConfigPath)) { if (createIfMissing) { File.WriteAllText(DebugConfigPath, "[Debug]\n# Local debug logging for ConditionalConfigSync. This file is not synchronized.\nEnabled = false\nLevel = Basic\nFilter = \n"); return; } debugConfigEnabled = false; debugConfigLevel = ConditionalConfigSyncDebugLevel.Basic; debugConfigFilter = ""; if (!quiet && DebugLoggingEnabled) { LogSource.LogInfo((object)"[DebugConfig] ConditionalConfigSync.Debug.cfg was deleted; file-based debug settings were reset."); } return; } Dictionary dictionary = ReadSimpleDebugConfig(DebugConfigPath); bool flag = ReadBool(dictionary, "Enabled", fallback: false); string value; ConditionalConfigSyncDebugLevel conditionalConfigSyncDebugLevel = ReadDebugLevel(dictionary.TryGetValue("Level", out value) ? value : null, ConditionalConfigSyncDebugLevel.Basic); string value2; string text = (dictionary.TryGetValue("Filter", out value2) ? value2.Trim() : ""); debugConfigEnabled = flag; debugConfigLevel = conditionalConfigSyncDebugLevel; debugConfigFilter = text; if (!quiet && (flag || DebugLoggingEnabled)) { LogSource.LogInfo((object)string.Format("[DebugConfig] Reloaded {0}: enabled={1}, level={2}, filter='{3}'", "ConditionalConfigSync.Debug.cfg", flag, conditionalConfigSyncDebugLevel, text)); } } catch (Exception ex) { LogSource.LogWarning((object)("[DebugConfig] Failed to read ConditionalConfigSync.Debug.cfg: " + ex.Message)); } } private static Dictionary ReadSimpleDebugConfig(string path) { Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); string text = ""; string[] array = ReadAllLinesStable(path); foreach (string text2 in array) { string text3 = text2.Trim(); if (text3.Length == 0 || text3.StartsWith("#") || text3.StartsWith(";")) { continue; } if (text3.StartsWith("[") && text3.EndsWith("]")) { text = text3.Substring(1, text3.Length - 2).Trim(); } else if (text.Equals("Debug", StringComparison.OrdinalIgnoreCase)) { int num = text3.IndexOf('='); if (num >= 0) { dictionary[text3.Substring(0, num).Trim()] = text3.Substring(num + 1).Trim(); } } } return dictionary; } private static bool ReadBool(Dictionary values, string key, bool fallback) { if (!values.TryGetValue(key, out string value)) { return fallback; } if (bool.TryParse(value, out var result)) { return result; } if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result2)) { return result2 != 0; } return fallback; } private static ConditionalConfigSyncDebugLevel ReadDebugLevel(string? raw, ConditionalConfigSyncDebugLevel fallback) { ConditionalConfigSyncDebugLevel result; return Enum.TryParse(raw, ignoreCase: true, out result) ? result : fallback; } private bool ShouldDebugLog(ConditionalConfigSyncDebugLevel level) { if (!IsDebugActive || level > EffectiveDebugLevel) { return false; } string effectiveDebugFilter = EffectiveDebugFilter; if (string.IsNullOrWhiteSpace(effectiveDebugFilter)) { return true; } string text = DisplayName ?? Name; return text.IndexOf(effectiveDebugFilter, StringComparison.OrdinalIgnoreCase) >= 0 || Name.IndexOf(effectiveDebugFilter, StringComparison.OrdinalIgnoreCase) >= 0; } private void DebugLog(ConditionalConfigSyncDebugLevel level, string area, string message) { if (ShouldDebugLog(level)) { LogSource.LogInfo((object)("[" + GetDebugModName() + "][" + GetDebugSide() + "][" + area + "] " + message)); } } private void InfoLog(string message) { LogSource.LogInfo((object)("[" + GetDebugModName() + "][" + GetDebugSide() + "] " + message)); } private void DebugWarning(string area, string message) { LogSource.LogWarning((object)("[" + GetDebugModName() + "][" + GetDebugSide() + "][" + area + "] " + message)); } private string GetDebugModName() { return (!string.IsNullOrWhiteSpace(DisplayName)) ? DisplayName : Name; } private static string GetDebugSide() { if (!GameReflection.HasZNet) { return "NoZNet"; } return GameReflection.IsServer() ? "Server" : "Client"; } private void InvokeEventHandlers(Action? handlers, string eventName) { if (handlers == null) { return; } Delegate[] invocationList = handlers.GetInvocationList(); foreach (Delegate obj in invocationList) { try { ((Action)obj)(); } catch (Exception arg) { DebugWarning("Event", $"Subscriber of {eventName} failed; continuing. Error: {arg}"); } } } private void InvokeEventHandlers(Action? handlers, T value, string eventName) { if (handlers == null) { return; } Delegate[] invocationList = handlers.GetInvocationList(); foreach (Delegate obj in invocationList) { try { ((Action)obj)(value); } catch (Exception arg) { DebugWarning("Event", $"Subscriber of {eventName} failed; continuing. Error: {arg}"); } } } private void InvokeEventHandlers(Action? handlers, T1 value1, T2 value2, string eventName) { if (handlers == null) { return; } Delegate[] invocationList = handlers.GetInvocationList(); foreach (Delegate obj in invocationList) { try { ((Action)obj)(value1, value2); } catch (Exception arg) { DebugWarning("Event", $"Subscriber of {eventName} failed; continuing. Error: {arg}"); } } } private void InvokeEventHandlers(EventHandler? handlers, TEventArgs args, string eventName) where TEventArgs : EventArgs { if (handlers == null) { return; } Delegate[] invocationList = handlers.GetInvocationList(); foreach (Delegate obj in invocationList) { try { ((EventHandler)obj)(this, args); } catch (Exception arg) { DebugWarning("Event", $"Subscriber of {eventName} failed; continuing. Error: {arg}"); } } } private void RaiseInitialSyncCompletedIfNeeded() { if (!InitialSyncDone) { InitialSyncDone = true; InvokeEventHandlers(this.InitialSyncCompleted, "InitialSyncCompleted"); } } private void RaisePolicyStateEvents(OwnConfigEntryBase config, bool oldServerControlled, bool newServerControlled, bool oldHidden, bool newHidden, string source) { if (oldServerControlled != newServerControlled || oldHidden != newHidden) { RaisePolicyStateEvents(new PolicyStateChangedEventArgs(config, oldServerControlled, newServerControlled, oldHidden, newHidden, source)); } } private void RaisePolicyStateEvents(PolicyStateChangedEventArgs args) { InvokeEventHandlers(this.PolicyStateChanged, args, "PolicyStateChanged"); if (args.OldServerControlled != args.NewServerControlled) { InvokeEventHandlers(this.ServerControlledChanged, args.Config, args.NewServerControlled, "ServerControlledChanged"); } if (args.OldHidden != args.NewHidden) { InvokeEventHandlers(this.HiddenStateChanged, args.Config, args.NewHidden, "HiddenStateChanged"); } } private void RaiseLockStateChangedIfNeeded() { bool isLocked = IsLocked; if (lastNotifiedLockState != isLocked) { lastNotifiedLockState = isLocked; InvokeEventHandlers(this.LockStateChanged, isLocked, "LockStateChanged"); } } private void RejectSync(string reason, long? senderUid, bool incoming, Exception? exception = null) { DebugWarning("Reject", (exception == null) ? reason : $"{reason}{Environment.NewLine}{exception}"); InvokeEventHandlers(this.SyncRejected, new SyncRejectedEventArgs(reason, senderUid, incoming, exception), "SyncRejected"); } private static void RegisterDebugConsoleCommands() { if (debugCommandsRegistered) { return; } try { MethodInfo handler = AccessTools.DeclaredMethod(typeof(ConditionalConfigSync), "HandleDebugConsoleCommand", (Type[])null, (Type[])null) ?? throw new MissingMethodException(typeof(ConditionalConfigSync).FullName, "HandleDebugConsoleCommand"); MethodInfo handler2 = AccessTools.DeclaredMethod(typeof(ConditionalConfigSync), "HandlePolicyConsoleCommand", (Type[])null, (Type[])null) ?? throw new MissingMethodException(typeof(ConditionalConfigSync).FullName, "HandlePolicyConsoleCommand"); GameReflection.RegisterConsoleCommand("conditionalconfigsync_debug", "ConditionalConfigSync debug logging: status/on/off/level/filter/clearfilter", handler); GameReflection.RegisterConsoleCommand("conditionalconfigsync_debug_server", "ConditionalConfigSync server debug logging: status/on/off/level/filter/clearfilter", handler, isCheat: false, isNetwork: true, onlyServer: true, isSecret: false, allowInDevBuild: false, remoteCommand: true, onlyAdmin: true); RegisterPolicyConsoleCommand("conditionalconfigsync_status", "Show registered mods, config modes, and effective policy counts", handler2); RegisterPolicyConsoleCommand("conditionalconfigsync_policy_reload", "Reload and apply ConditionalConfigSync policy files", handler2); RegisterPolicyConsoleCommand("conditionalconfigsync_policy_validate", "Validate policy syntax, identifiers, and ignored rules", handler2); RegisterPolicyConsoleCommand("conditionalconfigsync_policy_dump", "Write a copy-ready policy identifier file", handler2); debugCommandsRegistered = true; } catch (Exception ex) { LogSource.LogWarning((object)("[Console] Could not register ConditionalConfigSync commands: " + ex.Message)); } } private static void RegisterPolicyConsoleCommand(string command, string description, MethodInfo handler) { GameReflection.RegisterConsoleCommand(command, description, handler, isCheat: false, isNetwork: true, onlyServer: true, isSecret: false, allowInDevBuild: false, remoteCommand: true, onlyAdmin: true); } private static object HandleDebugConsoleCommand(ConsoleEventArgs args) { EnsureDebugSupportInitialized(); bool flag = GameReflection.ConsoleArgsLength(args) <= 1; bool? flag2 = null; ConditionalConfigSyncDebugLevel? conditionalConfigSyncDebugLevel = null; string text = null; for (int i = 1; i < GameReflection.ConsoleArgsLength(args); i++) { string text2 = GameReflection.ConsoleArg(args, i).Trim(); switch (text2.ToLowerInvariant()) { case "status": flag = true; break; case "on": case "enable": case "enabled": flag2 = true; flag = false; break; case "off": case "disable": case "disabled": flag2 = false; flag = false; break; case "basic": case "verbose": case "trace": conditionalConfigSyncDebugLevel = ReadDebugLevel(text2, DebugLoggingLevel); flag = false; break; case "level": if (i + 1 < GameReflection.ConsoleArgsLength(args)) { conditionalConfigSyncDebugLevel = ReadDebugLevel(GameReflection.ConsoleArg(args, ++i), DebugLoggingLevel); flag = false; } break; case "filter": if (i + 1 < GameReflection.ConsoleArgsLength(args)) { text = GameReflection.ConsoleArg(args, ++i); flag = false; } break; case "clearfilter": case "nofilter": text = ""; flag = false; break; default: return "Unknown argument '" + text2 + "'"; } } if (flag) { WriteDebugStatus(args); return true; } DebugLoggingEnabled = flag2 ?? DebugLoggingEnabled; DebugLoggingLevel = conditionalConfigSyncDebugLevel ?? DebugLoggingLevel; DebugLoggingFilter = text ?? DebugLoggingFilter; AddConsoleLine(args, $"ConditionalConfigSync debug: enabled={DebugLoggingEnabled}, level={DebugLoggingLevel}, filter='{DebugLoggingFilter}'"); return true; } private static void WriteDebugStatus(ConsoleEventArgs args) { AddConsoleLine(args, $"ConditionalConfigSync debug: enabled={DebugLoggingEnabled}, configEnabled={debugConfigEnabled}, level={EffectiveDebugLevel}, filter='{EffectiveDebugFilter}'"); } private static void AddConsoleLine(ConsoleEventArgs args, string text) { GameReflection.ConsoleAddString(args, text); LogSource.LogInfo((object)("[Console] " + text)); } internal static void VersionDebugLog(string area, string modName, string message) { EnsureDebugSupportInitialized(); if (IsDebugActive && ConditionalConfigSyncDebugLevel.Basic <= EffectiveDebugLevel) { string effectiveDebugFilter = EffectiveDebugFilter; if (string.IsNullOrWhiteSpace(effectiveDebugFilter) || modName.IndexOf(effectiveDebugFilter, StringComparison.OrdinalIgnoreCase) >= 0) { LogSource.LogInfo((object)("[" + modName + "][" + GetDebugSide() + "][" + area + "] " + message)); } } } internal static void VersionInfoLog(string area, string modName, string message) { LogSource.LogInfo((object)("[" + modName + "][" + GetDebugSide() + "][" + area + "] " + message)); } internal static void VersionWarningLog(string area, string modName, string message) { LogSource.LogWarning((object)("[" + modName + "][" + GetDebugSide() + "][" + area + "] " + message)); } private void ApplyParsedConfigs(ParsedConfigs configs, bool receivedFromServer) { DebugLog(ConditionalConfigSyncDebugLevel.Verbose, "Apply", $"Applying configs={configs.configValues.Count}, custom={configs.customValues.Count}, states={configs.configStates.Count}"); Dictionary saveOnConfigSet = new Dictionary(); List list = new List(); try { if (receivedFromServer) { foreach (KeyValuePair configState in configs.configStates) { bool isServerControlled = configState.Key.IsServerControlled; bool isHidden = configState.Key.IsHidden; configState.Key.IsServerControlled = configState.Value.ServerControlled; configState.Key.IsHidden = configState.Value.Hidden; configState.Key.PolicyStateInitialized = true; if (isServerControlled != configState.Value.ServerControlled || isHidden != configState.Value.Hidden) { list.Add(new PolicyStateChangedEventArgs(configState.Key, isServerControlled, configState.Value.ServerControlled, isHidden, configState.Value.Hidden, "server package")); } } } foreach (KeyValuePair configValue in configs.configValues) { if (receivedFromServer) { configValue.Key.StoreServerValue(configValue.Value); if (!configValue.Key.IsServerControlled) { RestoreLocalValueIfNeeded(configValue.Key); continue; } if (!configValue.Key.HasLocalBaseValue) { configValue.Key.StoreLocalBaseValue(configValue.Key.BaseConfig.BoxedValue); } } SetConfigValue(configValue.Key, configValue.Value); } if (receivedFromServer) { foreach (KeyValuePair configState2 in configs.configStates) { if (!configs.configValues.ContainsKey(configState2.Key)) { if (configState2.Key.IsServerControlled) { ApplyServerValueIfAvailable(configState2.Key); } else { RestoreLocalValueIfNeeded(configState2.Key); } } } } } finally { foreach (KeyValuePair item in saveOnConfigSet) { item.Key.SaveOnConfigSet = item.Value; try { item.Key.Save(); } catch (Exception arg) { DebugWarning("Apply", $"Failed to save config file '{item.Key.ConfigFilePath}' after applying synchronized values: {arg}"); } } } foreach (KeyValuePair customValue in configs.customValues) { if (!isServer && !customValue.Key.HasLocalBaseValue) { customValue.Key.StoreLocalBaseValue(customValue.Key.BoxedValue); } customValuesBeingApplied.Add(customValue.Key); try { customValue.Key.BoxedValue = customValue.Value; } catch (Exception arg2) { DebugWarning("Apply", $"Failed to apply custom value '{customValue.Key.Identifier}'; continuing with the remaining entries. Error: {arg2}"); } finally { customValuesBeingApplied.Remove(customValue.Key); } } if (receivedFromServer) { foreach (ConditionalConfigSync configSync in configSyncs) { configSync.ServerLockedSettingChanged(); } } foreach (PolicyStateChangedEventArgs item2 in list) { RaisePolicyStateEvents(item2); } void ApplyServerValueIfAvailable(OwnConfigEntryBase config) { if (config.HasServerValue) { if (!config.HasLocalBaseValue) { config.StoreLocalBaseValue(config.BaseConfig.BoxedValue); } SetConfigValue(config, config.ServerValue); } } void DisableSaveOnConfigSet(ConfigFile configFile) { if (!saveOnConfigSet.ContainsKey(configFile)) { saveOnConfigSet[configFile] = configFile.SaveOnConfigSet; configFile.SaveOnConfigSet = false; } } void RestoreLocalValueIfNeeded(OwnConfigEntryBase config) { if (config.HasLocalBaseValue && SetConfigValue(config, config.LocalBaseValue)) { config.ClearLocalBaseValue(); } } bool SetConfigValue(OwnConfigEntryBase config, object? value) { ConfigFile configFile = config.BaseConfig.ConfigFile; DisableSaveOnConfigSet(configFile); configsBeingApplied.Add(config.BaseConfig); try { config.BaseConfig.BoxedValue = value; return true; } catch (Exception arg3) { ConfigDefinition definition = config.BaseConfig.Definition; DebugWarning("Apply", $"Failed to apply config {definition.Section} -> {definition.Key}; continuing with the remaining entries. Error: {arg3}"); return false; } finally { configsBeingApplied.Remove(config.BaseConfig); } } } private static string GetSingleEntryReceiveDetails(ParsedConfigs configs) { List list = new List(); if (configs.configValues.Count == 1) { ConfigDefinition definition = configs.configValues.Keys.First().BaseConfig.Definition; list.Add("config=" + definition.Section + " -> " + definition.Key); } if (configs.customValues.Count == 1) { list.Add("custom=" + configs.customValues.Keys.First().Identifier); } return (list.Count == 0) ? "" : (" (" + string.Join(", ", list) + ")"); } private ParsedConfigs ReadConfigsFromPackage(ZPackage package, bool receivedFromServer) { ParsedConfigs parsedConfigs = new ParsedConfigs(); Dictionary dictionary = allConfigs.ToDictionary((OwnConfigEntryBase c) => c.BaseConfig.Definition.Section + "\n" + c.BaseConfig.Definition.Key, (OwnConfigEntryBase c) => c); Dictionary dictionary2 = allCustomValues.ToDictionary((CustomSyncedValueBase c) => c.Identifier, (CustomSyncedValueBase c) => c); int num = GameReflection.PackageReadInt(package); if (num < 0 || num > 8192) { throw new InvalidDataException($"Invalid config entry count {num}"); } for (int num2 = 0; num2 < num; num2++) { PackageEntryKind packageEntryKind = (PackageEntryKind)GameReflection.PackageReadByte(package); byte[] array = GameReflection.PackageReadByteArray(package, 20971520); if (array.Length > 20971520) { DebugWarning("Read", $"Skipping too large entry payload ({array.Length})"); continue; } ZPackage package2 = GameReflection.NewPackage(array); switch (packageEntryKind) { case PackageEntryKind.Config: { string text = GameReflection.PackageReadString(package2); string text2 = GameReflection.PackageReadString(package2); string text3 = GameReflection.PackageReadString(package2); if (dictionary.TryGetValue(text + "\n" + text2, out var value)) { try { parsedConfigs.configValues[value] = TomlTypeConverter.ConvertToValue(text3, value.BaseConfig.SettingType); } catch (Exception ex) { DebugWarning("Read", $"Config value of setting \"{value.BaseConfig.Definition}\" could not be parsed and will be ignored. Reason: {ex.Message}"); } } else { DebugWarning("Read", "Received unknown config entry " + text + "/" + text2 + ". This may happen if client and server versions of the mod do not match."); } break; } case PackageEntryKind.ConfigState: { string text5 = GameReflection.PackageReadString(package2); string text6 = GameReflection.PackageReadString(package2); bool serverControlled = GameReflection.PackageReadBool(package2); bool hidden = GameReflection.PackageReadBool(package2); if (dictionary.TryGetValue(text5 + "\n" + text6, out var value2)) { parsedConfigs.configStates[value2] = new ReceivedConfigState(serverControlled, hidden); break; } DebugWarning("Read", "Received unknown config state " + text5 + "/" + text6 + ". This may happen if client and server versions of the mod do not match."); break; } case PackageEntryKind.CustomValue: { string text7 = GameReflection.PackageReadString(package2); string text8 = GameReflection.PackageReadString(package2); if (!dictionary2.TryGetValue(text7, out var value3)) { DebugWarning("Read", "Received unknown custom synced value " + text7 + ". This may happen if client and server versions of the mod do not match."); break; } string zPackageTypeString = GetZPackageTypeString(value3.Type); if (text8 != zPackageTypeString) { DebugWarning("Read", "Got unexpected type " + text8 + " for custom synced value " + text7 + ", expecting " + zPackageTypeString); break; } try { parsedConfigs.customValues[value3] = ReadCustomValueFromPackage(package2, value3.Type); } catch (InvalidDeserializationTypeException ex2) { DebugWarning("Read", "Got unexpected struct internal type " + ex2.received + " for field " + ex2.field + " struct " + text8 + " for custom synced value " + text7 + ", expecting " + ex2.expected); } catch (Exception arg) { DebugWarning("Read", $"Could not deserialize custom synced value {text7}: {arg}"); } break; } case PackageEntryKind.ServerVersion: { string text4 = GameReflection.PackageReadString(package2); if (receivedFromServer && text4 != CurrentVersion) { DebugWarning("Version", "Received server version is not equal: server version=" + text4 + ", local version=" + (CurrentVersion ?? "unknown")); } break; } case PackageEntryKind.LockExempt: { bool flag = GameReflection.PackageReadBool(package2); if (receivedFromServer) { lockExempt = flag; } break; } default: DebugWarning("Read", $"Received unknown config entry kind {(byte)packageEntryKind}"); break; } } return parsedConfigs; } private static ZPackage ConfigsToPackage(IEnumerable? configs = null, IEnumerable? customValues = null, IEnumerable? packageEntries = null, bool partial = true, bool includeConfigValues = true, bool includeAllProvidedConfigStates = false) { List list = new List(); if (configs != null) { foreach (ConfigEntryBase config in configs) { OwnConfigEntryBase configData = GetConfigData(config); if (configData != null && (includeAllProvidedConfigStates || ShouldIncludeConfigInPackage(configData)) && !list.Contains(configData)) { list.Add(configData); } } } List list2 = (includeConfigValues ? list.Where(GetPackageServerControlled).ToList() : new List()); List list3 = customValues?.ToList() ?? new List(); List list4 = packageEntries?.ToList() ?? new List(); ZPackage val = GameReflection.NewPackage(); GameReflection.PackageWrite(val, (byte)((partial ? 1 : 0) | 8)); GameReflection.PackageWrite(val, list2.Count + list.Count + list3.Count + list4.Count); foreach (PackageEntry item in list4) { AddEntryToPackage(val, item); } foreach (OwnConfigEntryBase item2 in list) { AddEntryToPackage(val, PackageEntry.ConfigState(item2.BaseConfig, GetPackageServerControlled(item2), GetPackageHidden(item2))); } foreach (CustomSyncedValueBase item3 in list3) { AddEntryToPackage(val, new PackageEntry { kind = PackageEntryKind.CustomValue, key = item3.Identifier, type = item3.Type, value = item3.BoxedValue }); } foreach (OwnConfigEntryBase item4 in list2) { AddEntryToPackage(val, new PackageEntry { kind = PackageEntryKind.Config, section = item4.BaseConfig.Definition.Section, key = item4.BaseConfig.Definition.Key, type = item4.BaseConfig.SettingType, value = item4.BaseConfig.BoxedValue }); } return val; } private static void AddEntryToPackage(ZPackage package, PackageEntry entry) { ZPackage package2 = GameReflection.NewPackage(); switch (entry.kind) { case PackageEntryKind.Config: GameReflection.PackageWrite(package2, entry.section); GameReflection.PackageWrite(package2, entry.key); GameReflection.PackageWrite(package2, TomlTypeConverter.ConvertToString(entry.value, entry.type)); break; case PackageEntryKind.CustomValue: GameReflection.PackageWrite(package2, entry.key); GameReflection.PackageWrite(package2, GetZPackageTypeString(entry.type)); WriteCustomValueToPackage(package2, entry.type, entry.value); break; case PackageEntryKind.ServerVersion: GameReflection.PackageWrite(package2, entry.value?.ToString() ?? ""); break; case PackageEntryKind.LockExempt: { object value = entry.value; bool flag = default(bool); int num; if (value is bool) { flag = (bool)value; num = 1; } else { num = 0; } GameReflection.PackageWrite(package2, (byte)((uint)num & (flag ? 1u : 0u)) != 0); break; } case PackageEntryKind.ConfigState: GameReflection.PackageWrite(package2, entry.section); GameReflection.PackageWrite(package2, entry.key); GameReflection.PackageWrite(package2, entry.serverControlled); GameReflection.PackageWrite(package2, entry.hidden); break; default: throw new ArgumentOutOfRangeException("kind", entry.kind, null); } GameReflection.PackageWrite(package, (byte)entry.kind); GameReflection.PackageWrite(package, GameReflection.PackageGetArray(package2)); } private static string GetZPackageTypeString(Type type) { return type.AssemblyQualifiedName; } private static void WriteCustomValueToPackage(ZPackage package, Type type, object? value) { GameReflection.PackageWrite(package, value != null); if (value != null) { WriteValueWithTypeToZPackage(package, type, value); } } private static object? ReadCustomValueFromPackage(ZPackage package, Type type) { if (!GameReflection.PackageReadBool(package)) { return null; } return ReadValueWithTypeFromZPackage(package, type); } private static void WriteValueWithTypeToZPackage(ZPackage package, Type type, object value) { Type type2 = Nullable.GetUnderlyingType(type) ?? type; if (typeof(ISerializableParameter).IsAssignableFrom(type2)) { GameReflection.SerializeParameter(value, ref package); return; } if (type2.IsEnum) { WriteValueWithTypeToZPackage(package, Enum.GetUnderlyingType(type2), ((IConvertible)value).ToType(Enum.GetUnderlyingType(type2), CultureInfo.InvariantCulture)); return; } if (value is ICollection collection && type2 != typeof(string) && type2 != typeof(List)) { GameReflection.PackageWrite(package, collection.Count); { foreach (object item in collection) { WriteValueWithTypeToZPackage(package, item.GetType(), item); } return; } } if ((object)type2 != null && type2.IsValueType && !type2.IsPrimitive) { FieldInfo[] fields = type2.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); GameReflection.PackageWrite(package, fields.Length); FieldInfo[] array = fields; foreach (FieldInfo fieldInfo in array) { GameReflection.PackageWrite(package, GetZPackageTypeString(fieldInfo.FieldType)); object value2 = fieldInfo.GetValue(value); WriteCustomValueToPackage(package, fieldInfo.FieldType, value2); } } else { GameReflection.Serialize(new object[1] { value }, ref package); } } private static object ReadValueWithTypeFromZPackage(ZPackage package, Type type) { Type type2 = Nullable.GetUnderlyingType(type) ?? type; if (typeof(ISerializableParameter).IsAssignableFrom(type2)) { object obj = Activator.CreateInstance(type2) ?? throw new MissingMethodException("Cannot create " + type2.FullName + " for ISerializableParameter deserialization"); GameReflection.DeserializeParameter(obj, ref package); return obj; } if (type2.IsEnum) { object value = ReadValueWithTypeFromZPackage(package, Enum.GetUnderlyingType(type2)); return Enum.ToObject(type2, value); } if ((object)type2 != null && type2.IsValueType && !type2.IsPrimitive) { FieldInfo[] fields = type2.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); int num = GameReflection.PackageReadInt(package); if (num != fields.Length) { throw new InvalidDeserializationTypeException { received = $"(field count: {num})", expected = $"(field count: {fields.Length})" }; } object uninitializedObject = FormatterServices.GetUninitializedObject(type2); FieldInfo[] array = fields; foreach (FieldInfo fieldInfo in array) { string text = GameReflection.PackageReadString(package); if (text != GetZPackageTypeString(fieldInfo.FieldType)) { throw new InvalidDeserializationTypeException { received = text, expected = GetZPackageTypeString(fieldInfo.FieldType), field = fieldInfo.Name }; } fieldInfo.SetValue(uninitializedObject, ReadCustomValueFromPackage(package, fieldInfo.FieldType)); } return uninitializedObject; } if (type2.IsGenericType && type2.GetGenericTypeDefinition() == typeof(Dictionary<, >)) { int num2 = GameReflection.PackageReadInt(package); IDictionary dictionary = (IDictionary)Activator.CreateInstance(type2); Type type3 = typeof(KeyValuePair<, >).MakeGenericType(type2.GenericTypeArguments); FieldInfo field = type3.GetField("key", BindingFlags.Instance | BindingFlags.NonPublic); FieldInfo field2 = type3.GetField("value", BindingFlags.Instance | BindingFlags.NonPublic); for (int j = 0; j < num2; j++) { object obj2 = ReadValueWithTypeFromZPackage(package, type3); dictionary.Add(field.GetValue(obj2), field2.GetValue(obj2)); } return dictionary; } if (type2 != typeof(List) && type2.IsGenericType) { Type type4 = typeof(ICollection<>).MakeGenericType(type2.GenericTypeArguments[0]); if ((object)type4 != null && type4.IsAssignableFrom(type2)) { int num3 = GameReflection.PackageReadInt(package); object obj3 = Activator.CreateInstance(type2); MethodInfo method = type4.GetMethod("Add"); for (int k = 0; k < num3; k++) { method.Invoke(obj3, new object[1] { ReadValueWithTypeFromZPackage(package, type2.GenericTypeArguments[0]) }); } return obj3; } } ParameterInfo parameterInfo = (ParameterInfo)FormatterServices.GetUninitializedObject(typeof(ParameterInfo)); AccessTools.DeclaredField(typeof(ParameterInfo), "ClassImpl").SetValue(parameterInfo, type2); List data = new List(); GameReflection.Deserialize(new ParameterInfo[2] { null, parameterInfo }, package, ref data); return data.First(); } private static ArraySegment GetPackageArraySegment(ZPackage package) { if (GameReflection.PackageStream(package).TryGetBuffer(out var buffer)) { return new ArraySegment(buffer.Array, buffer.Offset, GameReflection.PackageSize(package)); } byte[] array = GameReflection.PackageGetArray(package); return new ArraySegment(array, 0, array.Length); } private static void WriteByteArray(ZPackage package, ArraySegment data) { GameReflection.PackageWrite(package, data.Count); GameReflection.PackageStream(package).Write(data.Array, data.Offset, data.Count); } private static byte[] DecompressLimited(byte[] data, int maxBytes) { using MemoryStream stream = new MemoryStream(data); using DeflateStream deflateStream = new DeflateStream(stream, CompressionMode.Decompress); using MemoryStream memoryStream = new MemoryStream(); byte[] array = new byte[81920]; while (true) { int num = deflateStream.Read(array, 0, array.Length); if (num <= 0) { break; } if (memoryStream.Length + num > maxBytes) { throw new InvalidDataException($"Decompressed package exceeds limit {maxBytes}"); } memoryStream.Write(array, 0, num); } return memoryStream.ToArray(); } private static void EnsurePolicySupportInitialized(bool createIfMissing = false) { bool flag; lock (policyLock) { flag = !policySupportInitialized || syncPolicyWatcher == null || hiddenConfigsWatcher == null; policySupportInitialized = true; } LoadPolicyFiles(createIfMissing, quiet: true, "server startup"); if (flag) { StartPolicyWatchers(); } } private static void StartPolicyWatchers() { try { EnsureConfigDirectory(); if (syncPolicyWatcher == null) { syncPolicyWatcher = CreatePolicyWatcher("ConditionalConfigSync.SyncPolicy.cfg"); } if (hiddenConfigsWatcher == null) { hiddenConfigsWatcher = CreatePolicyWatcher("ConditionalConfigSync.HiddenConfigs.cfg"); } } catch (Exception ex) { LogSource.LogWarning((object)("[Policy] Could not start policy watchers: " + ex.Message)); } } private static FileSystemWatcher CreatePolicyWatcher(string fileName) { FileSystemWatcher fileSystemWatcher = new FileSystemWatcher(ConfigDirectoryPath, fileName) { NotifyFilter = (NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite | NotifyFilters.CreationTime), EnableRaisingEvents = true }; fileSystemWatcher.Changed += delegate { SchedulePolicyReload(); }; fileSystemWatcher.Created += delegate { SchedulePolicyReload(); }; fileSystemWatcher.Renamed += delegate { SchedulePolicyReload(); }; fileSystemWatcher.Deleted += delegate { SchedulePolicyReload(); }; return fileSystemWatcher; } private static void SchedulePolicyReload() { long generation; lock (policyLock) { if (policyReloadScheduled) { return; } policyReloadScheduled = true; generation = ++policyReadGeneration; } ThreadPool.QueueUserWorkItem(delegate { Thread.Sleep(500); if (!TryReadPolicyFiles(createIfMissing: false, out Dictionary newSyncPolicy, out HashSet newHiddenPolicy, out List syncRecords, out List hiddenRecords, out string error)) { lock (policyLock) { policyReloadScheduled = false; } LogSource.LogWarning((object)("[Policy] Failed to read policy files: " + error)); } else { lock (policyLock) { policyReloadScheduled = false; } EnqueueMainThread(delegate { ApplyPolicyFiles(newSyncPolicy, newHiddenPolicy, syncRecords, hiddenRecords, quiet: false, "policy file watcher", generation); }); } }); } private static bool LoadPolicyFiles(bool createIfMissing, bool quiet, string source) { if (!TryReadPolicyFiles(createIfMissing, out Dictionary newSyncPolicy, out HashSet newHiddenPolicy, out List syncRecords, out List hiddenRecords, out string error)) { LogSource.LogWarning((object)("[Policy] Failed to read policy files: " + error)); return false; } long generation = Interlocked.Increment(ref policyReadGeneration); if (IsMainThread) { ApplyPolicyFiles(newSyncPolicy, newHiddenPolicy, syncRecords, hiddenRecords, quiet, source, generation); } else { EnqueueMainThread(delegate { ApplyPolicyFiles(newSyncPolicy, newHiddenPolicy, syncRecords, hiddenRecords, quiet, source, generation); }); } return true; } private static bool TryReadPolicyFiles(bool createIfMissing, out Dictionary newSyncPolicy, out HashSet newHiddenPolicy, out List syncRecords, out List hiddenRecords, out string? error) { newSyncPolicy = new Dictionary(StringComparer.OrdinalIgnoreCase); newHiddenPolicy = new HashSet(StringComparer.OrdinalIgnoreCase); syncRecords = new List(); hiddenRecords = new List(); error = null; try { EnsureConfigDirectory(); if (createIfMissing) { EnsurePolicyFilesExist(); } newSyncPolicy = ReadSyncPolicyFile(SyncPolicyPath, out syncRecords); newHiddenPolicy = ReadHiddenConfigsFile(HiddenConfigsPath, out hiddenRecords); return true; } catch (Exception ex) { error = ex.Message; return false; } } private static void ApplyPolicyFiles(Dictionary newSyncPolicy, HashSet newHiddenPolicy, List syncRecords, List hiddenRecords, bool quiet, string source, long generation) { lock (policyLock) { if (generation < policyAppliedGeneration) { return; } policyAppliedGeneration = generation; syncPolicy = newSyncPolicy; hiddenConfigPolicy = newHiddenPolicy; } LogPolicyFileRecords(syncRecords, hiddenRecords, source); if (!quiet) { int num = newSyncPolicy.Count>((KeyValuePair kv) => kv.Value == ConfigPolicyOverride.ForceServerControlled); int num2 = newSyncPolicy.Count>((KeyValuePair kv) => kv.Value == ConfigPolicyOverride.ForceClientControlled); LogSource.LogInfo((object)$"[Policy] Reloaded: forceServer={num}, forceClient={num2}, hidden={newHiddenPolicy.Count}, source={source}"); } RefreshPolicyStatesForAll(source, !quiet); } private static void EnsurePolicyFilesExist() { if (!File.Exists(SyncPolicyPath)) { File.WriteAllText(SyncPolicyPath, "# ConditionalConfigSync sync policy. Server-side only.\n# Exact setting: + ModGuid.Section.Key or - ModGuid.Section.Key\n# Whole section: + ModGuid.Section or - ModGuid.Section\n# + forces server-controlled; - forces client-controlled.\n# Exact-setting rules take precedence over section rules.\n# Rules apply only to configs registered as Conditional.\n"); } if (!File.Exists(HiddenConfigsPath)) { File.WriteAllText(HiddenConfigsPath, "# ConditionalConfigSync hidden config policy. Server-side only.\n# Exact setting: ModGuid.Section.Key\n# Whole section: ModGuid.Section\n# Exact and section entries may be combined.\n"); } } private static Dictionary ReadSyncPolicyFile(string path, out List records) { Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); records = new List(); string[] array = ReadAllLinesStable(path, missingIsEmpty: true); foreach (string line in array) { string text = StripPolicyComment(line).Trim(); if (text.Length == 0) { continue; } char c = text[0]; if (c != '+' && c != '-') { records.Add(new SyncPolicyRecord(text, null, ConfigPolicyOverride.Default, "expected '+' or '-'")); continue; } string text2 = text.Substring(1).Trim(); if (text2.Length == 0) { records.Add(new SyncPolicyRecord(text, null, ConfigPolicyOverride.Default, "config record is empty")); continue; } ConfigPolicyOverride configPolicyOverride = ((c == '+') ? ConfigPolicyOverride.ForceServerControlled : ConfigPolicyOverride.ForceClientControlled); records.Add(new SyncPolicyRecord(text2, text2, configPolicyOverride, null)); dictionary[text2] = configPolicyOverride; } return dictionary; } private static HashSet ReadHiddenConfigsFile(string path, out List records) { HashSet hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); records = new List(); string[] array = ReadAllLinesStable(path, missingIsEmpty: true); foreach (string line in array) { string text = StripPolicyComment(line).Trim(); if (text.Length != 0) { records.Add(new HiddenPolicyRecord(text, text, null)); hashSet.Add(text); } } return hashSet; } private static string StripPolicyComment(string line) { int num = line.IndexOf('#'); int num2 = line.IndexOf(';'); int num3 = ((num < 0) ? num2 : ((num2 < 0) ? num : Math.Min(num, num2))); return (num3 < 0) ? line : line.Substring(0, num3); } private static void RefreshPolicyStatesForAll(string source, bool broadcast) { foreach (ConditionalConfigSync configSync in configSyncs) { configSync.RefreshPolicyStates(source, broadcast); } } private void RefreshPolicyStates(string source, bool broadcast) { if (!IsSourceOfTruth) { return; } List list = new List(); List list2 = new List(); foreach (OwnConfigEntryBase allConfig in allConfigs) { bool flag = !allConfig.PolicyStateInitialized; bool flag2 = (flag ? GetDefaultServerControlled(allConfig) : allConfig.IsServerControlled); bool flag3 = !flag && allConfig.IsHidden; bool isServerControlled = allConfig.IsServerControlled; bool isHidden = allConfig.IsHidden; bool flag4 = ComputeServerControlled(allConfig); bool flag5 = ComputeHidden(allConfig); allConfig.IsServerControlled = flag4; allConfig.IsHidden = flag5; allConfig.PolicyStateInitialized = true; bool flag6 = isServerControlled != flag4 || isHidden != flag5; bool flag7 = flag2 != flag4 || flag3 != flag5; if (flag6 || flag7) { list.Add(allConfig); if (flag7) { list2.Add(new PolicyStateChangedEventArgs(allConfig, flag2, flag4, flag3, flag5, source)); } } } if (list.Count > 0) { ServerLockedSettingChanged(); } foreach (PolicyStateChangedEventArgs item in list2) { RaisePolicyStateEvents(item); } if (broadcast && isServer && GameReflection.HasZNet && list.Count > 0) { StartBroadcastPackage(GameReflection.Everybody, ConfigsToPackage(list.Select((OwnConfigEntryBase c) => c.BaseConfig), null, null, partial: true, includeConfigValues: true, includeAllProvidedConfigStates: true)); } } private static void LogPolicyFileRecords(IReadOnlyList syncRecords, IReadOnlyList hiddenRecords, string source) { LogSyncPolicyRecords(syncRecords, source); LogHiddenPolicyRecords(hiddenRecords, source); } private static void LogSyncPolicyRecords(IReadOnlyList records, string source) { Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < records.Count; i++) { if (records[i].Key != null) { dictionary[records[i].Key] = i; } } for (int j = 0; j < records.Count; j++) { SyncPolicyRecord record = records[j]; string policySourceSuffix = GetPolicySourceSuffix(source); if (record.Error != null || record.Key == null) { LogSource.LogWarning((object)("[SyncPolicy] " + record.DisplayText + ": " + record.Error + policySourceSuffix)); continue; } PolicyTargetResolution policyTargetResolution = ResolvePolicyTarget(record.Key); if (LogPolicyTargetFailure(policyTargetResolution, record.DisplayText, "SyncPolicy", policySourceSuffix)) { continue; } ConditionalConfigSync configSync = policyTargetResolution.ConfigSync; string text = "[SyncPolicy][" + configSync.GetDebugModName() + "]"; string policyOverrideName = GetPolicyOverrideName(record.PolicyOverride); if (dictionary[record.Key] != j) { LogSource.LogWarning((object)(text + " " + record.DisplayText + ": overridden by a later rule" + policySourceSuffix)); continue; } string matchedKey; List list = policyTargetResolution.Configs.Where((OwnConfigEntryBase config) => configSync.GetPolicyOverride(config, out matchedKey) != ConfigPolicyOverride.Default && string.Equals(matchedKey, record.Key, StringComparison.OrdinalIgnoreCase)).ToList(); if (list.Count == 0) { LogSource.LogInfo((object)(text + " " + record.DisplayText + ": " + policyOverrideName + " has no effect on mod behavior" + policySourceSuffix)); continue; } List list2 = list.Where((OwnConfigEntryBase config) => config.SyncMode == ConfigSyncMode.Conditional).ToList(); if (list2.Count == 0) { ConfigSyncMode[] array = list.Select((OwnConfigEntryBase config) => config.SyncMode).Distinct().ToArray(); string text2 = ((array.Length == 1) ? $"ignored because its mode is {array[0]}" : "ignored because matching configs are not Conditional"); LogSource.LogInfo((object)(text + " " + record.DisplayText + ": " + text2 + policySourceSuffix)); continue; } bool targetServerControlled = record.PolicyOverride == ConfigPolicyOverride.ForceServerControlled; if (list2.Any((OwnConfigEntryBase config) => config.SynchronizedConfig != targetServerControlled)) { LogSource.LogWarning((object)(text + " " + record.DisplayText + ": " + policyOverrideName + " changes mod behavior" + policySourceSuffix)); } else { LogSource.LogInfo((object)(text + " " + record.DisplayText + ": " + policyOverrideName + " has no effect on mod behavior" + policySourceSuffix)); } } } private static void LogHiddenPolicyRecords(IReadOnlyList records, string source) { foreach (HiddenPolicyRecord record in records) { string policySourceSuffix = GetPolicySourceSuffix(source); if (record.Error != null || record.Key == null) { LogSource.LogWarning((object)("[HiddenConfigs] " + record.DisplayText + ": " + record.Error + policySourceSuffix)); continue; } PolicyTargetResolution policyTargetResolution = ResolvePolicyTarget(record.Key); if (!LogPolicyTargetFailure(policyTargetResolution, record.DisplayText, "HiddenConfigs", policySourceSuffix)) { ConditionalConfigSync configSync = policyTargetResolution.ConfigSync; LogSource.LogWarning((object)("[HiddenConfigs][" + configSync.GetDebugModName() + "] " + record.DisplayText + ": is now hidden in configuration manager" + policySourceSuffix)); } } } private static bool LogPolicyTargetFailure(PolicyTargetResolution resolution, string displayText, string area, string sourceSuffix) { if (resolution.Failure == PolicyTargetFailure.None) { return false; } if (resolution.Failure == PolicyTargetFailure.Mod) { LogSource.LogWarning((object)("[" + area + "] " + displayText + ": can not find mod by GUID" + sourceSuffix)); return true; } string debugModName = resolution.ConfigSync.GetDebugModName(); string text = ((resolution.Failure == PolicyTargetFailure.Section) ? "can not find config section" : "can not find config name"); LogSource.LogWarning((object)("[" + area + "][" + debugModName + "] " + displayText + ": " + text + sourceSuffix)); return true; } private static PolicyTargetResolution ResolvePolicyTarget(string key) { PolicyTargetResolution policyTargetResolution = new PolicyTargetResolution(); ConditionalConfigSync configSync = (from sync in configSyncs where key.StartsWith(sync.Name + ".", StringComparison.OrdinalIgnoreCase) orderby sync.Name.Length descending select sync).FirstOrDefault(); if (configSync == null) { policyTargetResolution.Failure = PolicyTargetFailure.Mod; return policyTargetResolution; } policyTargetResolution.ConfigSync = configSync; OwnConfigEntryBase ownConfigEntryBase = configSync.allConfigs.FirstOrDefault((OwnConfigEntryBase config) => string.Equals(configSync.GetPolicyKey(config), key, StringComparison.OrdinalIgnoreCase)); if (ownConfigEntryBase != null) { policyTargetResolution.Configs.Add(ownConfigEntryBase); return policyTargetResolution; } List list = configSync.allConfigs.Where((OwnConfigEntryBase config) => string.Equals(configSync.GetPolicySectionKey(config), key, StringComparison.OrdinalIgnoreCase)).ToList(); if (list.Count > 0) { policyTargetResolution.Configs.AddRange(list); return policyTargetResolution; } string remainder = key.Substring(configSync.Name.Length + 1); bool flag = configSync.allConfigs.Select((OwnConfigEntryBase config) => config.BaseConfig.Definition.Section).Distinct(StringComparer.OrdinalIgnoreCase).Any((string section) => remainder.StartsWith(section + ".", StringComparison.OrdinalIgnoreCase)); policyTargetResolution.Failure = (flag ? PolicyTargetFailure.Config : PolicyTargetFailure.Section); return policyTargetResolution; } private static string GetPolicyOverrideName(ConfigPolicyOverride policyOverride) { return (policyOverride == ConfigPolicyOverride.ForceServerControlled) ? "ForceServerControlled" : "ForceClientControlled"; } private static string GetPolicySourceSuffix(string source) { return (IsDebugActive && EffectiveDebugLevel >= ConditionalConfigSyncDebugLevel.Verbose) ? (", source=" + source) : ""; } private static List ValidatePolicyFiles(out int syncRuleCount, out int hiddenRuleCount) { List list = new List(); Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); HashSet hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); EnsureConfigDirectory(); ValidateSyncPolicyFile(dictionary, list); ValidateHiddenPolicyFile(hashSet, list); syncRuleCount = dictionary.Count; hiddenRuleCount = hashSet.Count; HashSet hashSet2 = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (ConditionalConfigSync configSync in configSyncs) { foreach (OwnConfigEntryBase allConfig in configSync.allConfigs) { hashSet2.Add(configSync.GetPolicyKey(allConfig)); hashSet2.Add(configSync.GetPolicySectionKey(allConfig)); } } foreach (string item in dictionary.Keys.Concat(hashSet).Distinct(StringComparer.OrdinalIgnoreCase)) { if (!hashSet2.Contains(item)) { list.Add("Unknown identifier: " + item); } } foreach (ConditionalConfigSync configSync2 in configSyncs) { foreach (OwnConfigEntryBase allConfig2 in configSync2.allConfigs) { string matchedKey; ConfigPolicyOverride configPolicyOverride = ResolvePolicyOverride(dictionary, configSync2.GetPolicyKey(allConfig2), configSync2.GetPolicySectionKey(allConfig2), out matchedKey); if (configPolicyOverride != ConfigPolicyOverride.Default) { if (allConfig2 == configSync2.lockedConfig && configPolicyOverride == ConfigPolicyOverride.ForceClientControlled) { list.Add("ForceClientControlled is ignored for protected locking config " + configSync2.GetPolicyKey(allConfig2) + " (matched rule: " + matchedKey + ")."); } else if (allConfig2.SyncMode != ConfigSyncMode.Conditional) { list.Add($"Rule {matchedKey} is ignored for {configSync2.GetPolicyKey(allConfig2)} because its mode is {allConfig2.SyncMode}."); } } } } return list; } private static void ValidateSyncPolicyFile(Dictionary result, List diagnostics) { if (!File.Exists(SyncPolicyPath)) { diagnostics.Add("File does not exist: " + SyncPolicyPath); return; } string[] array = ReadAllLinesStable(SyncPolicyPath); for (int i = 0; i < array.Length; i++) { string text = array[i]; string text2 = StripPolicyComment(text).Trim(); if (text2.Length == 0) { continue; } char c = text2[0]; if (c != '+' && c != '-') { diagnostics.Add(string.Format("{0}:{1}: expected '+' or '-': {2}", "ConditionalConfigSync.SyncPolicy.cfg", i + 1, text)); continue; } string text3 = text2.Substring(1).Trim(); if (text3.Length == 0) { diagnostics.Add(string.Format("{0}:{1}: identifier is empty.", "ConditionalConfigSync.SyncPolicy.cfg", i + 1)); continue; } ConfigPolicyOverride configPolicyOverride = ((c == '+') ? ConfigPolicyOverride.ForceServerControlled : ConfigPolicyOverride.ForceClientControlled); if (result.TryGetValue(text3, out var value)) { diagnostics.Add((value == configPolicyOverride) ? string.Format("{0}:{1}: duplicate rule: {2}", "ConditionalConfigSync.SyncPolicy.cfg", i + 1, text3) : string.Format("{0}:{1}: conflicting rule overrides an earlier entry: {2}", "ConditionalConfigSync.SyncPolicy.cfg", i + 1, text3)); } result[text3] = configPolicyOverride; } } private static void ValidateHiddenPolicyFile(HashSet result, List diagnostics) { if (!File.Exists(HiddenConfigsPath)) { diagnostics.Add("File does not exist: " + HiddenConfigsPath); return; } string[] array = ReadAllLinesStable(HiddenConfigsPath); for (int i = 0; i < array.Length; i++) { string text = StripPolicyComment(array[i]).Trim(); if (text.Length != 0 && !result.Add(text)) { diagnostics.Add(string.Format("{0}:{1}: duplicate rule: {2}", "ConditionalConfigSync.HiddenConfigs.cfg", i + 1, text)); } } } private static ConfigPolicyOverride ResolvePolicyOverride(IReadOnlyDictionary policy, string exactKey, string sectionKey, out string? matchedKey) { if (policy.TryGetValue(exactKey, out var value)) { matchedKey = exactKey; return value; } if (policy.TryGetValue(sectionKey, out var value2)) { matchedKey = sectionKey; return value2; } matchedKey = null; return ConfigPolicyOverride.Default; } private static string WritePolicyDumpFile() { EnsureConfigDirectory(); string text = Path.Combine(ConfigDirectoryPath, "ConditionalConfigSync.PolicyDump.txt"); List list = new List { "# ConditionalConfigSync policy identifiers", "# Copy an identifier into SyncPolicy.cfg and prefix it with '+' or '-'.", "# Copy an identifier into HiddenConfigs.cfg without a prefix.", "# To target a whole section, copy the section identifier shown after '# Section:'.", "" }; foreach (ConditionalConfigSync item in configSyncs.OrderBy((ConditionalConfigSync sync) => sync.Name, StringComparer.OrdinalIgnoreCase)) { string text2 = null; foreach (OwnConfigEntryBase item2 in item.allConfigs.OrderBy((OwnConfigEntryBase entry) => entry.BaseConfig.Definition.Section, StringComparer.OrdinalIgnoreCase).ThenBy((OwnConfigEntryBase entry) => entry.BaseConfig.Definition.Key, StringComparer.OrdinalIgnoreCase)) { string policySectionKey = item.GetPolicySectionKey(item2); if (!string.Equals(text2, policySectionKey, StringComparison.OrdinalIgnoreCase)) { if (text2 != null) { list.Add(""); } list.Add("# Section: " + policySectionKey); text2 = policySectionKey; } list.Add(item.GetPolicyKey(item2)); list.Add(string.Format("Policy: {0}; Default: {1}", item2.SyncMode, GetDefaultServerControlled(item2) ? "ServerControlled" : "ClientControlled")); } list.Add(""); } File.WriteAllLines(text, list); return text; } private static object HandlePolicyConsoleCommand(ConsoleEventArgs args) { string text = GameReflection.ConsoleArg(args, 0).Trim().ToLowerInvariant(); if (!GameReflection.HasZNet || !GameReflection.IsServer()) { return "This command is available only on the server."; } switch (text) { case "conditionalconfigsync_status": WritePolicyStatus(args); return true; case "conditionalconfigsync_policy_reload": { bool flag = LoadPolicyFiles(createIfMissing: true, quiet: false, "console command"); AddConsoleLine(args, flag ? "ConditionalConfigSync policy reloaded." : "ConditionalConfigSync policy reload failed. See the server log."); return flag; } case "conditionalconfigsync_policy_validate": { int syncRuleCount; int hiddenRuleCount; List list = ValidatePolicyFiles(out syncRuleCount, out hiddenRuleCount); AddConsoleLine(args, $"ConditionalConfigSync policy validation: syncRules={syncRuleCount}, hiddenRules={hiddenRuleCount}, issues={list.Count}"); foreach (string item in list) { AddConsoleLine(args, item); } return true; } case "conditionalconfigsync_policy_dump": try { string text2 = WritePolicyDumpFile(); AddConsoleLine(args, "ConditionalConfigSync policy dump written to: " + text2); return true; } catch (Exception ex) { LogSource.LogWarning((object)$"[Policy] Failed to write policy dump: {ex}"); return "Failed to write policy dump: " + ex.Message; } default: return "Unknown ConditionalConfigSync policy command: " + text; } } private static void WritePolicyStatus(ConsoleEventArgs args) { Dictionary dictionary; HashSet hashSet; lock (policyLock) { dictionary = new Dictionary(syncPolicy, StringComparer.OrdinalIgnoreCase); hashSet = new HashSet(hiddenConfigPolicy, StringComparer.OrdinalIgnoreCase); } AddConsoleLine(args, $"ConditionalConfigSync policy status: protocol={1}, " + $"mods={configSyncs.Count}, syncRules={dictionary.Count}, hiddenRules={hashSet.Count}, " + "syncFile='" + SyncPolicyPath + "', hiddenFile='" + HiddenConfigsPath + "'"); foreach (ConditionalConfigSync item in configSyncs.OrderBy((ConditionalConfigSync sync) => sync.Name, StringComparer.OrdinalIgnoreCase)) { int num = item.allConfigs.Count((OwnConfigEntryBase config) => config.SyncMode == ConfigSyncMode.AlwaysServerControlled); int num2 = item.allConfigs.Count((OwnConfigEntryBase config) => config.SyncMode == ConfigSyncMode.Conditional); int num3 = item.allConfigs.Count((OwnConfigEntryBase config) => config.SyncMode == ConfigSyncMode.AlwaysClientControlled); int num4 = item.allConfigs.Count((OwnConfigEntryBase config) => config.IsServerControlled); int num5 = item.allConfigs.Count((OwnConfigEntryBase config) => config.IsHidden); AddConsoleLine(args, $"{item.GetDebugModName()} ({item.Name}): configs={item.allConfigs.Count}, " + $"modes={num}/{num2}/{num3} [AlwaysServer/Conditional/AlwaysClient], " + $"effectiveServer={num4}, hidden={num5}, locked={item.IsLocked}"); } } private string GetPolicyKey(OwnConfigEntryBase config) { ConfigDefinition definition = config.BaseConfig.Definition; return Name + "." + definition.Section + "." + definition.Key; } private string GetPolicySectionKey(OwnConfigEntryBase config) { ConfigDefinition definition = config.BaseConfig.Definition; return Name + "." + definition.Section; } private static ConditionalConfigSync? GetOwningConfigSync(OwnConfigEntryBase config) { return configSyncs.FirstOrDefault((ConditionalConfigSync cs) => cs.allConfigs.Contains(config)); } private ConfigPolicyOverride GetPolicyOverride(OwnConfigEntryBase config, out string? matchedKey) { string policyKey = GetPolicyKey(config); string policySectionKey = GetPolicySectionKey(config); lock (policyLock) { if (syncPolicy.TryGetValue(policyKey, out var value)) { matchedKey = policyKey; return value; } if (syncPolicy.TryGetValue(policySectionKey, out var value2)) { matchedKey = policySectionKey; return value2; } } matchedKey = null; return ConfigPolicyOverride.Default; } private bool IsHiddenByPolicy(OwnConfigEntryBase config, out string? matchedKey) { string policyKey = GetPolicyKey(config); string policySectionKey = GetPolicySectionKey(config); lock (policyLock) { if (hiddenConfigPolicy.Contains(policyKey)) { matchedKey = policyKey; return true; } if (hiddenConfigPolicy.Contains(policySectionKey)) { matchedKey = policySectionKey; return true; } } matchedKey = null; return false; } private static bool GetDefaultServerControlled(OwnConfigEntryBase config) { return config.SyncMode switch { ConfigSyncMode.AlwaysServerControlled => true, ConfigSyncMode.AlwaysClientControlled => false, _ => config.SynchronizedConfig, }; } private bool ComputeServerControlled(OwnConfigEntryBase config) { if (config == lockedConfig || config.SyncMode == ConfigSyncMode.AlwaysServerControlled) { return true; } if (config.SyncMode == ConfigSyncMode.AlwaysClientControlled) { return false; } string matchedKey; return GetPolicyOverride(config, out matchedKey) switch { ConfigPolicyOverride.ForceServerControlled => true, ConfigPolicyOverride.ForceClientControlled => false, _ => config.SynchronizedConfig, }; } private bool ComputeHidden(OwnConfigEntryBase config) { string matchedKey; return IsHiddenByPolicy(config, out matchedKey); } private static bool GetPackageServerControlled(OwnConfigEntryBase config) { ConditionalConfigSync owningConfigSync = GetOwningConfigSync(config); return (owningConfigSync == null) ? GetDefaultServerControlled(config) : (owningConfigSync.IsSourceOfTruth ? owningConfigSync.ComputeServerControlled(config) : config.IsServerControlled); } private static bool GetPackageHidden(OwnConfigEntryBase config) { ConditionalConfigSync owningConfigSync = GetOwningConfigSync(config); return (owningConfigSync != null && owningConfigSync.IsSourceOfTruth) ? owningConfigSync.ComputeHidden(config) : config.IsHidden; } private static bool ShouldIncludeConfigInPackage(OwnConfigEntryBase config) { ConditionalConfigSync owningConfigSync = GetOwningConfigSync(config); if (owningConfigSync == null) { return GetDefaultServerControlled(config); } if (!owningConfigSync.IsSourceOfTruth) { return config.IsServerControlled || config.IsHidden || config.IsServerControlled != GetDefaultServerControlled(config); } bool flag = owningConfigSync.ComputeServerControlled(config); return flag || owningConfigSync.ComputeHidden(config) || flag != GetDefaultServerControlled(config); } private static void EnsureConfigDirectory() { lock (configDirectoryLock) { if (!configDirectoryInitialized) { Directory.CreateDirectory(ConfigDirectoryPath); configDirectoryInitialized = true; } } } private static void EnqueueMainThread(Action action) { if (action == null) { return; } if (IsMainThread) { action(); return; } lock (mainThreadQueueLock) { mainThreadQueue.Enqueue(action); } } private static void DrainMainThreadQueue() { if (mainThreadId == 0) { mainThreadId = Thread.CurrentThread.ManagedThreadId; } while (true) { Action action; lock (mainThreadQueueLock) { if (mainThreadQueue.Count == 0) { break; } action = mainThreadQueue.Dequeue(); } try { action(); } catch (Exception arg) { LogSource.LogWarning((object)$"[MainThread] Queued action failed: {arg}"); } } } internal static void InitializeRuntime() { //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Expected O, but got Unknown RuntimeGuard.ThrowIfEmbedded(); lock (runtimeLock) { if (!runtimeInitialized) { mainThreadId = Thread.CurrentThread.ManagedThreadId; EnsureDebugSupportInitialized(); try { GameReflection.ValidateBindings(); } catch (Exception arg) { LogSource.LogFatal((object)$"[Reflection] Failed to bind required Valheim runtime members: {arg}"); throw; } runtimeHarmony = new Harmony("_shudnal.ConditionalConfigSync"); ApplyRuntimePatches(runtimeHarmony); VersionCheck.ApplyRuntimePatches(runtimeHarmony); assemblyLoadHandler = delegate(object _, AssemblyLoadEventArgs args) { WarnIfEmbeddedAssembly(args.LoadedAssembly); }; AppDomain.CurrentDomain.AssemblyLoad += assemblyLoadHandler; Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { WarnIfEmbeddedAssembly(assembly); } runtimeInitialized = true; } } } private static void ApplyRuntimePatches(Harmony harmony) { harmony.Patch((MethodBase)AccessTools.DeclaredMethod(typeof(ZNet), "Awake", Type.EmptyTypes, (Type[])null), (HarmonyMethod)null, CreateHarmonyMethod(typeof(ZNetAwakePatch), "Postfix"), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)AccessTools.DeclaredMethod(typeof(ZNet), "Update", Type.EmptyTypes, (Type[])null), (HarmonyMethod)null, CreateHarmonyMethod(typeof(ZNetUpdatePatch), "Postfix"), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)AccessTools.DeclaredMethod(typeof(ZNet), "OnNewConnection", new Type[1] { typeof(ZNetPeer) }, (Type[])null), (HarmonyMethod)null, CreateHarmonyMethod(typeof(ZNetOnNewConnectionPatch), "Postfix"), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)AccessTools.DeclaredMethod(typeof(Terminal), "InitTerminal", Type.EmptyTypes, (Type[])null), (HarmonyMethod)null, CreateHarmonyMethod(typeof(TerminalInitPatch), "Postfix"), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)AccessTools.DeclaredMethod(typeof(ZNet), "Shutdown", new Type[1] { typeof(bool) }, (Type[])null), (HarmonyMethod)null, CreateHarmonyMethod(typeof(ZNetShutdownPatch), "Postfix"), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); HarmonyMethod val = CreateHarmonyMethod(typeof(ZNetRpcPeerInfoSyncPatch), "Prefix"); val.priority = 800; harmony.Patch((MethodBase)AccessTools.DeclaredMethod(typeof(ZNet), "RPC_PeerInfo", new Type[2] { typeof(ZRpc), typeof(ZPackage) }, (Type[])null), val, CreateHarmonyMethod(typeof(ZNetRpcPeerInfoSyncPatch), "Postfix"), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)AccessTools.DeclaredMethod(typeof(ConfigEntryBase), "GetSerializedValue", Type.EmptyTypes, (Type[])null), CreateHarmonyMethod(typeof(ConfigEntryGetSerializedValuePatch), "Prefix"), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)AccessTools.DeclaredMethod(typeof(ConfigEntryBase), "SetSerializedValue", new Type[1] { typeof(string) }, (Type[])null), CreateHarmonyMethod(typeof(ConfigEntrySetSerializedValuePatch), "Prefix"), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } private static HarmonyMethod CreateHarmonyMethod(Type type, string methodName) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.DeclaredMethod(type, methodName, (Type[])null, (Type[])null) ?? throw new MissingMethodException(type.FullName, methodName); return new HarmonyMethod(methodInfo); } private static void WarnIfEmbeddedAssembly(Assembly assembly) { if (!(assembly == typeof(ConditionalConfigSync).Assembly) && (!(assembly.GetType("ConditionalConfigSync.ConditionalConfigSync", throwOnError: false) == null) || !(assembly.GetType("ConditionalConfigSync.ConfigSync", throwOnError: false) == null))) { LogSource.LogError((object)("Embedded ConditionalConfigSync copy detected in assembly '" + assembly.GetName().Name + "'. Embedded copies are unsupported and will reject ConfigSync creation. Reference ConditionalConfigSync.dll and declare the BepInEx hard dependency '_shudnal.ConditionalConfigSync'.")); } } internal static void EnsureRuntimeReady() { RuntimeGuard.ThrowIfEmbedded(); if (!runtimeInitialized) { throw new InvalidOperationException("ConditionalConfigSync runtime is not initialized. Install the standalone ConditionalConfigSync mod and add the BepInEx hard dependency '{PluginInfo.PluginGuid}'."); } } [Description("Requests a complete server synchronization, primarily for late-registered configs and custom values.")] public bool RequestFullSync() { if (!sessionActive || !GameReflection.HasZNet || isServer || !GameReflection.GetPeers().Any(GameReflection.IsPeerReady)) { return false; } try { ZPackage package = GameReflection.NewPackage(); GameReflection.PackageWrite(package, 1); GameReflection.InvokeRoutedPackage(Name + " ConditionalConfigSync Resync", package); DebugLog(ConditionalConfigSyncDebugLevel.Basic, "Resync", "Requested a complete synchronization package from the server"); return true; } catch (Exception ex) { RejectSync("Failed to request complete synchronization: " + ex.Message, null, incoming: false, ex); return false; } } private static void QueueMainThread(Action action) { lock (mainThreadQueueLock) { mainThreadQueue.Enqueue(action); } } private void RegisterForActiveSession() { if (!sessionActive || !GameReflection.HasZNet) { return; } if (isServer) { RegisterServerRpcHandlers(); InitialSyncDone = true; return; } bool flag = false; foreach (ZNetPeer peer in GameReflection.GetPeers()) { RegisterClientRpcHandler(peer); flag = true; } if (!InitialSyncDone && flag) { RequestFullSync(); } } private void RegisterServerRpcHandlers() { if (!serverRpcsRegistered && GameReflection.HasZRoutedRpc) { GameReflection.RegisterRoutedPackage(Name + " ConditionalConfigSync", RPC_FromOtherClientConfigSync); GameReflection.RegisterRoutedPackage(Name + " ConditionalConfigSync Resync", RPC_RequestFullSync); serverRpcsRegistered = true; DebugLog(ConditionalConfigSyncDebugLevel.Basic, "Register", "Registered server RPCs for '" + Name + "'"); } } private void RegisterClientRpcHandler(ZNetPeer peer) { ZRpc peerRpc = GameReflection.GetPeerRpc(peer); if (registeredClientRpcs.Add(peerRpc)) { GameReflection.RegisterRpcPackage(peerRpc, Name + " ConditionalConfigSync", RPC_FromServerConfigSync); DebugLog(ConditionalConfigSyncDebugLevel.Basic, "Register", "Registered client RPC for '" + Name + "'"); } } private void RPC_RequestFullSync(long sender, ZPackage request) { if (!isServer || !sessionActive) { return; } try { int num = ((GameReflection.PackageSize(request) - GameReflection.PackageGetPos(request) >= 4) ? GameReflection.PackageReadInt(request) : 0); if (!IsProtocolCompatible(num)) { RejectSync("Rejected resync request from " + FormatClient(sender) + " using ConditionalConfigSync protocol " + ((num == 0) ? "missing" : num.ToString()) + ". " + $"Required protocol is {1}.", sender, incoming: true); return; } ZNetPeer routedPeer = GameReflection.GetRoutedPeer(sender); if (routedPeer == null || !GameReflection.IsPeerReady(routedPeer)) { RejectSync("Could not serve resync request from " + FormatClient(sender) + " because the peer is unavailable.", sender, incoming: true); return; } ZPackage package = CreateFullSyncPackage(routedPeer); DebugLog(ConditionalConfigSyncDebugLevel.Basic, "Resync", $"Sending complete resync to {FormatPeer(routedPeer)}, configs={allConfigs.Count}, custom={allCustomValues.Count}, size={GameReflection.PackageSize(package)}"); StartBroadcastPackage(new List { routedPeer }, package); } catch (Exception ex) { RejectSync("Failed to process resync request from " + FormatClient(sender) + ": " + ex.Message, sender, incoming: true, ex); } } private ZPackage CreateFullSyncPackage(ZNetPeer peer) { List list = new List(); if (CurrentVersion != null) { list.Add(PackageEntry.ServerVersion(CurrentVersion)); } list.Add(PackageEntry.LockExempt(IsPeerAdmin(peer))); return ConfigsToPackage(allConfigs.Select((OwnConfigEntryBase c) => c.BaseConfig), allCustomValues, list, partial: false); } private void ScheduleLateRegistrationSync(OwnConfigEntryBase? config = null, CustomSyncedValueBase? customValue = null) { if (sessionActive && GameReflection.HasZNet && (isServer || InitialSyncDone)) { if (config != null) { lateRegisteredConfigs.Add(config); } if (customValue != null) { lateRegisteredCustomValues.Add(customValue); } if (!lateRegistrationSyncScheduled) { lateRegistrationSyncScheduled = true; QueueMainThread(FlushLateRegistrationSync); } } } private void FlushLateRegistrationSync() { lateRegistrationSyncScheduled = false; if (!sessionActive || !GameReflection.HasZNet) { lateRegisteredConfigs.Clear(); lateRegisteredCustomValues.Clear(); } else if (isServer) { ConfigEntryBase[] array = lateRegisteredConfigs.Select((OwnConfigEntryBase config) => config.BaseConfig).ToArray(); CustomSyncedValueBase[] array2 = (from value in lateRegisteredCustomValues orderby value.Priority descending, value.RegistrationIndex select value).ToArray(); lateRegisteredConfigs.Clear(); lateRegisteredCustomValues.Clear(); if (array.Length != 0 || array2.Length != 0) { ZPackage package = ConfigsToPackage(array, array2, null, partial: true, includeConfigValues: true, includeAllProvidedConfigStates: true); DebugLog(ConditionalConfigSyncDebugLevel.Basic, "LateRegistration", $"Broadcasting late registrations: configs={array.Length}, custom={array2.Length}"); StartBroadcastPackage(GameReflection.Everybody, package); } } else { lateRegisteredConfigs.Clear(); lateRegisteredCustomValues.Clear(); RequestFullSync(); } } private void ResetSessionRegistrationState() { serverRpcsRegistered = false; registeredClientRpcs.Clear(); lateRegisteredConfigs.Clear(); lateRegisteredCustomValues.Clear(); lateRegistrationSyncScheduled = false; } private static bool IsProtocolCompatible(int remoteProtocol) { return remoteProtocol == 1; } private static string[] ReadAllLinesStable(string path, bool missingIsEmpty = false) { Exception innerException = null; for (int i = 1; i <= 5; i++) { try { if (!File.Exists(path)) { if (missingIsEmpty && i == 5) { return Array.Empty(); } innerException = new FileNotFoundException($"File is temporarily unavailable (attempt {i}/{5}).", path); throw innerException; } FileInfo fileInfo = new FileInfo(path); long length = fileInfo.Length; DateTime lastWriteTimeUtc = fileInfo.LastWriteTimeUtc; string text; using (FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) { using StreamReader streamReader = new StreamReader(stream, detectEncodingFromByteOrderMarks: true); text = streamReader.ReadToEnd(); } Thread.Sleep(50); if (!File.Exists(path)) { if (missingIsEmpty && i == 5) { return Array.Empty(); } innerException = new FileNotFoundException($"File disappeared while it was being read (attempt {i}/{5}).", path); throw innerException; } FileInfo fileInfo2 = new FileInfo(path); if (length == fileInfo2.Length && lastWriteTimeUtc == fileInfo2.LastWriteTimeUtc) { return text.Replace("\r\n", "\n").Replace('\r', '\n').Split(new char[1] { '\n' }); } innerException = new IOException($"File changed while it was being read (attempt {i}/{5})."); } catch (IOException ex) { innerException = ex; } catch (UnauthorizedAccessException ex2) { innerException = ex2; } if (i < 5) { Thread.Sleep(50 * i); } } throw new IOException($"Could not read stable contents from '{path}' after {5} attempts.", innerException); } private static void StopPolicySupport() { lock (policyLock) { syncPolicyWatcher?.Dispose(); hiddenConfigsWatcher?.Dispose(); syncPolicyWatcher = null; hiddenConfigsWatcher = null; policySupportInitialized = false; policyReloadScheduled = false; syncPolicy = new Dictionary(StringComparer.OrdinalIgnoreCase); hiddenConfigPolicy = new HashSet(StringComparer.OrdinalIgnoreCase); Interlocked.Increment(ref policyReadGeneration); policyAppliedGeneration = policyReadGeneration; } } private static void StopDebugSupport() { lock (debugSupportLock) { debugConfigWatcher?.Dispose(); debugConfigWatcher = null; debugConfigReloadScheduled = false; debugSupportInitialized = false; debugConfigEnabled = false; debugConfigLevel = ConditionalConfigSyncDebugLevel.Basic; debugConfigFilter = ""; } } private static void ResetNetworkSessionState() { sessionActive = false; foreach (ConditionalConfigSync configSync in configSyncs) { configSync.ResetSessionRegistrationState(); } VersionCheck.ResetSessionState(); StopPolicySupport(); lock (mainThreadQueueLock) { mainThreadQueue.Clear(); } } [EditorBrowsable(EditorBrowsableState.Never)] internal static void ShutdownRuntime() { lock (runtimeLock) { if (!runtimeInitialized) { return; } ResetNetworkSessionState(); StopDebugSupport(); if (assemblyLoadHandler != null) { AppDomain.CurrentDomain.AssemblyLoad -= assemblyLoadHandler; assemblyLoadHandler = null; } try { if (runtimeHarmony != null) { MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(Harmony), "UnpatchSelf", (Type[])null, (Type[])null); if (methodInfo != null) { methodInfo.Invoke(runtimeHarmony, null); } else { LogSource.LogWarning((object)"[Cleanup] Harmony.UnpatchSelf was not found; runtime patches will remain until process exit."); } } } catch (Exception ex) { LogSource.LogWarning((object)("[Cleanup] Failed to remove Harmony patches: " + ex.Message)); } runtimeHarmony = null; VersionCheck.ShutdownRuntime(); configSyncs.Clear(); runtimeInitialized = false; mainThreadId = 0; isServer = false; lockExempt = false; } } private bool ShouldBroadcastConfigChange(OwnConfigEntryBase syncedEntry) { return GetPackageServerControlled(syncedEntry) && ShouldIncludeConfigInPackage(syncedEntry); } private bool CanBroadcastFromThisSide() { return GameReflection.HasZNet && (isServer || (!IsLocked && AllowClientConfigUpdatesWhenUnlocked)); } private void OnConfigEntryChanged(ConfigEntryBase configEntry, OwnConfigEntryBase syncedEntry) { if (!ShouldBroadcastConfigChange(syncedEntry) || configsBeingApplied.Contains(configEntry) || !CanBroadcastFromThisSide()) { DebugLog(ConditionalConfigSyncDebugLevel.Trace, "ConfigChanged", $"Ignored {configEntry.Definition.Section}/{configEntry.Definition.Key}: mode={syncedEntry.SyncMode}, defaultServer={syncedEntry.SynchronizedConfig}, serverControlled={syncedEntry.IsServerControlled}, applying={configsBeingApplied.Contains(configEntry)}, canBroadcast={CanBroadcastFromThisSide()}"); } else if (ShouldDeferOutgoingBroadcasts) { pendingConfigBroadcasts.Add(configEntry); DebugLog(ConditionalConfigSyncDebugLevel.Verbose, "ConfigChanged", $"Queued {configEntry.Definition.Section}/{configEntry.Definition.Key}, processing={IsProcessing}, sending={IsSending}"); } else { DebugLog(ConditionalConfigSyncDebugLevel.Verbose, "ConfigChanged", "Broadcast " + configEntry.Definition.Section + "/" + configEntry.Definition.Key); StartBroadcastPackage(GameReflection.Everybody, ConfigsToPackage((IEnumerable?)(object)new ConfigEntryBase[1] { configEntry })); } } private void OnCustomValueChanged(CustomSyncedValueBase customValue) { if (customValuesBeingApplied.Contains(customValue) || !CanBroadcastFromThisSide()) { DebugLog(ConditionalConfigSyncDebugLevel.Trace, "CustomValue", $"Ignored {customValue.Identifier}: applying={customValuesBeingApplied.Contains(customValue)}, canBroadcast={CanBroadcastFromThisSide()}"); } else if (ShouldDeferOutgoingBroadcasts) { if (customValue.PreserveUpdateSequence) { if (TryEnqueueSequencedPackage(ConfigsToPackage(null, new CustomSyncedValueBase[1] { customValue }), customValue.Identifier)) { DebugLog(ConditionalConfigSyncDebugLevel.Verbose, "CustomValue", $"Queued sequenced {customValue.Identifier}, priority={customValue.Priority}"); } } else { pendingCustomValueBroadcasts.Add(customValue); DebugLog(ConditionalConfigSyncDebugLevel.Verbose, "CustomValue", $"Queued latest-state {customValue.Identifier}, priority={customValue.Priority}"); } } else { DebugLog(ConditionalConfigSyncDebugLevel.Verbose, "CustomValue", $"Broadcast {customValue.Identifier}, priority={customValue.Priority}"); StartBroadcastPackage(GameReflection.Everybody, ConfigsToPackage(null, new CustomSyncedValueBase[1] { customValue })); } } private static bool IsSenderAdmin(long sender) { if (!GameReflection.HasZNet || !GameReflection.HasZRoutedRpc) { return false; } ZNetPeer routedPeer = GameReflection.GetRoutedPeer(sender); return IsPeerAdmin(routedPeer); } private static bool IsPeerAdmin(ZNetPeer? peer) { if (!GameReflection.HasZNet || peer == null || GameReflection.GetPeerSocket(peer) == null) { return false; } string text = GameReflection.SocketGetHostName(GameReflection.GetPeerSocket(peer)); return !string.IsNullOrEmpty(text) && GameReflection.IsAdmin(text); } private static string FormatClient(long uid) { ZNetPeer routedPeer = GameReflection.GetRoutedPeer(uid); string text = ((routedPeer == null) ? string.Empty : GameReflection.GetPeerPlayerName(routedPeer).Trim()); return string.IsNullOrEmpty(text) ? $"client {uid}" : $"client {uid} ({text})"; } private static string FormatPeer(ZNetPeer peer) { string text = GameReflection.GetPeerPlayerName(peer).Trim(); long peerUid = GameReflection.GetPeerUid(peer); return string.IsNullOrEmpty(text) ? $"client {peerUid}" : $"client {peerUid} ({text})"; } private void RemoveFragmentAssembly(string cacheKey) { configValueCache.Remove(cacheKey); configValueCacheExpectedFragments.Remove(cacheKey); configValueCacheBytes.Remove(cacheKey); configValueCacheSenders.Remove(cacheKey); cacheExpirations.RemoveAll((KeyValuePair kv) => kv.Value == cacheKey); } private int GetFragmentCacheBytesForSender(long sender) { long value; return configValueCacheBytes.Where>((KeyValuePair kv) => configValueCacheSenders.TryGetValue(kv.Key, out value) && value == sender).Sum((KeyValuePair kv) => kv.Value); } private int GetFragmentAssemblyCountForSender(long sender) { return configValueCacheSenders.Values.Count((long owner) => owner == sender); } private int GetFragmentCacheBytesGlobal() { return configValueCacheBytes.Values.Sum(); } private bool TryEnqueueSequencedPackage(ZPackage package, string identifier) { int num = GameReflection.PackageSize(package); if (num > 20971520) { RejectSync($"Rejected sequenced custom value '{identifier}': serialized payload is {num} bytes, limit is {20971520} bytes.", null, incoming: false); return false; } if (pendingSequencedCustomValuePackages.Count >= 100) { RejectSync($"Rejected newest sequenced custom value '{identifier}': pending queue already contains {100} events.", null, incoming: false); return false; } pendingSequencedCustomValuePackages.Enqueue(package); return true; } private bool ValidateOutgoingPayload(ZPackage package, string context) { int num = GameReflection.PackageSize(package); if (num <= 20971520) { return true; } RejectSync($"Rejected outgoing {context}: serialized payload is {num} bytes, limit is {20971520} bytes.", null, incoming: false); return false; } private void RPC_FromServerConfigSync(ZRpc rpc, ZPackage package) { LockedConfigChanged -= ServerLockedSettingChanged; LockedConfigChanged += ServerLockedSettingChanged; IsSourceOfTruth = false; if (HandleConfigSyncRPC(0L, package, clientUpdate: false) && lastHandledPackageWasFull) { RaiseInitialSyncCompletedIfNeeded(); } } private void RPC_FromOtherClientConfigSync(long sender, ZPackage package) { HandleConfigSyncRPC(sender, package, clientUpdate: true); } private bool HandleConfigSyncRPC(long sender, ZPackage package, bool clientUpdate) { bool flag = !isServer && !clientUpdate; ZPackage val = null; string text = null; lastHandledPackageWasFull = false; bool flag2 = false; try { bool flag3 = !isServer || !clientUpdate || IsSenderAdmin(sender); if (isServer && clientUpdate && IsLocked && !flag3) { RejectSync("Rejected config update from " + FormatClient(sender) + " because the configuration is locked.", sender, incoming: true); return false; } string[] array = (from kv in cacheExpirations where kv.Key < DateTimeOffset.Now.Ticks select kv.Value).Distinct().ToArray(); foreach (string cacheKey in array) { RemoveFragmentAssembly(cacheKey); } byte b = GameReflection.PackageReadByte(package); string text2 = (flag ? "server" : FormatClient(sender)); int num2 = GameReflection.PackageSize(package); DebugLog(ConditionalConfigSyncDebugLevel.Trace, "Receive", $"Start package from {text2}, flags={b}, size={num2}"); if ((b & 2) == 0 && num2 > 20971520) { RejectSync($"Package from {text2} is too large: {num2} bytes, limit is {20971520} bytes.", flag ? ((long?)null) : new long?(sender), incoming: true); return false; } if ((b & 2) != 0) { long num3 = GameReflection.PackageReadLong(package); string text3 = sender + ":" + num3; text = text3; int num4 = GameReflection.PackageReadInt(package); int num5 = GameReflection.PackageReadInt(package); byte[] array2 = GameReflection.PackageReadByteArray(package, 300000); if (num5 <= 0 || num5 > 128 || num4 < 0 || num4 >= num5 || array2.Length > 300000) { RejectSync($"Invalid fragmented package from {text2}: fragment {num4}/{num5}, size={array2.Length}.", flag ? ((long?)null) : new long?(sender), incoming: true); return false; } int value2; if (!configValueCache.TryGetValue(text3, out SortedDictionary value)) { if (GetFragmentAssemblyCountForSender(sender) >= 4) { RejectSync($"Rejected fragmented package from {text2}: more than {4} incomplete packages are already cached for this sender.", flag ? ((long?)null) : new long?(sender), incoming: true); return false; } if (GetFragmentCacheBytesForSender(sender) + array2.Length > 20971520 || GetFragmentCacheBytesGlobal() + array2.Length > 67108864) { RejectSync("Rejected fragmented package from " + text2 + ": fragment cache memory limit would be exceeded.", flag ? ((long?)null) : new long?(sender), incoming: true); return false; } value = new SortedDictionary(); configValueCache[text3] = value; configValueCacheExpectedFragments[text3] = num5; configValueCacheBytes[text3] = 0; configValueCacheSenders[text3] = sender; cacheExpirations.Add(new KeyValuePair(DateTimeOffset.Now.AddSeconds(60.0).Ticks, text3)); } else if (!configValueCacheExpectedFragments.TryGetValue(text3, out value2) || value2 != num5) { RemoveFragmentAssembly(text3); RejectSync("Rejected fragmented package from " + text2 + ": fragment count changed while assembling the package.", flag ? ((long?)null) : new long?(sender), incoming: true); return false; } if (value.ContainsKey(num4)) { RemoveFragmentAssembly(text3); RejectSync($"Duplicate package fragment {num4}/{num5} from {text2}; the incomplete package was discarded.", flag ? ((long?)null) : new long?(sender), incoming: true); return false; } int fragmentCacheBytesForSender = GetFragmentCacheBytesForSender(sender); int fragmentCacheBytesGlobal = GetFragmentCacheBytesGlobal(); int num6 = configValueCacheBytes[text3]; if (num6 + array2.Length > 20971520 || fragmentCacheBytesForSender + array2.Length > 20971520 || fragmentCacheBytesGlobal + array2.Length > 67108864) { RemoveFragmentAssembly(text3); RejectSync($"Rejected fragmented package from {text2}: fragment cache or {20971520}-byte payload limit was exceeded.", flag ? ((long?)null) : new long?(sender), incoming: true); return false; } value[num4] = array2; configValueCacheBytes[text3] = num6 + array2.Length; if (value.Count < num5) { return false; } int num7 = configValueCacheBytes[text3]; RemoveFragmentAssembly(text3); if (num7 > 20971520) { RejectSync($"Fragmented package from {text2} is too large: {num7} bytes, limit is {20971520} bytes.", flag ? ((long?)null) : new long?(sender), incoming: true); return false; } byte[] array3 = new byte[num7]; int num8 = 0; foreach (byte[] value3 in value.Values) { Buffer.BlockCopy(value3, 0, array3, num8, value3.Length); num8 += value3.Length; } package = GameReflection.NewPackage(array3); b = GameReflection.PackageReadByte(package); } ProcessingServerUpdate = true; processingCount++; flag2 = true; if ((b & 4) != 0) { byte[] array4 = GameReflection.PackageReadByteArray(package, 20971520); if (array4.Length > 20971520) { RejectSync($"Compressed package from {text2} is too large: {array4.Length} bytes, limit is {20971520} bytes.", flag ? ((long?)null) : new long?(sender), incoming: true); return false; } package = GameReflection.NewPackage(DecompressLimited(array4, 20971520)); DebugLog(ConditionalConfigSyncDebugLevel.Verbose, "Network", $"Decompressed package: compressed={array4.Length}, raw={GameReflection.PackageSize(package)}"); b = GameReflection.PackageReadByte(package); } if ((b & 8) == 0) { RejectSync("Received an unsupported package format. Client and server must use the same ConditionalConfigSync version.", flag ? ((long?)null) : new long?(sender), incoming: true); return false; } GameReflection.PackageSetPos(package, 0); val = GameReflection.NewPackage(GameReflection.PackageGetArray(package)); b = GameReflection.PackageReadByte(package); lastHandledPackageWasFull = (b & 1) == 0; if (lastHandledPackageWasFull && flag) { ResetConfigsFromServer(); } ParsedConfigs parsedConfigs = ReadConfigsFromPackage(package, flag); if (isServer && clientUpdate && !flag3 && lockedConfig != null && (parsedConfigs.configValues.ContainsKey(lockedConfig) || parsedConfigs.configStates.ContainsKey(lockedConfig))) { ConfigDefinition definition = lockedConfig.BaseConfig.Definition; RejectSync("Rejected non-admin attempt from " + FormatClient(sender) + " to change protected locking config " + definition.Section + " -> " + definition.Key + ".", sender, incoming: true); return false; } ApplyParsedConfigs(parsedConfigs, flag); string text4 = ((isServer || clientUpdate) ? FormatClient(sender) : "the server"); InfoLog($"Received {parsedConfigs.configValues.Count} configs and {parsedConfigs.customValues.Count} custom values from {text4}{GetSingleEntryReceiveDetails(parsedConfigs)}"); if (isServer && clientUpdate && val != null) { List list = (from p in GameReflection.GetPeers() where GameReflection.GetPeerUid(p) != sender select p).ToList(); if (list.Count > 0) { StartBroadcastPackage(list, val); } } return true; } catch (Exception ex) { if (text != null) { RemoveFragmentAssembly(text); } RejectSync("Error while applying config package: " + ex.Message, flag ? ((long?)null) : new long?(sender), incoming: true, ex); return false; } finally { if (flag2) { if (processingCount > 0) { processingCount--; } ProcessingServerUpdate = false; FlushPendingBroadcastsIfIdle(); } } } private IEnumerator DistributeConfigToPeer(ZNetPeer peer, ZPackage package) { if (!GameReflection.HasZRoutedRpc) { yield break; } if (GameReflection.PackageSize(package) > 250000) { DebugLog(ConditionalConfigSyncDebugLevel.Verbose, "Network", $"Fragmenting package for {FormatPeer(peer)}, size={GameReflection.PackageSize(package)}, slice={250000}"); ArraySegment data = GetPackageArraySegment(package); int len = GameReflection.PackageSize(package); int fragments = (len + 250000 - 1) / 250000; long packageIdentifier = ++packageCounter; int fragment = 0; while (fragment < fragments) { foreach (bool item in waitForQueue()) { yield return item; } ISocket connectedSocket = GameReflection.GetPeerSocket(peer); if (connectedSocket != null && GameReflection.SocketIsConnected(connectedSocket)) { int offset = fragment * 250000; int count = Math.Min(250000, len - offset); ZPackage fragmentedPackage = GameReflection.NewPackage(); GameReflection.PackageWrite(fragmentedPackage, (byte)2); GameReflection.PackageWrite(fragmentedPackage, packageIdentifier); GameReflection.PackageWrite(fragmentedPackage, fragment); GameReflection.PackageWrite(fragmentedPackage, fragments); WriteByteArray(fragmentedPackage, new ArraySegment(data.Array, data.Offset + offset, count)); SendPackage(fragmentedPackage); DebugLog(ConditionalConfigSyncDebugLevel.Trace, "Network", $"Sent fragment {fragment + 1}/{fragments} to {FormatPeer(peer)}, size={count}"); if (fragment != fragments - 1) { yield return true; } int num = fragment + 1; fragment = num; continue; } break; } yield break; } foreach (bool item2 in waitForQueue()) { yield return item2; } SendPackage(package); void SendPackage(ZPackage pkg) { string method = Name + " ConditionalConfigSync"; if (isServer) { GameReflection.InvokeRpc(GameReflection.GetPeerRpc(peer), method, pkg); } else { GameReflection.InvokeRoutedPackage(method, pkg); } } IEnumerable waitForQueue() { float timeout = Time.time + 30f; while (true) { ISocket peerSocket = GameReflection.GetPeerSocket(peer); if (peerSocket == null || GameReflection.SocketGetSendQueueSize(peerSocket) <= 20000) { break; } if (Time.time > timeout) { DebugWarning("Network", "Disconnecting " + FormatPeer(peer) + " after 30 seconds config sending timeout"); GameReflection.InvokeRpc(GameReflection.GetPeerRpc(peer), "Error", (object)(ConnectionStatus)5); GameReflection.Disconnect(peer); break; } yield return false; } } } private IEnumerator SendZPackage(long target, ZPackage package) { if (!GameReflection.HasZNet) { yield break; } List peers = GameReflection.GetRoutedPeers(); if (target != GameReflection.Everybody) { peers = peers.Where((ZNetPeer p) => GameReflection.GetPeerUid(p) == target).ToList(); } yield return SendZPackage(peers, package); } private IEnumerator SendZPackage(List peers, ZPackage package) { if (!GameReflection.HasZNet || peers.Count == 0) { yield break; } try { sendCount++; int rawSize = GameReflection.PackageSize(package); if (rawSize > 20971520) { RejectSync($"Rejected outgoing synchronization package: serialized payload is {rawSize} bytes, limit is {20971520} bytes.", null, incoming: false); yield break; } if (rawSize > 10000) { package = CompressPackage(package); int compressedSize = GameReflection.PackageSize(package); if (compressedSize > 20971520) { RejectSync($"Rejected outgoing compressed package: payload is {compressedSize} bytes, limit is {20971520} bytes.", null, incoming: false); yield break; } DebugLog(ConditionalConfigSyncDebugLevel.Verbose, "Network", $"Compressed outgoing package: raw={rawSize}, compressed={compressedSize}, peers={peers.Count}"); } else { DebugLog(ConditionalConfigSyncDebugLevel.Trace, "Network", $"Sending package: size={rawSize}, peers={peers.Count}"); } List> writers = (from p in peers.Where(GameReflection.IsPeerReady) select DistributeConfigToPeer(p, package)).ToList(); writers.RemoveAll((IEnumerator writer) => !writer.MoveNext()); while (writers.Count > 0) { yield return null; writers.RemoveAll((IEnumerator writer) => !writer.MoveNext()); } } finally { if (sendCount > 0) { sendCount--; } FlushPendingBroadcastsIfIdle(); } } private static ZPackage CompressPackage(ZPackage package) { ArraySegment packageArraySegment = GetPackageArraySegment(package); ZPackage val = GameReflection.NewPackage(); GameReflection.PackageWrite(val, (byte)4); using MemoryStream memoryStream = new MemoryStream(); using (DeflateStream deflateStream = new DeflateStream(memoryStream, CompressionLevel.Optimal, leaveOpen: true)) { deflateStream.Write(packageArraySegment.Array, packageArraySegment.Offset, GameReflection.PackageSize(package)); } GameReflection.PackageWrite(val, memoryStream.ToArray()); return val; } private void Broadcast(long target, params ConfigEntryBase[] configs) { if (!CanBroadcastFromThisSide()) { DebugLog(ConditionalConfigSyncDebugLevel.Trace, "Broadcast", $"Ignored config broadcast, target={target}, locked={IsLocked}, server={isServer}"); } else if (ShouldDeferOutgoingBroadcasts) { foreach (ConfigEntryBase val in configs) { OwnConfigEntryBase configData = GetConfigData(val); if (configData != null && ShouldBroadcastConfigChange(configData)) { pendingConfigBroadcasts.Add(val); } } } else { StartBroadcastPackage(target, ConfigsToPackage(configs)); } } private void Broadcast(long target, params CustomSyncedValueBase[] customValues) { if (!CanBroadcastFromThisSide()) { DebugLog(ConditionalConfigSyncDebugLevel.Trace, "Broadcast", $"Ignored custom broadcast, target={target}, locked={IsLocked}, server={isServer}"); } else if (ShouldDeferOutgoingBroadcasts) { foreach (CustomSyncedValueBase customSyncedValueBase in customValues) { if (customSyncedValueBase.PreserveUpdateSequence) { TryEnqueueSequencedPackage(ConfigsToPackage(null, new CustomSyncedValueBase[1] { customSyncedValueBase }), customSyncedValueBase.Identifier); } else { pendingCustomValueBroadcasts.Add(customSyncedValueBase); } } } else { StartBroadcastPackage(target, ConfigsToPackage(null, customValues)); } } private void StartBroadcastPackage(long target, ZPackage package) { if (ValidateOutgoingPayload(package, "synchronization package")) { GameReflection.StartCoroutine(SendZPackage(target, package)); } } private void StartBroadcastPackage(List peers, ZPackage package) { if (ValidateOutgoingPayload(package, "synchronization package")) { GameReflection.StartCoroutine(SendZPackage(peers, package)); } } private void FlushPendingBroadcastsIfIdle() { if (ProcessingServerUpdate || IsProcessing || IsSending || flushingPendingBroadcasts || !GameReflection.HasZNet || !CanBroadcastFromThisSide() || (pendingSequencedCustomValuePackages.Count == 0 && pendingConfigBroadcasts.Count == 0 && pendingCustomValueBroadcasts.Count == 0)) { return; } flushingPendingBroadcasts = true; DebugLog(ConditionalConfigSyncDebugLevel.Basic, "Pending", $"Flushing configs={pendingConfigBroadcasts.Count}, custom={pendingCustomValueBroadcasts.Count}, sequenced={pendingSequencedCustomValuePackages.Count}"); try { while (pendingSequencedCustomValuePackages.Count > 0) { StartBroadcastPackage(GameReflection.Everybody, pendingSequencedCustomValuePackages.Dequeue()); } if (pendingConfigBroadcasts.Count > 0) { ConfigEntryBase[] array = pendingConfigBroadcasts.Where(delegate(ConfigEntryBase config) { OwnConfigEntryBase configData = GetConfigData(config); return configData != null && ShouldBroadcastConfigChange(configData); }).ToArray(); pendingConfigBroadcasts.Clear(); if (array.Length != 0) { StartBroadcastPackage(GameReflection.Everybody, ConfigsToPackage(array)); } } if (pendingCustomValueBroadcasts.Count > 0) { CustomSyncedValueBase[] array2 = (from v in pendingCustomValueBroadcasts orderby v.Priority descending, v.RegistrationIndex select v).ToArray(); pendingCustomValueBroadcasts.Clear(); if (array2.Length != 0) { StartBroadcastPackage(GameReflection.Everybody, ConfigsToPackage(null, array2)); } } } finally { flushingPendingBroadcasts = false; } } } [Description("Compatibility alias for code originally written against ServerSync ConfigSync.")] public class ConfigSync : ConditionalConfigSync { [Description("Creates a compatibility synchronization instance. Pass a stable mod GUID as the name.")] public ConfigSync(string name) : base(name) { } } [Description("Controls whether a config is always server-owned, policy-controlled, or always client-owned.")] public enum ConfigSyncMode { AlwaysServerControlled, Conditional, AlwaysClientControlled } internal class ConfigurationManagerAttributes { public bool? ReadOnly = false; public bool? Browsable = true; } [Description("Base class for runtime values synchronized independently from BepInEx config files.")] public abstract class CustomSyncedValueBase { [Description("The local fallback retained while a server-owned custom value is active.")] public object? LocalBaseValue; internal bool HasLocalBaseValue; [Description("Unique stable identifier within the owning synchronization instance.")] public readonly string Identifier; [Description("Runtime type used for package serialization and validation.")] public readonly Type Type; private object? boxedValue; private bool hasBoxedValue; protected bool localIsOwner; [Description("Batch ordering priority. Higher values are processed before lower values.")] public readonly int Priority; internal readonly long RegistrationIndex; private static long nextRegistrationIndex; [Description("Non-generic active value. Prefer Value and AssignLocalValue methods in mod code.")] public object? BoxedValue { get { return boxedValue; } set { AssignBoxedValue(value, PreserveUpdateSequence); } } internal virtual bool PreserveUpdateSequence => false; [Description("Raised when the active value is applied or explicitly re-notified.")] public event Action? ValueChanged; [Description("Compatibility alias for NotifyChanged. Re-processes and republishes the current value.")] public void Update() { this.ValueChanged?.Invoke(); } [Description("Forces subscribers and synchronization to process the current value again.")] public void NotifyChanged() { this.ValueChanged?.Invoke(); } protected bool AssignBoxedValue(object? value, bool notifyIfEqual) { if (hasBoxedValue && BoxedValuesEqual(boxedValue, value) && !notifyIfEqual) { return false; } boxedValue = value; hasBoxedValue = true; this.ValueChanged?.Invoke(); return true; } internal virtual bool BoxedValuesEqual(object? current, object? next) { return object.Equals(current, next); } protected CustomSyncedValueBase(ConditionalConfigSync configSync, string identifier, Type type, int priority) { Priority = priority; Identifier = identifier; Type = type; RegistrationIndex = ++nextRegistrationIndex; configSync.AddCustomValue(this); localIsOwner = configSync.IsSourceOfTruth; configSync.SourceOfTruthChanged += delegate(bool truth) { localIsOwner = truth; }; } internal void StoreLocalBaseValue(object? value) { LocalBaseValue = value; HasLocalBaseValue = true; } internal void ClearLocalBaseValue() { LocalBaseValue = null; HasLocalBaseValue = false; } } [Description("State-like synchronized value. Equal assignments are suppressed by the configured comparer.")] public class CustomSyncedValue : CustomSyncedValueBase { private readonly IEqualityComparer valueComparer; [Description("The currently active synchronized value. Equal assignments are suppressed for normal values.")] public T Value { get { return (T)base.BoxedValue; } set { base.BoxedValue = value; } } internal override bool BoxedValuesEqual(object? current, object? next) { if (current == next) { return true; } if (current == null || next == null) { return false; } return valueComparer.Equals((T)current, (T)next); } [Description("Creates a state value. Use valueComparer for content equality of collections or domain objects.")] public CustomSyncedValue(ConditionalConfigSync configSync, string identifier, T value = default(T), int priority = 0, IEqualityComparer? valueComparer = null) : base(configSync, identifier, typeof(T), priority) { this.valueComparer = valueComparer ?? EqualityComparer.Default; Value = value; } [Description("Assigns using the type default: suppress duplicates for state values, preserve them for sequenced values.")] public void AssignLocalValue(T value) { if (localIsOwner) { Value = value; } else { StoreLocalBaseValue(value); } } [Description("Assigns only when the configured comparer reports a real change, even for sequenced values.")] public void AssignLocalValueIfChanged(T value) { if (localIsOwner) { AssignBoxedValue(value, notifyIfEqual: false); } else if (!HasLocalBaseValue || !BoxedValuesEqual(LocalBaseValue, value)) { StoreLocalBaseValue(value); } } [Description("Assigns and forces one notification even when the value compares equal. Useful for initial processing.")] public void AssignLocalValueAndNotify(T value) { if (localIsOwner) { AssignBoxedValue(value, notifyIfEqual: true); } else { StoreLocalBaseValue(value); } } } [Description("Event-like synchronized value. Every assignment is preserved in order, including equal payloads.")] public sealed class SequencedCustomSyncedValue : CustomSyncedValue { internal override bool PreserveUpdateSequence => true; [Description("Creates an event stream. Use for commands or pulses where repeated equal values are separate events.")] public SequencedCustomSyncedValue(ConditionalConfigSync configSync, string identifier, T value = default(T), int priority = 0, IEqualityComparer? valueComparer = null) : base(configSync, identifier, value, priority, valueComparer) { } } internal static class GameReflection { private const BindingFlags Any = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; private static readonly Type ZNetType = typeof(ZNet); private static readonly Type ZRoutedRpcType = typeof(ZRoutedRpc); private static readonly Type ZNetPeerType = typeof(ZNetPeer); private static readonly Type ZRpcType = typeof(ZRpc); private static readonly Type ZPackageType = typeof(ZPackage); private static readonly Type ISocketType = typeof(ISocket); private static readonly Type GameType = typeof(Game); private static readonly Type TerminalType = typeof(Terminal); private static readonly Type FejdStartupType = typeof(FejdStartup); private static readonly Type ZPlayFabSocketType = typeof(ZPlayFabSocket); private static readonly PropertyInfo? ZNetInstanceProperty = ZNetType.GetProperty("instance", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? ZNetInstanceField = ZNetType.GetField("s_instance", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) ?? ZNetType.GetField("m_instance", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); private static readonly MethodInfo ZNetIsServerMethod = RequiredMethod(ZNetType, "IsServer", Type.EmptyTypes); private static readonly MethodInfo ZNetGetPeersMethod = RequiredMethod(ZNetType, "GetPeers", Type.EmptyTypes); private static readonly MethodInfo ZNetIsAdminMethod = RequiredMethod(ZNetType, "IsAdmin", typeof(string)); private static readonly MethodInfo ZNetDisconnectMethod = RequiredMethod(ZNetType, "Disconnect", ZNetPeerType); private static readonly MethodInfo ZNetGetPeerByRpcMethod = RequiredMethod(ZNetType, "GetPeer", ZRpcType); private static readonly MethodInfo ZNetGetConnectionStatusMethod = RequiredMethod(ZNetType, "GetConnectionStatus", Type.EmptyTypes); private static readonly FieldInfo ZNetAdminListField = RequiredField(ZNetType, "m_adminList"); private static readonly FieldInfo ZNetOnlineBackendField = RequiredField(ZNetType, "m_onlineBackend"); private static readonly FieldInfo ZNetConnectionStatusField = RequiredField(ZNetType, "m_connectionStatus"); private static readonly PropertyInfo? ZRoutedRpcInstanceProperty = ZRoutedRpcType.GetProperty("instance", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? ZRoutedRpcInstanceField = ZRoutedRpcType.GetField("s_instance", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo ZRoutedRpcEverybodyField = RequiredField(ZRoutedRpcType, "Everybody"); private static readonly FieldInfo ZRoutedRpcPeersField = RequiredField(ZRoutedRpcType, "m_peers"); private static readonly MethodInfo ZRoutedRpcGetPeerMethod = RequiredMethod(ZRoutedRpcType, "GetPeer", typeof(long)); private static readonly MethodInfo ZRoutedRpcInvokePackageMethod = RequiredMethod(ZRoutedRpcType, "InvokeRoutedRPC", typeof(string), typeof(object[])); private static readonly MethodInfo ZRoutedRpcRegisterPackageMethod = ZRoutedRpcType.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Single((MethodInfo m) => m.Name == "Register" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 2).MakeGenericMethod(ZPackageType); private static readonly FieldInfo ZNetPeerRpcField = RequiredField(ZNetPeerType, "m_rpc"); private static readonly FieldInfo ZNetPeerSocketField = RequiredField(ZNetPeerType, "m_socket"); private static readonly FieldInfo ZNetPeerUidField = RequiredField(ZNetPeerType, "m_uid"); private static readonly FieldInfo ZNetPeerPlayerNameField = RequiredField(ZNetPeerType, "m_playerName"); private static readonly MethodInfo ZNetPeerIsReadyMethod = RequiredMethod(ZNetPeerType, "IsReady", Type.EmptyTypes); private static readonly FieldInfo ZRpcSocketField = RequiredField(ZRpcType, "m_socket"); private static readonly FieldInfo ZRpcFunctionsField = RequiredField(ZRpcType, "m_functions"); private static readonly MethodInfo ZRpcGetSocketMethod = RequiredMethod(ZRpcType, "GetSocket", Type.EmptyTypes); private static readonly MethodInfo ZRpcInvokeMethod = RequiredMethod(ZRpcType, "Invoke", typeof(string), typeof(object[])); private static readonly MethodInfo ZRpcRegisterPackageMethod = ZRpcType.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Single((MethodInfo m) => m.Name == "Register" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 2).MakeGenericMethod(ZPackageType); private static readonly MethodInfo ZRpcSerializeMethod = RequiredMethod(ZRpcType, "Serialize", typeof(object[]), ZPackageType.MakeByRefType()); private static readonly MethodInfo ZRpcDeserializeMethod = ZRpcType.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Single(delegate(MethodInfo m) { if (m.Name != "Deserialize" || !m.IsStatic) { return false; } ParameterInfo[] parameters = m.GetParameters(); return parameters.Length == 3 && parameters[0].ParameterType == typeof(ParameterInfo[]) && parameters[1].ParameterType == ZPackageType && parameters[2].ParameterType == typeof(List).MakeByRefType(); }); private static readonly ConstructorInfo ZPackageDefaultConstructor = RequiredConstructor(ZPackageType, Type.EmptyTypes); private static readonly ConstructorInfo ZPackageBytesConstructor = RequiredConstructor(ZPackageType, typeof(byte[])); private static readonly FieldInfo ZPackageStreamField = RequiredField(ZPackageType, "m_stream"); private static readonly MethodInfo ZPackageSizeMethod = RequiredMethod(ZPackageType, "Size", Type.EmptyTypes); private static readonly MethodInfo ZPackageGetArrayMethod = RequiredMethod(ZPackageType, "GetArray", Type.EmptyTypes); private static readonly MethodInfo ZPackageSetPosMethod = RequiredMethod(ZPackageType, "SetPos", typeof(int)); private static readonly MethodInfo ZPackageGetPosMethod = RequiredMethod(ZPackageType, "GetPos", Type.EmptyTypes); private static readonly Dictionary ZPackageWriteMethods = (from m in ZPackageType.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) where m.Name == "Write" && m.GetParameters().Length == 1 group m by m.GetParameters()[0].ParameterType).ToDictionary((IGrouping g) => g.Key, (IGrouping g) => g.First()); private static readonly MethodInfo ZPackageReadByteMethod = RequiredMethod(ZPackageType, "ReadByte", Type.EmptyTypes); private static readonly MethodInfo ZPackageReadBoolMethod = RequiredMethod(ZPackageType, "ReadBool", Type.EmptyTypes); private static readonly MethodInfo ZPackageReadIntMethod = RequiredMethod(ZPackageType, "ReadInt", Type.EmptyTypes); private static readonly MethodInfo ZPackageReadLongMethod = RequiredMethod(ZPackageType, "ReadLong", Type.EmptyTypes); private static readonly MethodInfo ZPackageReadStringMethod = RequiredMethod(ZPackageType, "ReadString", Type.EmptyTypes); private static readonly MethodInfo ZPackageReadByteArrayCountMethod = RequiredMethod(ZPackageType, "ReadByteArray", typeof(int)); private static readonly Type SerializableParameterType = typeof(ISerializableParameter); private static readonly MethodInfo SerializableParameterSerializeMethod = RequiredMethod(SerializableParameterType, "Serialize", ZPackageType.MakeByRefType()); private static readonly MethodInfo SerializableParameterDeserializeMethod = RequiredMethod(SerializableParameterType, "Deserialize", ZPackageType.MakeByRefType()); private static readonly Dictionary SocketMethods = (from m in ISocketType.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) group m by m.Name).ToDictionary((IGrouping g) => g.Key, (IGrouping g) => g.First()); private static readonly PropertyInfo? GameInstanceProperty = GameType.GetProperty("instance", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? GameInstanceField = GameType.GetField("m_instance", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) ?? GameType.GetField("s_instance", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); private static readonly MethodInfo GameLogoutMethod = RequiredMethod(GameType, "Logout", typeof(bool), typeof(bool)); private static readonly FieldInfo FejdConnectionFailedPanelField = RequiredField(FejdStartupType, "m_connectionFailedPanel"); private static readonly FieldInfo FejdConnectionFailedErrorField = RequiredField(FejdStartupType, "m_connectionFailedError"); private static readonly FieldInfo PlayFabRemotePlayerIdField = RequiredField(ZPlayFabSocketType, "m_remotePlayerId"); private static readonly MethodInfo MonoBehaviourStartCoroutineMethod = typeof(MonoBehaviour).GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Single((MethodInfo m) => m.Name == "StartCoroutine" && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType == typeof(IEnumerator)); private static readonly MethodInfo? SyncedListGetListMethod = typeof(SyncedList).GetMethod("GetList", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null); private static readonly Type? TerminalConsoleCommandType = TerminalType.GetNestedType("ConsoleCommand", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); private static readonly Type? TerminalConsoleEventFailableType = TerminalType.GetNestedType("ConsoleEventFailable", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); internal static ZNet? ZNetInstance => (ZNet)(ZNetInstanceProperty?.GetValue(null) ?? ZNetInstanceField?.GetValue(null)); internal static ZRoutedRpc? ZRoutedRpcInstance => (ZRoutedRpc)(ZRoutedRpcInstanceProperty?.GetValue(null) ?? ZRoutedRpcInstanceField?.GetValue(null)); internal static long Everybody => Convert.ToInt64(ZRoutedRpcEverybodyField.GetValue(null)); internal static bool HasZNet => (Object)(object)ZNetInstance != (Object)null; internal static bool HasZRoutedRpc => ZRoutedRpcInstance != null; private static MethodInfo RequiredMethod(Type type, string name, params Type[] parameters) { return type.GetMethod(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, parameters, null) ?? throw new MissingMethodException(type.FullName, name); } private static FieldInfo RequiredField(Type type, string name) { return type.GetField(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new MissingFieldException(type.FullName, name); } private static ConstructorInfo RequiredConstructor(Type type, params Type[] parameters) { return type.GetConstructor(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, parameters, null) ?? throw new MissingMethodException(type.FullName, ".ctor"); } private static object? Invoke(MethodInfo method, object? instance, params object?[]? arguments) { try { return method.Invoke(instance, arguments); } catch (TargetInvocationException ex) when (ex.InnerException != null) { ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); throw; } } internal static void ValidateBindings() { } internal static bool IsServer(ZNet? znet = null) { ZNet val = znet ?? ZNetInstance; return (Object)(object)val != (Object)null && (bool)(Invoke(ZNetIsServerMethod, val) ?? ((object)false)); } internal static List GetPeers(ZNet? znet = null) { return (List)(Invoke(ZNetGetPeersMethod, znet ?? ZNetInstance) ?? new List()); } internal static bool IsAdmin(string hostName, ZNet? znet = null) { return (bool)(Invoke(ZNetIsAdminMethod, znet ?? ZNetInstance, hostName) ?? ((object)false)); } internal static void Disconnect(ZNetPeer peer, ZNet? znet = null) { Invoke(ZNetDisconnectMethod, znet ?? ZNetInstance, peer); } internal static ZNetPeer? GetPeer(ZRpc rpc, ZNet? znet = null) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Expected O, but got Unknown return (ZNetPeer)Invoke(ZNetGetPeerByRpcMethod, znet ?? ZNetInstance, rpc); } internal static object? GetConnectionStatus() { return Invoke(ZNetGetConnectionStatusMethod, null); } internal static void SetConnectionStatus(object value) { ZNetConnectionStatusField.SetValue(null, value); } internal static object? GetOnlineBackend() { return ZNetOnlineBackendField.GetValue(null); } internal static SyncedList? GetAdminList(ZNet? znet = null) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Expected O, but got Unknown return (SyncedList)ZNetAdminListField.GetValue(znet ?? ZNetInstance); } internal static List GetSyncedListValues(SyncedList list) { return (SyncedListGetListMethod == null) ? new List() : new List((IEnumerable)Invoke(SyncedListGetListMethod, list)); } internal static Coroutine? StartCoroutine(IEnumerator routine, ZNet? znet = null) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Expected O, but got Unknown return (Coroutine)Invoke(MonoBehaviourStartCoroutineMethod, znet ?? ZNetInstance, routine); } internal static ZNetPeer? GetRoutedPeer(long uid) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) ZRoutedRpc zRoutedRpcInstance = ZRoutedRpcInstance; return (zRoutedRpcInstance == null) ? ((ZNetPeer)null) : ((ZNetPeer)Invoke(ZRoutedRpcGetPeerMethod, zRoutedRpcInstance, uid)); } internal static List GetRoutedPeers() { ZRoutedRpc zRoutedRpcInstance = ZRoutedRpcInstance; return (zRoutedRpcInstance == null) ? new List() : new List((IEnumerable)ZRoutedRpcPeersField.GetValue(zRoutedRpcInstance)); } internal static void RegisterRoutedPackage(string name, Action handler) { ZRoutedRpc instance = ZRoutedRpcInstance ?? throw new InvalidOperationException("ZRoutedRpc is not initialized."); Invoke(ZRoutedRpcRegisterPackageMethod, instance, name, handler); } internal static void InvokeRoutedPackage(string method, ZPackage package) { ZRoutedRpc instance = ZRoutedRpcInstance ?? throw new InvalidOperationException("ZRoutedRpc is not initialized."); Invoke(ZRoutedRpcInvokePackageMethod, instance, method, new object[1] { package }); } internal static ZRpc GetPeerRpc(ZNetPeer peer) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Expected O, but got Unknown return (ZRpc)ZNetPeerRpcField.GetValue(peer); } internal static ISocket? GetPeerSocket(ZNetPeer peer) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Expected O, but got Unknown return (ISocket)ZNetPeerSocketField.GetValue(peer); } internal static void SetPeerSocket(ZNetPeer peer, ISocket socket) { ZNetPeerSocketField.SetValue(peer, socket); } internal static long GetPeerUid(ZNetPeer peer) { return Convert.ToInt64(ZNetPeerUidField.GetValue(peer)); } internal static string GetPeerPlayerName(ZNetPeer peer) { return ((string)ZNetPeerPlayerNameField.GetValue(peer)) ?? string.Empty; } internal static bool IsPeerReady(ZNetPeer peer) { return (bool)(Invoke(ZNetPeerIsReadyMethod, peer) ?? ((object)false)); } internal static ISocket? GetRpcSocket(ZRpc rpc) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Expected O, but got Unknown return (ISocket)Invoke(ZRpcGetSocketMethod, rpc); } internal static void SetRpcSocket(ZRpc rpc, ISocket socket) { ZRpcSocketField.SetValue(rpc, socket); } internal static IDictionary GetRpcFunctions(ZRpc rpc) { return (IDictionary)ZRpcFunctionsField.GetValue(rpc); } internal static Action GetRpcPackageAction(object rpcMethod) { FieldInfo fieldInfo = rpcMethod.GetType().GetField("m_action", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new MissingFieldException(rpcMethod.GetType().FullName, "m_action"); return (Action)(fieldInfo.GetValue(rpcMethod) ?? throw new InvalidOperationException("RPC package action is null.")); } internal static void RegisterRpcPackage(ZRpc rpc, string name, Action handler) { Invoke(ZRpcRegisterPackageMethod, rpc, name, handler); } internal static void InvokeRpc(ZRpc rpc, string method, params object[] parameters) { Invoke(ZRpcInvokeMethod, rpc, method, parameters); } internal static void Serialize(object[] parameters, ref ZPackage package) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Expected O, but got Unknown object[] array = new object[2] { parameters, package }; Invoke(ZRpcSerializeMethod, null, array); package = (ZPackage)array[1]; } internal static void Deserialize(ParameterInfo[] parameters, ZPackage package, ref List data) { object[] array = new object[3] { parameters, package, data }; Invoke(ZRpcDeserializeMethod, null, array); data = (List)array[2]; } internal static void SerializeParameter(object value, ref ZPackage package) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Expected O, but got Unknown object[] array = new object[1] { package }; Invoke(SerializableParameterSerializeMethod, value, array); package = (ZPackage)array[0]; } internal static void DeserializeParameter(object value, ref ZPackage package) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Expected O, but got Unknown object[] array = new object[1] { package }; Invoke(SerializableParameterDeserializeMethod, value, array); package = (ZPackage)array[0]; } internal static ZPackage NewPackage() { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Expected O, but got Unknown return (ZPackage)ZPackageDefaultConstructor.Invoke(Array.Empty()); } internal static ZPackage NewPackage(byte[] data) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Expected O, but got Unknown return (ZPackage)ZPackageBytesConstructor.Invoke(new object[1] { data }); } internal static int PackageSize(ZPackage package) { return Convert.ToInt32(Invoke(ZPackageSizeMethod, package)); } internal static byte[] PackageGetArray(ZPackage package) { return (byte[])Invoke(ZPackageGetArrayMethod, package); } internal static void PackageSetPos(ZPackage package, int position) { Invoke(ZPackageSetPosMethod, package, position); } internal static int PackageGetPos(ZPackage package) { return Convert.ToInt32(Invoke(ZPackageGetPosMethod, package)); } internal static MemoryStream PackageStream(ZPackage package) { return (MemoryStream)ZPackageStreamField.GetValue(package); } internal static void PackageWrite(ZPackage package, T value) { Type typeFromHandle = typeof(T); if (!ZPackageWriteMethods.TryGetValue(typeFromHandle, out MethodInfo value2)) { object obj = value?.GetType(); if (obj == null) { obj = typeFromHandle; } Type type = (Type)obj; if (!ZPackageWriteMethods.TryGetValue(type, out value2)) { throw new MissingMethodException(ZPackageType.FullName, "Write(" + type.FullName + ")"); } } Invoke(value2, package, value); } internal static byte PackageReadByte(ZPackage package) { return (byte)Invoke(ZPackageReadByteMethod, package); } internal static bool PackageReadBool(ZPackage package) { return (bool)Invoke(ZPackageReadBoolMethod, package); } internal static int PackageReadInt(ZPackage package) { return (int)Invoke(ZPackageReadIntMethod, package); } internal static long PackageReadLong(ZPackage package) { return (long)Invoke(ZPackageReadLongMethod, package); } internal static string PackageReadString(ZPackage package) { return (string)Invoke(ZPackageReadStringMethod, package); } internal static byte[] PackageReadByteArray(ZPackage package, int maxLength) { int num = PackageReadInt(package); int num2 = PackageSize(package) - PackageGetPos(package); if (num < 0 || num > maxLength || num > num2) { throw new InvalidDataException($"Invalid byte-array length {num}; limit={maxLength}, remaining={num2}."); } byte[] array = (byte[])Invoke(ZPackageReadByteArrayCountMethod, package, num); if (array.Length != num) { throw new EndOfStreamException($"Expected {num} bytes, received {array.Length}."); } return array; } internal static bool SocketIsConnected(ISocket socket) { return (bool)(Invoke(SocketMethods["IsConnected"], socket) ?? ((object)false)); } internal static ZPackage? SocketRecv(ISocket socket) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Expected O, but got Unknown return (ZPackage)Invoke(SocketMethods["Recv"], socket); } internal static int SocketGetSendQueueSize(ISocket socket) { return Convert.ToInt32(Invoke(SocketMethods["GetSendQueueSize"], socket)); } internal static int SocketGetCurrentSendRate(ISocket socket) { return Convert.ToInt32(Invoke(SocketMethods["GetCurrentSendRate"], socket)); } internal static bool SocketIsHost(ISocket socket) { return (bool)(Invoke(SocketMethods["IsHost"], socket) ?? ((object)false)); } internal static void SocketDispose(ISocket socket) { Invoke(SocketMethods["Dispose"], socket); } internal static bool SocketGotNewData(ISocket socket) { return (bool)(Invoke(SocketMethods["GotNewData"], socket) ?? ((object)false)); } internal static void SocketClose(ISocket socket) { Invoke(SocketMethods["Close"], socket); } internal static string SocketGetEndPointString(ISocket socket) { return ((string)Invoke(SocketMethods["GetEndPointString"], socket)) ?? string.Empty; } internal static ISocket? SocketAccept(ISocket socket) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Expected O, but got Unknown return (ISocket)Invoke(SocketMethods["Accept"], socket); } internal static int SocketGetHostPort(ISocket socket) { return Convert.ToInt32(Invoke(SocketMethods["GetHostPort"], socket)); } internal static bool SocketFlush(ISocket socket) { return (bool)(Invoke(SocketMethods["Flush"], socket) ?? ((object)false)); } internal static string SocketGetHostName(ISocket socket) { return ((string)Invoke(SocketMethods["GetHostName"], socket)) ?? string.Empty; } internal static void SocketVersionMatch(ISocket socket) { Invoke(SocketMethods["VersionMatch"], socket); } internal static void SocketSend(ISocket socket, ZPackage package) { Invoke(SocketMethods["Send"], socket, package); } internal static void SocketGetAndResetStats(ISocket socket, out int totalSent, out int totalRecv) { object[] array = new object[2] { 0, 0 }; Invoke(SocketMethods["GetAndResetStats"], socket, array); totalSent = (int)array[0]; totalRecv = (int)array[1]; } internal static void SocketGetConnectionQuality(ISocket socket, out float localQuality, out float remoteQuality, out int ping, out float outByteSec, out float inByteSec) { object[] array = new object[5] { 0f, 0f, 0, 0f, 0f }; Invoke(SocketMethods["GetConnectionQuality"], socket, array); localQuality = (float)array[0]; remoteQuality = (float)array[1]; ping = (int)array[2]; outByteSec = (float)array[3]; inByteSec = (float)array[4]; } internal static void Logout() { object obj = GameInstanceProperty?.GetValue(null) ?? GameInstanceField?.GetValue(null); if (obj != null) { Invoke(GameLogoutMethod, obj, true, true); } } internal static GameObject? GetConnectionFailedPanel(FejdStartup startup) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Expected O, but got Unknown return (GameObject)FejdConnectionFailedPanelField.GetValue(startup); } internal static TMP_Text? GetConnectionFailedError(FejdStartup startup) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Expected O, but got Unknown return (TMP_Text)FejdConnectionFailedErrorField.GetValue(startup); } internal static object? GetPlayFabRemotePlayerId(ZPlayFabSocket socket) { return PlayFabRemotePlayerIdField.GetValue(socket); } internal static void SetPlayFabRemotePlayerId(ZPlayFabSocket socket, object? value) { PlayFabRemotePlayerIdField.SetValue(socket, value); } internal static void RegisterConsoleCommand(string command, string description, MethodInfo handler, bool isCheat = false, bool isNetwork = false, bool onlyServer = false, bool isSecret = false, bool allowInDevBuild = false, bool remoteCommand = false, bool onlyAdmin = false) { if (TerminalConsoleCommandType == null || TerminalConsoleEventFailableType == null) { throw new MissingMemberException("Terminal console command types were not found."); } Delegate obj = Delegate.CreateDelegate(TerminalConsoleEventFailableType, handler); ConstructorInfo constructorInfo = TerminalConsoleCommandType.GetConstructors(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).First((ConstructorInfo c) => c.GetParameters().Length == 12 && c.GetParameters()[2].ParameterType == TerminalConsoleEventFailableType); constructorInfo.Invoke(new object[12] { command, description, obj, isCheat, isNetwork, onlyServer, isSecret, allowInDevBuild, null, false, remoteCommand, onlyAdmin }); } internal static int ConsoleArgsLength(ConsoleEventArgs args) { return Convert.ToInt32(((object)args).GetType().GetProperty("Length", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(args) ?? ((object)0)); } internal static string ConsoleArg(ConsoleEventArgs args, int index) { return ((string)((object)args).GetType().GetProperty("Item", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, typeof(string), new Type[1] { typeof(int) }, null)?.GetValue(args, new object[1] { index })) ?? string.Empty; } internal static void ConsoleAddString(ConsoleEventArgs args, string text) { object obj = ((object)args).GetType().GetProperty("Context", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(args) ?? ((object)args).GetType().GetField("Context", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(args); MethodInfo methodInfo = obj?.GetType().GetMethod("AddString", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(string) }, null); if (obj != null && methodInfo != null) { Invoke(methodInfo, obj, text); } } internal static int StableHash(string value) { int num = 5381; int num2 = num; for (int i = 0; i < value.Length; i += 2) { num = ((num << 5) + num) ^ value[i]; if (i == value.Length - 1) { break; } num2 = ((num2 << 5) + num2) ^ value[i + 1]; } return num + num2 * 1566083941; } } [Description("Shared Conditional Config Sync package metadata. PluginVersion is the single release version source.")] public static class PluginInfo { public const string PluginGuid = "_shudnal.ConditionalConfigSync"; public const string PluginName = "Conditional Config Sync"; public const string RepositoryUrl = "https://github.com/shudnal/ConditionalConfigSync"; public const string PluginVersion = "1.0.0"; public const int ProtocolVersion = 1; } internal static class RuntimeGuard { internal const string StandaloneAssemblyName = "ConditionalConfigSync"; internal const string PluginGuid = "_shudnal.ConditionalConfigSync"; internal const string HarmonyId = "_shudnal.ConditionalConfigSync"; internal static bool IsStandaloneAssembly => string.Equals(typeof(RuntimeGuard).Assembly.GetName().Name, "ConditionalConfigSync", StringComparison.Ordinal); internal static void ThrowIfEmbedded() { if (IsStandaloneAssembly) { return; } Assembly assembly = typeof(RuntimeGuard).Assembly; throw new InvalidOperationException("An embedded copy of ConditionalConfigSync was detected inside assembly '" + assembly.GetName().Name + "'. Embedded copies are not supported. Remove the embedded library, reference ConditionalConfigSync.dll normally, and add the BepInEx hard dependency '{PluginInfo.PluginGuid}'."); } } [Description("Base metadata for a BepInEx config entry registered with ConditionalConfigSync.")] public abstract class OwnConfigEntryBase { [Description("The client's saved local fallback while a server-controlled value is active.")] public object? LocalBaseValue; internal bool HasLocalBaseValue; internal object? ServerValue; internal bool HasServerValue; internal bool IsServerControlled = true; internal bool IsHidden; internal bool PolicyStateInitialized; [Description("Whether this setting is always server-owned, policy-controlled, or always client-owned.")] public ConfigSyncMode SyncMode = ConfigSyncMode.Conditional; [Description("Default ownership for Conditional mode: true for server-controlled, false for client-controlled.")] public bool SynchronizedConfig = true; [Description("The underlying BepInEx config entry.")] public abstract ConfigEntryBase BaseConfig { get; } internal void StoreLocalBaseValue(object? value) { LocalBaseValue = value; HasLocalBaseValue = true; } internal void ClearLocalBaseValue() { LocalBaseValue = null; HasLocalBaseValue = false; } internal void StoreServerValue(object? value) { ServerValue = value; HasServerValue = true; } internal void ClearServerValue() { ServerValue = null; HasServerValue = false; } } [Description("Typed wrapper for a regular BepInEx config entry with separate local and server values.")] public class SyncedConfigEntry : OwnConfigEntryBase { [Description("The original typed BepInEx config entry.")] public readonly ConfigEntry SourceConfig; public override ConfigEntryBase BaseConfig => (ConfigEntryBase)(object)SourceConfig; [Description("The currently active value. On a client this may be the server value.")] public T Value { get { return SourceConfig.Value; } set { SourceConfig.Value = value; } } public SyncedConfigEntry(ConfigEntry sourceConfig) { SourceConfig = sourceConfig; } [Description("Updates the local fallback without overwriting an active server value on a client.")] public void AssignLocalValue(T value) { if (!HasLocalBaseValue) { Value = value; } else { LocalBaseValue = value; } } } [Description("Details an effective server-controlled or hidden-state transition for one config entry.")] public sealed class PolicyStateChangedEventArgs : EventArgs { public OwnConfigEntryBase Config { get; } public bool OldServerControlled { get; } public bool NewServerControlled { get; } public bool OldHidden { get; } public bool NewHidden { get; } public string Source { get; } internal PolicyStateChangedEventArgs(OwnConfigEntryBase config, bool oldServerControlled, bool newServerControlled, bool oldHidden, bool newHidden, string source) { Config = config; OldServerControlled = oldServerControlled; NewServerControlled = newServerControlled; OldHidden = oldHidden; NewHidden = newHidden; Source = source; } } [Description("Diagnostic details for an incoming or outgoing synchronization operation that was rejected.")] public sealed class SyncRejectedEventArgs : EventArgs { public string Reason { get; } public long? SenderUid { get; } public bool Incoming { get; } public Exception? Exception { get; } internal SyncRejectedEventArgs(string reason, long? senderUid, bool incoming, Exception? exception = null) { Reason = reason; SenderUid = senderUid; Incoming = incoming; Exception = exception; } } [Description("Performs peer version compatibility checks for a mod.")] public class VersionCheck { private static readonly HashSet versionChecks = new HashSet(); private static readonly Dictionary notProcessedNames = new Dictionary(); [Description("Stable mod identifier used by the version-check RPC.")] public string Name; private string? displayName; private string? currentVersion; private string? minimumRequiredVersion; private string? ReceivedCurrentVersion; private string? ReceivedMinimumRequiredVersion; private int receivedProtocolVersion; private readonly HashSet ValidatedClients = new HashSet(); private readonly ConditionalConfigSync? configSync; [Description("Human-readable mod name used in version errors and logs.")] public string DisplayName { get { return displayName ?? Name; } set { displayName = value; } } [Description("Current local mod version.")] public string CurrentVersion { get { return currentVersion ?? "0.0.0"; } set { currentVersion = value; } } [Description("Oldest compatible version accepted from the remote peer.")] public string MinimumRequiredVersion { get { return minimumRequiredVersion ?? (ModRequired ? CurrentVersion : "0.0.0"); } set { minimumRequiredVersion = value; } } [Description("Whether the checked mod must be installed and compatible on the remote peer.")] public bool ModRequired { get; set; } = true; [Description("Protocol version reported by the connected server, or zero before the handshake completes.")] public static int RemoteServerProtocolVersion { get; private set; } [Description("Whether the connected server's ConditionalConfigSync protocol is known.")] public static bool RemoteServerProtocolKnown { get; private set; } internal static void ApplyRuntimePatches(Harmony harmony) { harmony.Patch((MethodBase)AccessTools.DeclaredMethod(typeof(ZNet), "RPC_PeerInfo", new Type[2] { typeof(ZRpc), typeof(ZPackage) }, (Type[])null), CreateHarmonyMethod("RPC_PeerInfo"), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)AccessTools.DeclaredMethod(typeof(ZNet), "OnNewConnection", new Type[1] { typeof(ZNetPeer) }, (Type[])null), CreateHarmonyMethod("RegisterAndCheckVersion"), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)AccessTools.DeclaredMethod(typeof(ZNet), "Disconnect", new Type[1] { typeof(ZNetPeer) }, (Type[])null), CreateHarmonyMethod("RemoveDisconnected"), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)AccessTools.DeclaredMethod(typeof(FejdStartup), "ShowConnectError", new Type[1] { typeof(ConnectionStatus) }, (Type[])null), (HarmonyMethod)null, CreateHarmonyMethod("ShowConnectionError"), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } private static HarmonyMethod CreateHarmonyMethod(string methodName) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(VersionCheck), methodName, (Type[])null, (Type[])null) ?? throw new MissingMethodException(typeof(VersionCheck).FullName, methodName); return new HarmonyMethod(methodInfo); } [Description("Creates a standalone version check without synchronized values.")] public VersionCheck(string name) { ConditionalConfigSync.EnsureRuntimeReady(); Name = name; ModRequired = true; versionChecks.Add(this); } [Description("Creates a version check backed by a ConditionalConfigSync instance.")] public VersionCheck(ConditionalConfigSync configSync) { ConditionalConfigSync.EnsureRuntimeReady(); this.configSync = configSync; Name = configSync.Name; versionChecks.Add(this); } [Description("Clears received peer state and refreshes fields from the backing sync instance.")] public void Initialize() { ReceivedCurrentVersion = null; ReceivedMinimumRequiredVersion = null; receivedProtocolVersion = 0; if (configSync != null) { Name = configSync.Name; DisplayName = configSync.DisplayName; CurrentVersion = configSync.CurrentVersion; MinimumRequiredVersion = configSync.MinimumRequiredVersion; ModRequired = configSync.ModRequired; } } private bool IsVersionOk() { if (ReceivedMinimumRequiredVersion == null || ReceivedCurrentVersion == null) { return !ModRequired; } bool flag = new Version(CurrentVersion) >= new Version(ReceivedMinimumRequiredVersion); bool flag2 = new Version(ReceivedCurrentVersion) >= new Version(MinimumRequiredVersion); return flag && flag2 && IsProtocolOk(); } private bool IsProtocolOk() { return receivedProtocolVersion == 1; } private string ErrorClient() { if (ReceivedMinimumRequiredVersion == null) { return DisplayName + " is not installed on the server."; } if (!IsProtocolOk()) { string text = ((receivedProtocolVersion == 0) ? "missing" : receivedProtocolVersion.ToString()); return DisplayName + " uses incompatible ConditionalConfigSync protocol " + text + ". " + $"This client requires protocol {1}."; } return (new Version(CurrentVersion) >= new Version(ReceivedMinimumRequiredVersion)) ? (DisplayName + " may not be higher than version " + ReceivedCurrentVersion + ". You have version " + CurrentVersion + ".") : (DisplayName + " needs to be at least version " + ReceivedMinimumRequiredVersion + ". You have version " + CurrentVersion + "."); } private string ErrorServer(ZRpc rpc) { string text = GameReflection.SocketGetHostName(GameReflection.GetRpcSocket(rpc) ?? throw new InvalidOperationException("RPC socket is unavailable.")); if (!IsProtocolOk()) { string text2 = ((receivedProtocolVersion == 0) ? "missing" : receivedProtocolVersion.ToString()); return "Disconnect: The client (" + text + ") uses incompatible ConditionalConfigSync protocol " + text2 + ". " + $"The server requires protocol {1}."; } return "Disconnect: The client (" + text + ") doesn't have the correct " + DisplayName + " version " + MinimumRequiredVersion; } private string Error(ZRpc? rpc = null) { return (rpc == null) ? ErrorClient() : ErrorServer(rpc); } private static VersionCheck[] GetFailedClient() { return versionChecks.Where((VersionCheck check) => !check.IsVersionOk()).ToArray(); } private static VersionCheck[] GetFailedServer(ZRpc rpc) { return versionChecks.Where((VersionCheck check) => check.ModRequired && !check.ValidatedClients.Contains(rpc)).ToArray(); } private static void Logout() { GameReflection.Logout(); GameReflection.SetConnectionStatus((object)(ConnectionStatus)3); } private static void DisconnectClient(ZRpc rpc) { GameReflection.InvokeRpc(rpc, "Error", 3); } private static void CheckVersion(ZRpc rpc, ZPackage pkg) { CheckVersion(rpc, pkg, null); } private static void CheckVersion(ZRpc rpc, ZPackage pkg, Action? original) { string text = GameReflection.PackageReadString(pkg); string text2 = GameReflection.PackageReadString(pkg); string text3 = GameReflection.PackageReadString(pkg); int num = 0; if (GameReflection.PackageSize(pkg) - GameReflection.PackageGetPos(pkg) >= 4) { num = GameReflection.PackageReadInt(pkg); } bool flag = false; foreach (VersionCheck versionCheck in versionChecks) { if (!(text != versionCheck.Name)) { ConditionalConfigSync.VersionInfoLog("Version", versionCheck.DisplayName, "Received " + versionCheck.DisplayName + " version " + text3 + ", minimum version " + text2 + ", ConditionalConfigSync protocol " + ((num == 0) ? "missing" : num.ToString()) + " from the " + (GameReflection.IsServer() ? "client" : "server") + "."); versionCheck.ReceivedMinimumRequiredVersion = text2; versionCheck.ReceivedCurrentVersion = text3; versionCheck.receivedProtocolVersion = num; if (!GameReflection.IsServer()) { RemoteServerProtocolVersion = num; RemoteServerProtocolKnown = true; } if (GameReflection.IsServer() && versionCheck.IsVersionOk()) { versionCheck.ValidatedClients.Add(rpc); } flag = true; } } if (flag) { return; } GameReflection.PackageSetPos(pkg, 0); if (original != null) { original(rpc, pkg); if (GameReflection.PackageGetPos(pkg) == 0) { notProcessedNames[text] = text3; } } } private static bool RPC_PeerInfo(ZRpc rpc, ZNet __instance) { VersionCheck[] array = (GameReflection.IsServer(__instance) ? GetFailedServer(rpc) : GetFailedClient()); if (array.Length == 0) { return true; } VersionCheck[] array2 = array; foreach (VersionCheck versionCheck in array2) { ConditionalConfigSync.VersionWarningLog("Version", versionCheck.DisplayName, versionCheck.Error(rpc)); } if (GameReflection.IsServer(__instance)) { DisconnectClient(rpc); } else { Logout(); } return false; } private static void RegisterAndCheckVersion(ZNetPeer peer, ZNet __instance) { notProcessedNames.Clear(); ZRpc peerRpc = GameReflection.GetPeerRpc(peer); IDictionary rpcFunctions = GameReflection.GetRpcFunctions(peerRpc); if (rpcFunctions.Contains(GameReflection.StableHash("ConditionalConfigSync VersionCheck"))) { object rpcMethod = rpcFunctions[GameReflection.StableHash("ConditionalConfigSync VersionCheck")]; Action action = GameReflection.GetRpcPackageAction(rpcMethod); GameReflection.RegisterRpcPackage(peerRpc, "ConditionalConfigSync VersionCheck", delegate(ZRpc rpc, ZPackage pkg) { CheckVersion(rpc, pkg, action); }); } else { GameReflection.RegisterRpcPackage(peerRpc, "ConditionalConfigSync VersionCheck", CheckVersion); } foreach (VersionCheck versionCheck in versionChecks) { versionCheck.Initialize(); if (versionCheck.ModRequired || GameReflection.IsServer(__instance)) { ConditionalConfigSync.VersionDebugLog("Version", versionCheck.DisplayName, "Sending version " + versionCheck.CurrentVersion + ", minimum " + versionCheck.MinimumRequiredVersion + ", " + $"ConditionalConfigSync protocol {1} to " + (GameReflection.IsServer(__instance) ? "client" : "server")); ZPackage val = GameReflection.NewPackage(); GameReflection.PackageWrite(val, versionCheck.Name); GameReflection.PackageWrite(val, versionCheck.MinimumRequiredVersion); GameReflection.PackageWrite(val, versionCheck.CurrentVersion); GameReflection.PackageWrite(val, 1); GameReflection.InvokeRpc(peerRpc, "ConditionalConfigSync VersionCheck", val); } } } internal static void ResetSessionState() { RemoteServerProtocolVersion = 0; RemoteServerProtocolKnown = false; notProcessedNames.Clear(); foreach (VersionCheck versionCheck in versionChecks) { versionCheck.ReceivedCurrentVersion = null; versionCheck.ReceivedMinimumRequiredVersion = null; versionCheck.receivedProtocolVersion = 0; versionCheck.ValidatedClients.Clear(); } } internal static void ShutdownRuntime() { ResetSessionState(); versionChecks.Clear(); } private static void RemoveDisconnected(ZNetPeer peer, ZNet __instance) { if (!GameReflection.IsServer(__instance)) { return; } foreach (VersionCheck versionCheck in versionChecks) { versionCheck.ValidatedClients.Remove(GameReflection.GetPeerRpc(peer)); } } private static void ShowConnectionError(FejdStartup __instance) { //IL_0193: Unknown result type (might be due to invalid IL or missing references) //IL_0198: Unknown result type (might be due to invalid IL or missing references) //IL_01a6: Unknown result type (might be due to invalid IL or missing references) //IL_01e1: Unknown result type (might be due to invalid IL or missing references) //IL_01ed: Unknown result type (might be due to invalid IL or missing references) //IL_01fb: Unknown result type (might be due to invalid IL or missing references) //IL_020d: Unknown result type (might be due to invalid IL or missing references) //IL_021c: Unknown result type (might be due to invalid IL or missing references) //IL_0221: Unknown result type (might be due to invalid IL or missing references) //IL_022c: Unknown result type (might be due to invalid IL or missing references) GameObject connectionFailedPanel = GameReflection.GetConnectionFailedPanel(__instance); TMP_Text connectionFailedError = GameReflection.GetConnectionFailedError(__instance); if ((Object)(object)connectionFailedPanel == (Object)null || (Object)(object)connectionFailedError == (Object)null || !connectionFailedPanel.activeSelf || !object.Equals(GameReflection.GetConnectionStatus(), (object)(ConnectionStatus)3)) { return; } bool flag = false; VersionCheck[] failedClient = GetFailedClient(); if (failedClient.Length != 0) { string text = string.Join("\n", failedClient.Select((VersionCheck check) => check.Error())); connectionFailedError.text = connectionFailedError.text + "\n" + text; flag = true; } foreach (KeyValuePair item in notProcessedNames.OrderBy, string>((KeyValuePair kv) => kv.Key)) { if (!connectionFailedError.text.Contains(item.Key)) { TMP_Text val = connectionFailedError; val.text = val.text + "\nServer expects you to have " + item.Key + " (Version: " + item.Value + ") installed."; flag = true; } } if (flag) { RectTransform component = ((Component)connectionFailedPanel.transform.Find("Image")).GetComponent(); Vector2 sizeDelta = component.sizeDelta; sizeDelta.x = 675f; component.sizeDelta = sizeDelta; connectionFailedError.ForceMeshUpdate(false, false); float num = connectionFailedError.renderedHeight + 105f; RectTransform component2 = ((Component)((Component)component).transform.Find("ButtonOk")).GetComponent(); component2.anchoredPosition = new Vector2(component2.anchoredPosition.x, component2.anchoredPosition.y - (num - component.sizeDelta.y) / 2f); sizeDelta = component.sizeDelta; sizeDelta.y = num; component.sizeDelta = sizeDelta; } } } }