diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 6864ceb..f8763a7 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -7,6 +7,12 @@ "commands": [ "tcli" ] + }, + "evaisa.netcodepatcher.cli": { + "version": "4.2.0", + "commands": [ + "netcode-patch" + ] } } } \ No newline at end of file diff --git a/CSync/CSync.csproj b/CSync/CSync.csproj index 6a15b57..593124b 100644 --- a/CSync/CSync.csproj +++ b/CSync/CSync.csproj @@ -1,6 +1,6 @@ - netstandard2.1;net472;net48 + netstandard2.1 com.sigurd.csync Configuration file syncing library for BepInEx. @@ -43,10 +43,12 @@ - + + + @@ -55,18 +57,18 @@ - + $(LethalCompanyDir)Lethal Company_Data\Managed\Assembly-CSharp.dll $(LethalCompanyDir)Lethal Company_Data\Managed\Unity.Collections.dll - + $(LethalCompanyDir)Lethal Company_Data\Managed\Unity.Netcode.Runtime.dll - - - + + + diff --git a/CSync/Extensions/ConfigDefinitionExtensions.cs b/CSync/Extensions/ConfigDefinitionExtensions.cs new file mode 100644 index 0000000..d8d3a5c --- /dev/null +++ b/CSync/Extensions/ConfigDefinitionExtensions.cs @@ -0,0 +1,12 @@ +using BepInEx.Configuration; +using CSync.Lib; + +namespace CSync.Extensions; + +internal static class ConfigDefinitionExtensions +{ + public static SyncedConfigDefinition ToSynced(this ConfigDefinition definition) + { + return new(definition.Section, definition.Key); + } +} diff --git a/CSync/Extensions/ConfigEntryExtensions.cs b/CSync/Extensions/ConfigEntryExtensions.cs new file mode 100644 index 0000000..ad6b39f --- /dev/null +++ b/CSync/Extensions/ConfigEntryExtensions.cs @@ -0,0 +1,13 @@ +using BepInEx.Configuration; +using CSync.Lib; + +namespace CSync.Extensions; + +internal static class ConfigEntryExtensions +{ + public static (string ConfigFileRelativePath, SyncedConfigDefinition Definition) ToSyncedEntryIdentifier( + this ConfigEntryBase entry) + { + return (entry.ConfigFile.GetConfigFileRelativePath(), entry.Definition.ToSynced()); + } +} diff --git a/CSync/Extensions/ConfigFileExtensions.cs b/CSync/Extensions/ConfigFileExtensions.cs new file mode 100644 index 0000000..39acca3 --- /dev/null +++ b/CSync/Extensions/ConfigFileExtensions.cs @@ -0,0 +1,13 @@ +using System.IO; +using BepInEx; +using BepInEx.Configuration; + +namespace CSync.Extensions; + +internal static class ConfigFileExtensions +{ + public static string GetConfigFileRelativePath(this ConfigFile configFile) + { + return Path.GetRelativePath(Paths.BepInExRootPath, configFile.ConfigFilePath); + } +} diff --git a/CSync/Extensions/SyncedBindingExtensions.cs b/CSync/Extensions/SyncedBindingExtensions.cs new file mode 100644 index 0000000..2e2b5fd --- /dev/null +++ b/CSync/Extensions/SyncedBindingExtensions.cs @@ -0,0 +1,63 @@ +using BepInEx.Configuration; +using CSync.Lib; + +namespace CSync.Extensions; + +/// +/// Contains helpful extension methods to aid with synchronization and reduce code duplication. +/// +public static class SyncedBindingExtensions { + /// + /// Binds an entry to this file and returns the converted synced entry. + /// + /// The currently selected config file. + /// The category that this entry should show under. + /// The name/identifier of this entry. + /// The value assigned to this entry if not changed. + /// The description indicating what this entry does. + public static SyncedEntry BindSyncedEntry( + this ConfigFile configFile, + string section, + string key, + T defaultVal, + string description + ) { + return configFile.BindSyncedEntry(new ConfigDefinition(section, key), defaultVal, new ConfigDescription(description)); + } + + public static SyncedEntry BindSyncedEntry( + this ConfigFile configFile, + string section, + string key, + T defaultValue, + ConfigDescription? desc = null + ) { + return configFile.BindSyncedEntry(new ConfigDefinition(section, key), defaultValue, desc); + } + + public static SyncedEntry BindSyncedEntry( + this ConfigFile configFile, + ConfigDefinition definition, + T defaultValue, + string description + ) { + return configFile.BindSyncedEntry(definition, defaultValue, new ConfigDescription(description)); + } + + public static SyncedEntry BindSyncedEntry( + this ConfigFile configFile, + ConfigDefinition definition, + T defaultValue, + ConfigDescription? description = null + ) { + ConfigManager.AddToFileCache(configFile); + return configFile.Bind(definition, defaultValue, description).ToSyncedEntry(); + } + + /// + /// Converts this entry into a serializable alternative, allowing it to be synced. + /// + public static SyncedEntry ToSyncedEntry(this ConfigEntry entry) { + return new SyncedEntry(entry); + } +} diff --git a/CSync/Lib/ConfigManager.cs b/CSync/Lib/ConfigManager.cs index 44f476b..f9ed84c 100644 --- a/CSync/Lib/ConfigManager.cs +++ b/CSync/Lib/ConfigManager.cs @@ -1,8 +1,15 @@ +using System; using BepInEx.Configuration; using BepInEx; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; -using HarmonyLib; +using System.Security.Cryptography; +using System.Text; +using CSync.Extensions; +using JetBrains.Annotations; +using Unity.Netcode; +using UnityEngine; namespace CSync.Lib; @@ -10,47 +17,90 @@ namespace CSync.Lib; /// Helper class enabling the user to easily setup CSync.

