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
+
+
+
+
+