/// Handles config registration, instance syncing and caching of BepInEx files.

/// +[PublicAPI] public class ConfigManager { - internal static Dictionary FileCache = []; - internal static Dictionary Instances = []; + internal static readonly Dictionary FileCache = []; + internal static readonly Dictionary Instances = []; - internal static ConfigFile GetConfigFile(string fileName) { - bool exists = FileCache.TryGetValue(fileName, out ConfigFile cfg); - if (!exists) { - string absPath = Path.Combine(Paths.ConfigPath, fileName); + private static event Action? OnPopulateEntriesRequested; + internal static void PopulateEntries() => OnPopulateEntriesRequested?.Invoke(); - cfg = new(absPath, false); - FileCache.Add(fileName, cfg); - } + private static readonly Lazy LazyPrefab; + internal static GameObject Prefab => LazyPrefab.Value; + + static ConfigManager() + { + LazyPrefab = new Lazy(() => + { + var container = new GameObject("CSyncPrefabContainer") + { + hideFlags = HideFlags.HideAndDontSave + }; + container.SetActive(false); + UnityEngine.Object.DontDestroyOnLoad(container); + + var prefab = new GameObject("ConfigSyncHolder"); + prefab.transform.SetParent(container.transform); + var networkObject = prefab.AddComponent(); + var hash = MD5.Create().ComputeHash(Encoding.UTF8.GetBytes($"{MyPluginInfo.PLUGIN_GUID}:ConfigSyncHolder")); + networkObject.GlobalObjectIdHash = BitConverter.ToUInt32(hash); + + return prefab; + }); + } + + internal static void AddToFileCache(ConfigFile configFile) + { + FileCache.TryAdd(configFile.GetConfigFileRelativePath(), configFile); + } + + internal static ConfigFile GetConfigFile(string relativePath) + { + if (FileCache.TryGetValue(relativePath, out ConfigFile configFile)) + return configFile; - return cfg; + string absolutePath = Path.GetFullPath(Path.Combine(Paths.BepInExRootPath, relativePath)); + configFile = new(absolutePath, false); + FileCache.Add(relativePath, configFile); + return configFile; } /// /// Register a config with CSync, making it responsible for synchronization.

/// After calling this method, all clients will receive the host's config upon joining. ///
- public static void Register(T config) where T : SyncedConfig, ISynchronizable { - string guid = config.GUID; - - if (config == null) { - Plugin.Logger.LogError($"An error occurred registering config: {guid}\nConfig instance cannot be null!"); + public static void Register(T config) where T : SyncedConfig { + if (config is null) + { + throw new ArgumentNullException(nameof(config), "Config instance is null, cannot register."); } - if (Instances.ContainsKey(guid)) { - Plugin.Logger.LogWarning($"Attempted to register config `{guid}` after it has already been registered!"); - return; + var assemblyQualifiedTypeName = typeof(T).AssemblyQualifiedName ?? throw new ArgumentException(nameof(config)); + var key = new InstanceKey(config.GUID, assemblyQualifiedTypeName); + + try { + Instances.Add(key, config); } + catch (ArgumentException exc) { + throw new InvalidOperationException($"Attempted to register config instance of type `{typeof(T)}`, but an instance has already been registered.", exc); + } + + SyncedInstance.Instance = config; + OnPopulateEntriesRequested += config.PopulateEntryContainer; - config.InitInstance(config); - Instances.Add(guid, config); + var syncBehaviour = Prefab.AddComponent(); + syncBehaviour.ConfigInstanceKey = key; } - internal static void SyncInstances() => Instances.Values.Do(i => i.SetupSync()); - internal static void RevertSyncedInstances() => Instances.Values.Do(i => i.RevertSync()); -} + [UsedImplicitly] + [Serializable] + [SuppressMessage("ReSharper", "Unity.RedundantSerializeFieldAttribute")] // they are *not* redundant! + public readonly record struct InstanceKey(string Guid, string AssemblyQualifiedName) + { + [field: SerializeField] + public string Guid { get; } -public interface ISynchronizable { - void SetupSync(); - void RevertSync(); -} \ No newline at end of file + [field: SerializeField] + public string AssemblyQualifiedName { get; } + } +} diff --git a/CSync/Lib/ConfigSyncBehaviour.cs b/CSync/Lib/ConfigSyncBehaviour.cs new file mode 100644 index 0000000..4b40b15 --- /dev/null +++ b/CSync/Lib/ConfigSyncBehaviour.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Unity.Netcode; +using UnityEngine; +using LogLevel = BepInEx.Logging.LogLevel; + +namespace CSync.Lib; + +public class ConfigSyncBehaviour : NetworkBehaviour +{ + [field: SerializeField] + public ConfigManager.InstanceKey ConfigInstanceKey { get; internal set; } + + private ISyncedConfig? Config { + get { + var success = ConfigManager.Instances.TryGetValue(ConfigInstanceKey, out var config); + return success ? config : null; + } + } + + private ISyncedEntryContainer? _entryContainer; + internal ISyncedEntryContainer? EntryContainer => _entryContainer ??= Config?.EntryContainer; + + public bool SyncEnabled + { + get => _syncEnabled.Value; + set => _syncEnabled.Value = value; + } + + private readonly NetworkVariable _syncEnabled = new(); + private NetworkList _deltas = null!; + + [MemberNotNull(nameof(EntryContainer))] + private void EnsureEntryContainer() + { + if (EntryContainer is not null) return; + throw new InvalidOperationException("Entry container has not been assigned."); + } + + private void Awake() + { + EnsureEntryContainer(); + _deltas = new NetworkList(); + } + + public override void OnNetworkSpawn() + { + EnsureEntryContainer(); + + if (IsServer) + { + _syncEnabled.Value = true; + + foreach (var syncedEntryBase in EntryContainer.Values) + { + var currentIndex = _deltas.Count; + _deltas.Add(syncedEntryBase.ToDelta()); + + syncedEntryBase.BoxedEntry.ConfigFile.SettingChanged += (_, args) => + { + if (!ReferenceEquals(syncedEntryBase.BoxedEntry, args.ChangedSetting)) return; + _deltas[currentIndex] = syncedEntryBase.ToDelta(); + }; + } + + return; + } + + if (IsClient) + { + _syncEnabled.OnValueChanged += OnSyncEnabledChanged; + _deltas.OnListChanged += OnClientDeltaListChanged; + + foreach (var delta in _deltas) + { + UpdateOverrideValue(delta); + } + + if (_syncEnabled.Value) EnableOverrides(); + } + } + + public override void OnDestroy() + { + DisableOverrides(); + foreach (var delta in _deltas) + { + ResetOverrideValue(delta); + } + base.OnDestroy(); + } + + private void OnSyncEnabledChanged(bool previousValue, bool newValue) + { + if (previousValue == newValue) return; + + if (newValue) + { + EnableOverrides(); + } + else + { + DisableOverrides(); + } + } + + private void OnClientDeltaListChanged(NetworkListEvent args) + { + switch (args.Type) + { + case NetworkListEvent.EventType.Remove: + case NetworkListEvent.EventType.RemoveAt: + ResetOverrideValue(args.PreviousValue); + break; + case NetworkListEvent.EventType.Add: + case NetworkListEvent.EventType.Insert: + case NetworkListEvent.EventType.Value: + UpdateOverrideValue(args.Value); + break; + case NetworkListEvent.EventType.Clear: + foreach (var delta in _deltas) + { + ResetOverrideValue(delta); + } + break; + case NetworkListEvent.EventType.Full: + foreach (var delta in _deltas) + { + UpdateOverrideValue(delta); + } + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + private void ResetOverrideValue(SyncedEntryDelta delta) + { + EnsureEntryContainer(); + try { + var entry = EntryContainer[delta.SyncedEntryIdentifier]; + entry.BoxedValueOverride = entry.BoxedEntry.DefaultValue; + } + catch (KeyNotFoundException) { } + } + + private void UpdateOverrideValue(SyncedEntryDelta delta) + { + EnsureEntryContainer(); + try { + var entry = EntryContainer[delta.SyncedEntryIdentifier]; + entry.SetSerializedValueOverride(delta.SerializedValue.Value); + } + catch (KeyNotFoundException) { + Plugin.Logger.Log(LogLevel.Warning, $"Setting \"{delta.Definition}\" could not be found, so its synced value override will be ignored."); + } + catch (Exception exc) { + Plugin.Logger.Log(LogLevel.Warning, $"Synced value override of setting \"{delta.Definition}\" could not be parsed and will be ignored. Reason: {exc.Message}; Value: {delta.SerializedValue.Value}"); + } + } + + private void EnableOverrides() + { + EnsureEntryContainer(); + foreach (var syncedEntryBase in EntryContainer.Values) + { + syncedEntryBase.ValueOverridden = true; + } + } + + private void DisableOverrides() + { + EnsureEntryContainer(); + foreach (var syncedEntryBase in EntryContainer.Values) + { + syncedEntryBase.ValueOverridden = false; + } + } +} diff --git a/CSync/Lib/ISyncedConfig.cs b/CSync/Lib/ISyncedConfig.cs new file mode 100644 index 0000000..7081bb1 --- /dev/null +++ b/CSync/Lib/ISyncedConfig.cs @@ -0,0 +1,8 @@ +namespace CSync.Lib; + +public interface ISyncedConfig +{ + public string GUID { get; } + + public ISyncedEntryContainer EntryContainer { get; } +} diff --git a/CSync/Lib/ISyncedEntryContainer.cs b/CSync/Lib/ISyncedEntryContainer.cs new file mode 100644 index 0000000..224b425 --- /dev/null +++ b/CSync/Lib/ISyncedEntryContainer.cs @@ -0,0 +1,5 @@ +using System.Collections.Generic; + +namespace CSync.Lib; + +public interface ISyncedEntryContainer : IDictionary<(string ConfigFileRelativePath, SyncedConfigDefinition Definition), SyncedEntryBase>; diff --git a/CSync/Lib/SyncedConfig.cs b/CSync/Lib/SyncedConfig.cs index b8eac1c..e460f73 100644 --- a/CSync/Lib/SyncedConfig.cs +++ b/CSync/Lib/SyncedConfig.cs @@ -1,8 +1,9 @@ -using System; -using Unity.Collections; -using Unity.Netcode; - -using CSync.Util; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using CSync.Extensions; +using HarmonyLib; +using JetBrains.Annotations; namespace CSync.Lib; @@ -10,118 +11,31 @@ namespace CSync.Lib; /// Wrapper class allowing the config class (type parameter) to be synchronized.

/// Stores the mod's unique identifier and handles registering and sending of named messages. /// -[Serializable] -public class SyncedConfig(string guid) : SyncedInstance, ISynchronizable where T : class { - static void LogErr(string str) => Plugin.Logger.LogError(str); - static void LogDebug(string str) => Plugin.Logger.LogDebug(str); - - /// - /// Invoked on the host when a client requests to sync. - /// - [field:NonSerialized] public event EventHandler SyncRequested; - internal void OnSyncRequested() => SyncRequested?.Invoke(this, EventArgs.Empty); - - /// - /// Invoked on the client when they receive the host config. - /// - [field:NonSerialized] public event EventHandler SyncReceived; - internal void OnSyncReceived() => SyncReceived?.Invoke(this, EventArgs.Empty); +[PublicAPI] +public class SyncedConfig : SyncedInstance, ISyncedConfig where T : SyncedConfig +{ + public ISyncedEntryContainer EntryContainer { get; } = new SyncedEntryContainer(); + + public SyncedConfig(string guid) + { + GUID = guid; + } /// /// The mod name or abbreviation. After being given to the constructor, it cannot be changed. /// - public readonly string GUID = guid; - - internal SyncedEntry SYNC_TO_CLIENTS { get; private set; } = null; - - /// - /// Allow the host to control whether clients can use their own config. - /// This MUST be called after binding the entry parameter. - /// - /// The entry for the host to use in your config file. - protected void EnableHostSyncControl(SyncedEntry hostSyncControlOption) { - SYNC_TO_CLIENTS = hostSyncControlOption; - - hostSyncControlOption.SettingChanged += (object sender, EventArgs e) => { - SYNC_TO_CLIENTS = hostSyncControlOption; - }; - } - - void ISynchronizable.SetupSync() { - if (IsHost) { - MessageManager.RegisterNamedMessageHandler($"{GUID}_OnRequestConfigSync", OnRequestSync); - return; - } - - MessageManager.RegisterNamedMessageHandler($"{GUID}_OnHostDisabledSyncing", OnHostDisabledSyncing); - MessageManager.RegisterNamedMessageHandler($"{GUID}_OnReceiveConfigSync", OnReceiveSync); - RequestSync(); - } - - void RequestSync() { - if (!IsClient) return; - - using FastBufferWriter stream = new(IntSize, Allocator.Temp); - - // Method `OnRequestSync` will then get called on the host. - stream.SendMessage(GUID, "OnRequestConfigSync"); - } - - internal void OnRequestSync(ulong clientId, FastBufferReader _) { - if (!IsHost) return; - OnSyncRequested(); - - if (SYNC_TO_CLIENTS != null && SYNC_TO_CLIENTS == false) { - using FastBufferWriter s = new(IntSize, Allocator.Temp); - s.SendMessage(GUID, "OnHostDisabledSyncing", clientId); - - LogDebug($"{GUID} - The host (you) has disabled syncing, sending clients a message!"); - return; + public string GUID { get; } + + internal void PopulateEntryContainer() + { + var fields = AccessTools.GetDeclaredFields(typeof(T)) + .Where(field => field.GetCustomAttribute() is not null) + .Where(field => typeof(SyncedEntryBase).IsAssignableFrom(field.FieldType)); + + foreach (var fieldInfo in fields) + { + var entryBase = (SyncedEntryBase)fieldInfo.GetValue(this); + EntryContainer.Add(entryBase.BoxedEntry.ToSyncedEntryIdentifier(), entryBase); } - - LogDebug($"{GUID} - Config sync request received from client: {clientId}"); - - byte[] array = SerializeToBytes(Instance); - int value = array.Length; - - using FastBufferWriter stream = new(value + IntSize, Allocator.Temp); - - try { - stream.WriteValueSafe(in value, default); - stream.WriteBytesSafe(array); - - stream.SendMessage(GUID, "OnReceiveConfigSync", clientId); - } catch(Exception e) { - LogErr($"{GUID} - Error occurred syncing config with client: {clientId}\n{e}"); - } - } - - internal void OnReceiveSync(ulong _, FastBufferReader reader) { - OnSyncReceived(); - - if (!reader.TryBeginRead(IntSize)) { - LogErr($"{GUID} - Config sync error: Could not begin reading buffer."); - return; - } - - reader.ReadValueSafe(out int val, default); - if (!reader.TryBeginRead(val)) { - LogErr($"{GUID} - Config sync error: Host could not sync."); - return; - } - - byte[] data = new byte[val]; - reader.ReadBytesSafe(ref data, val); - - try { - SyncInstance(data); - } catch(Exception e) { - LogErr($"Error syncing config instance!\n{e}"); - } - } - - internal void OnHostDisabledSyncing(ulong _, FastBufferReader reader) { - OnSyncCompleted(); - LogDebug($"{GUID} - Host disabled syncing. The SyncComplete event will still be invoked."); } } diff --git a/CSync/Lib/SyncedConfigDefinition.cs b/CSync/Lib/SyncedConfigDefinition.cs new file mode 100644 index 0000000..25079fe --- /dev/null +++ b/CSync/Lib/SyncedConfigDefinition.cs @@ -0,0 +1,48 @@ +using System; +using Unity.Collections; +using Unity.Netcode; + +namespace CSync.Lib; + +public struct SyncedConfigDefinition : INetworkSerializable, IEquatable +{ + public FixedString128Bytes Section; + public FixedString128Bytes Key; + + public SyncedConfigDefinition(FixedString128Bytes section, FixedString128Bytes key) + { + Section = section; + Key = key; + } + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + if (serializer.IsReader) + { + var reader = serializer.GetFastBufferReader(); + reader.ReadValueSafe(out Section); + reader.ReadValueSafe(out Key); + } + else + { + var writer = serializer.GetFastBufferWriter(); + writer.WriteValueSafe(Section); + writer.WriteValueSafe(Key); + } + } + + public bool Equals(SyncedConfigDefinition other) + { + return Section.Equals(other.Section) && Key.Equals(other.Key); + } + + public override bool Equals(object? obj) + { + return obj is SyncedConfigDefinition other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(Section, Key); + } +} diff --git a/CSync/Lib/SyncedEntry.cs b/CSync/Lib/SyncedEntry.cs index 23dd347..0226821 100644 --- a/CSync/Lib/SyncedEntry.cs +++ b/CSync/Lib/SyncedEntry.cs @@ -1,8 +1,5 @@ using System; -using System.IO; -using System.Runtime.Serialization; using BepInEx.Configuration; -using CSync.Util; namespace CSync.Lib; @@ -10,54 +7,47 @@ namespace CSync.Lib; /// Wrapper class around a BepInEx .
/// Can serialize and deserialize itself to avoid runtime errors when syncing configs.
/// -[Serializable] -public class SyncedEntry : ISerializable { - [NonSerialized] public readonly ConfigEntry Entry; - - string ConfigFileName => Path.GetFileName(Entry.ConfigFile.ConfigFilePath); - public string Key => Entry.Definition.Key; - public string Section => Entry.Definition.Section; - public string Description => Entry.Description.Description; - public object DefaultValue => Entry.DefaultValue; +public sealed class SyncedEntry : SyncedEntryBase +{ + public ConfigEntry Entry { get; private set; } + + public override ConfigEntryBase BoxedEntry + { + get => Entry; + protected init => Entry = (ConfigEntry) value; + } - public V Value { + public T LocalValue + { get => Entry.Value; - set => Entry.Value = value; + set => Entry.Value = value!; } - public static implicit operator V(SyncedEntry e) => e.Value; + private T _typedValueOverride; - public event EventHandler SettingChanged { - add => Entry.SettingChanged += value; - remove => Entry.SettingChanged -= value; + public override object? BoxedValueOverride + { + get => _typedValueOverride; + set => _typedValueOverride = (T) value!; } - public SyncedEntry(ConfigEntry cfgEntry) { - Entry = cfgEntry; + public T Value { + get { + if (ValueOverridden) return _typedValueOverride!; + return LocalValue; + } } - // Deserialization - SyncedEntry(SerializationInfo info, StreamingContext ctx) { - // Reconstruct or get cached file - string fileName = info.GetString("ConfigFileName"); - ConfigFile cfg = ConfigManager.GetConfigFile(fileName); + public static implicit operator T(SyncedEntry e) => e.Value; - // Reconstruct entry and reassign its value. - Entry = cfg.Reconstruct(info); - Value = info.GetObject("CurrentValue"); + public event EventHandler SettingChanged { + add => Entry.SettingChanged += value; + remove => Entry.SettingChanged -= value; } - // Serialization - public void GetObjectData(SerializationInfo info, StreamingContext context) { - info.AddValue("ConfigFileName", ConfigFileName); - info.AddValue("Key", Key); - info.AddValue("Section", Section); - info.AddValue("Description", Description); - info.AddValue("DefaultValue", DefaultValue); - info.AddValue("CurrentValue", Value); - } + public SyncedEntry(ConfigEntry entry) : base(entry) { } public override string ToString() { - return $"Key: {Key}\nDefault Value: {DefaultValue}\nCurrent Value: {Value}"; + return $"Key: {Entry.Definition.Key}\nLocal Value: {LocalValue}\nCurrent Value: {Value}"; } } diff --git a/CSync/Lib/SyncedEntryBase.cs b/CSync/Lib/SyncedEntryBase.cs new file mode 100644 index 0000000..5e90ae5 --- /dev/null +++ b/CSync/Lib/SyncedEntryBase.cs @@ -0,0 +1,30 @@ +using System.IO; +using BepInEx.Configuration; +using CSync.Extensions; + +namespace CSync.Lib; + +public abstract class SyncedEntryBase +{ + public abstract ConfigEntryBase BoxedEntry { get; protected init; } + + public abstract object? BoxedValueOverride { get; set; } + protected internal bool ValueOverridden = false; + + internal SyncedEntryBase(ConfigEntryBase configEntry) + { + BoxedEntry = configEntry; + BoxedValueOverride = configEntry.DefaultValue; + } + + public void SetSerializedValueOverride(string value) + { + BoxedValueOverride = TomlTypeConverter.ConvertToValue(value, BoxedEntry.SettingType); + } + + internal SyncedEntryDelta ToDelta() => new SyncedEntryDelta( + configFileRelativePath: BoxedEntry.ConfigFile.GetConfigFileRelativePath(), + definition: BoxedEntry.Definition.ToSynced(), + serializedValue: BoxedEntry.GetSerializedValue() + ); +} diff --git a/CSync/Lib/SyncedEntryContainer.cs b/CSync/Lib/SyncedEntryContainer.cs new file mode 100644 index 0000000..5fdcbd9 --- /dev/null +++ b/CSync/Lib/SyncedEntryContainer.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace CSync.Lib; + +public class SyncedEntryContainer : Dictionary<(string ConfigFileRelativePath, SyncedConfigDefinition Definition), SyncedEntryBase>, ISyncedEntryContainer +{ + public bool TryGetEntry(string configFileRelativePath, SyncedConfigDefinition configDefinition, [MaybeNullWhen(false)] out SyncedEntry entry) + { + if (TryGetValue((configFileRelativePath, configDefinition), out var entryBase)) + { + entry = (SyncedEntry)entryBase; + return true; + } + + entry = null; + return false; + } + + public bool TryGetEntry(string configFileRelativePath, string section, string key, [MaybeNullWhen(false)] out SyncedEntry entry) + { + return TryGetEntry(configFileRelativePath, new SyncedConfigDefinition(section, key), out entry); + } +} diff --git a/CSync/Lib/SyncedEntryDelta.cs b/CSync/Lib/SyncedEntryDelta.cs new file mode 100644 index 0000000..fd121cf --- /dev/null +++ b/CSync/Lib/SyncedEntryDelta.cs @@ -0,0 +1,60 @@ +using System; +using Unity.Collections; +using Unity.Netcode; + +namespace CSync.Lib; + +internal struct SyncedEntryDelta : INetworkSerializable, IEquatable +{ + public SyncedConfigDefinition Definition; + public FixedString128Bytes ConfigFileRelativePath; + public FixedString512Bytes SerializedValue; + + public SyncedEntryDelta(FixedString128Bytes configFileRelativePath, SyncedConfigDefinition definition, FixedString512Bytes serializedValue) + { + ConfigFileRelativePath = configFileRelativePath; + Definition = definition; + SerializedValue = serializedValue; + } + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + if (serializer.IsReader) + { + serializer.SerializeValue(ref Definition); + + var reader = serializer.GetFastBufferReader(); + reader.ReadValueSafe(out ConfigFileRelativePath); + reader.ReadValueSafe(out SerializedValue); + } + else + { + serializer.SerializeValue(ref Definition); + + var writer = serializer.GetFastBufferWriter(); + writer.WriteValueSafe(ConfigFileRelativePath); + writer.WriteValueSafe(SerializedValue); + } + } + + public bool Equals(SyncedEntryDelta other) + { + return Definition.Equals(other.Definition) && ConfigFileRelativePath.Equals(other.ConfigFileRelativePath) && SerializedValue.Equals(other.SerializedValue); + } + + public override bool Equals(object? obj) + { + return obj is SyncedEntryDelta other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(Definition, ConfigFileRelativePath, SerializedValue); + } + + public (string ConfigFileRelativePath, SyncedConfigDefinition Definition) SyncedEntryIdentifier { + get { + return (ConfigFileRelativePath.Value, Definition); + } + } +} diff --git a/CSync/Lib/SyncedInstance.cs b/CSync/Lib/SyncedInstance.cs index d16457f..30cf7a1 100644 --- a/CSync/Lib/SyncedInstance.cs +++ b/CSync/Lib/SyncedInstance.cs @@ -1,63 +1,8 @@ using CSync.Util; -using System; -using Unity.Netcode; namespace CSync.Lib; -/// -/// Generic class that can be serialized to bytes.

-/// Handles syncing and reverting as well as holding references to the client-side and synchronized instances.

-///



-/// This class should always be inherited from, never use it directly! -///
-[Serializable] -public class SyncedInstance : ByteSerializer where T : class { - public static CustomMessagingManager MessageManager => NetworkManager.Singleton.CustomMessagingManager; - public static bool IsClient => NetworkManager.Singleton.IsClient; - public static bool IsHost => NetworkManager.Singleton.IsHost; - - /// - /// The instance of the class used to fall back to when reverting.

- /// All of the properties on this instance are unsynced and always the same. - ///
- public static T Default { get; private set; } - - /// - /// The current instance of the class.

- /// Properties contained in this instance can be synchronized. - ///
- public static T Instance { get; private set; } - - /// - /// Invoked when deserialization of data has finished and is assigned to. - /// - [field:NonSerialized] public event EventHandler SyncComplete; - internal void OnSyncCompleted() => SyncComplete?.Invoke(this, EventArgs.Empty); - - /// - /// Invoked when is set back to and no longer synced. - /// - [field:NonSerialized] public event EventHandler SyncReverted; - internal void OnSyncReverted() => SyncReverted?.Invoke(this, EventArgs.Empty); - - public static bool Synced; - - public void InitInstance(T instance) { - Default = instance; - Instance = instance; - } - - public void SyncInstance(byte[] data) { - Instance = DeserializeFromBytes(data); - Synced = Instance != default(T); - - OnSyncCompleted(); - } - - public void RevertSync() { - Instance = Default; - Synced = false; - - OnSyncReverted(); - } -} \ No newline at end of file +public class SyncedInstance : ByteSerializer where T : class +{ + public static T? Instance { get; internal set; } +} diff --git a/CSync/Patches/GameNetworkManagerPatch.cs b/CSync/Patches/GameNetworkManagerPatch.cs new file mode 100644 index 0000000..9ce59ee --- /dev/null +++ b/CSync/Patches/GameNetworkManagerPatch.cs @@ -0,0 +1,22 @@ +using System.Diagnostics.CodeAnalysis; +using CSync.Lib; +using HarmonyLib; +using Unity.Netcode; + +namespace CSync.Patches; + +[SuppressMessage("ReSharper", "InconsistentNaming")] +[HarmonyPatch(typeof(GameNetworkManager))] +public static class GameNetworkManagerPatch +{ + [HarmonyPatch(nameof(GameNetworkManager.Start))] + [HarmonyPostfix] + public static void OnNetworkManagerStart(GameNetworkManager __instance) + { + ConfigManager.PopulateEntries(); + + if (NetworkManager.Singleton.NetworkConfig.Prefabs.Contains(ConfigManager.Prefab)) + return; + NetworkManager.Singleton.AddNetworkPrefab(ConfigManager.Prefab); + } +} diff --git a/CSync/Patches/JoinPatch.cs b/CSync/Patches/JoinPatch.cs deleted file mode 100644 index 8fb700d..0000000 --- a/CSync/Patches/JoinPatch.cs +++ /dev/null @@ -1,14 +0,0 @@ -using CSync.Lib; -using GameNetcodeStuff; -using HarmonyLib; - -namespace CSync.Patches; - -[HarmonyPatch(typeof(PlayerControllerB))] -internal class JoinPatch { - [HarmonyPostfix] - [HarmonyPatch("ConnectClientToPlayerObject")] - private static void SyncOnJoin() { - ConfigManager.SyncInstances(); - } -} diff --git a/CSync/Patches/LeavePatch.cs b/CSync/Patches/LeavePatch.cs deleted file mode 100644 index e71e3c9..0000000 --- a/CSync/Patches/LeavePatch.cs +++ /dev/null @@ -1,13 +0,0 @@ -using CSync.Lib; -using HarmonyLib; - -namespace CSync.Patches; - -[HarmonyPatch(typeof(GameNetworkManager))] -internal class LeavePatch { - [HarmonyPostfix] - [HarmonyPatch("StartDisconnect")] - private static void RevertOnDisconnect() { - ConfigManager.RevertSyncedInstances(); - } -} \ No newline at end of file diff --git a/CSync/Patches/StartOfRoundPatch.cs b/CSync/Patches/StartOfRoundPatch.cs new file mode 100644 index 0000000..f7a4dfd --- /dev/null +++ b/CSync/Patches/StartOfRoundPatch.cs @@ -0,0 +1,30 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using CSync.Lib; +using HarmonyLib; +using Unity.Netcode; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace CSync.Patches; + +[SuppressMessage("ReSharper", "InconsistentNaming")] +[HarmonyPatch(typeof(StartOfRound))] +public static class StartOfRoundPatch +{ + [HarmonyPatch(nameof(StartOfRound.Start))] + [HarmonyPostfix] + public static void OnSessionStart(StartOfRound __instance) + { + if (!__instance.IsOwner) return; + + try { + var configManagerGameObject = Object.Instantiate(ConfigManager.Prefab, __instance.transform); + configManagerGameObject.hideFlags = HideFlags.None; + configManagerGameObject.GetComponent().Spawn(); + } + catch (Exception exc) { + Plugin.Logger.LogError($"Failed to instantiate config sync behaviours:\n{exc}"); + } + } +} diff --git a/CSync/Plugin.cs b/CSync/Plugin.cs index b2ff2fc..29da868 100644 --- a/CSync/Plugin.cs +++ b/CSync/Plugin.cs @@ -14,9 +14,9 @@ namespace CSync; /// [BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)] public class Plugin : BaseUnityPlugin { - internal static new ManualLogSource Logger { get; private set; } + internal new static ManualLogSource Logger { get; private set; } = null!; - Harmony Patcher; + internal static Harmony Patcher = null!; private void Awake() { Logger = base.Logger; diff --git a/CSync/Util/ByteSerializer.cs b/CSync/Util/ByteSerializer.cs index 6671f33..7901d1f 100644 --- a/CSync/Util/ByteSerializer.cs +++ b/CSync/Util/ByteSerializer.cs @@ -1,35 +1,6 @@ -using System.IO; using System; -using System.Runtime.Serialization; namespace CSync.Util; -/// -/// Responsible for serializing to and from bytes via a .

-/// Uses as a fast and safer alternative to BinaryFormatter. -///
-[Serializable] -public class ByteSerializer { - [NonSerialized] - static readonly DataContractSerializer Serializer = new(typeof(T)); - - // Ensures the size of an integer is correct for the current system. - public static int IntSize => sizeof(int); - - public static byte[] SerializeToBytes(T val) { - using MemoryStream stream = new(); - - Serializer.WriteObject(stream, val); - return stream.ToArray(); - } - - public static T DeserializeFromBytes(byte[] data) { - using MemoryStream stream = new(data); - - try { - return (T) Serializer.ReadObject(stream); - } catch (Exception) { - return default; - } - } -} \ No newline at end of file +[Obsolete] +public class ByteSerializer; diff --git a/CSync/Util/Extensions.cs b/CSync/Util/Extensions.cs index 5bf2f5c..ff8f1af 100644 --- a/CSync/Util/Extensions.cs +++ b/CSync/Util/Extensions.cs @@ -1,77 +1,93 @@ +using System; using BepInEx.Configuration; +using CSync.Extensions; using CSync.Lib; -using System.Runtime.Serialization; -using Unity.Netcode; namespace CSync.Util; /// /// Contains helpful extension methods to aid with synchronization and reduce code duplication. /// +[Obsolete($"Use {nameof(SyncedBindingExtensions)} instead.")] public static class Extensions { /// /// Binds an entry to this file and returns the converted synced entry. /// - /// The currently selected config file. + /// The currently selected config file. /// The category that this entry should show under. /// The name/identifier of this entry. - /// The value assigned to this entry if not changed. - /// The description indicating what this entry does. - public static SyncedEntry BindSyncedEntry(this ConfigFile cfg, - string section, string key, V defaultVal, string desc + /// The value assigned to this entry if not changed. + /// The description indicating what this entry does. + [Obsolete($"Use {nameof(SyncedBindingExtensions)} instead.")] + public static SyncedEntry BindSyncedEntry( + this ConfigFile configFile, + string section, + string key, + T defaultValue, + string description ) { - return cfg.Bind(section, key, defaultVal, desc).ToSyncedEntry(); + return SyncedBindingExtensions.BindSyncedEntry( + configFile, + section, + key, + defaultValue, + description + ); } - public static SyncedEntry BindSyncedEntry(this ConfigFile cfg, - string section, string key, V defaultVal, ConfigDescription desc + [Obsolete($"Use {nameof(SyncedBindingExtensions)} instead.")] + public static SyncedEntry BindSyncedEntry( + this ConfigFile configFile, + string section, + string key, + T defaultValue, + ConfigDescription? description = null ) { - return cfg.BindSyncedEntry(section, key, defaultVal, desc.Description); + return SyncedBindingExtensions.BindSyncedEntry( + configFile, + section, + key, + defaultValue, + description + ); } - public static SyncedEntry BindSyncedEntry(this ConfigFile cfg, - ConfigDefinition definition, T defaultValue, ConfigDescription desc = null + [Obsolete($"Use {nameof(SyncedBindingExtensions)} instead.")] + public static SyncedEntry BindSyncedEntry( + this ConfigFile configFile, + ConfigDefinition definition, + T defaultValue, + string description ) { - return cfg.BindSyncedEntry(definition.Section, definition.Key, defaultValue, desc.Description); + return SyncedBindingExtensions.BindSyncedEntry( + configFile, + definition, + defaultValue, + description + ); } - public static SyncedEntry BindSyncedEntry(this ConfigFile cfg, - ConfigDefinition definition, T defaultValue, string desc + [Obsolete($"Use {nameof(SyncedBindingExtensions)} instead.")] + public static SyncedEntry BindSyncedEntry( + this ConfigFile configFile, + ConfigDefinition definition, + T defaultValue, + ConfigDescription? description = null ) { - return cfg.BindSyncedEntry(definition.Section, definition.Key, defaultValue, desc); + return SyncedBindingExtensions.BindSyncedEntry( + configFile, + definition, + defaultValue, + description + ); } /// /// Converts this entry into a serializable alternative, allowing it to be synced. /// - public static SyncedEntry ToSyncedEntry(this ConfigEntry entry) { - return new SyncedEntry(entry); - } - - /// - /// Helper method to grab a value from SerializationInfo and cast it to the specified type. - /// - public static T GetObject(this SerializationInfo info, string key) { - return (T) info.GetValue(key, typeof(T)); - } - - internal static ConfigEntry Reconstruct(this ConfigFile cfg, SerializationInfo info) { - ConfigDefinition definition = new(info.GetString("Section"), info.GetString("Key")); - ConfigDescription description = new(info.GetString("Description")); - - return cfg.Bind(definition, info.GetObject("DefaultValue"), description); - } - - internal static void SendMessage(this FastBufferWriter stream, string guid, string label, ulong clientId = 0uL) { - bool fragment = stream.Capacity > 1300; - NetworkDelivery delivery = fragment ? NetworkDelivery.ReliableFragmentedSequenced : NetworkDelivery.Reliable; - - if (fragment) Plugin.Logger.LogDebug( - $"{guid} - Size of stream ({stream.Capacity}) was past the max buffer size.\n" + - "Config instance will be sent in fragments to avoid overflowing the buffer." - ); - - var msgManager = NetworkManager.Singleton.CustomMessagingManager; - msgManager.SendNamedMessage($"{guid}_{label}", clientId, stream, delivery); + [Obsolete($"Use {nameof(SyncedBindingExtensions)} instead.")] + public static SyncedEntry ToSyncedEntry(this ConfigEntry entry) + { + return SyncedBindingExtensions.ToSyncedEntry(entry); } -} \ No newline at end of file +} diff --git a/Directory.Build.props b/Directory.Build.props index ed13220..25a5914 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -65,6 +65,7 @@ all + diff --git a/Directory.Build.targets b/Directory.Build.targets index 48c7dae..09b8185 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -13,4 +13,8 @@ $(MinVerMajor).$(MinVerMinor).$(MinVerPatch) - \ No newline at end of file + + + + +