diff --git a/MonkeyLoader/Configuration/ConfigKeyChangedEvent.cs b/MonkeyLoader/Configuration/ConfigKeyChangedEvent.cs index 612b379..00d5871 100644 --- a/MonkeyLoader/Configuration/ConfigKeyChangedEvent.cs +++ b/MonkeyLoader/Configuration/ConfigKeyChangedEvent.cs @@ -1,7 +1,7 @@ using System.Collections.Specialized; -using System.ComponentModel; using System; using System.Diagnostics.CodeAnalysis; +using MonkeyLoader.Meta; namespace MonkeyLoader.Configuration { @@ -24,14 +24,8 @@ namespace MonkeyLoader.Configuration /// Represents the data for the and events. /// /// The type of the key's value. - public sealed class ConfigKeyChangedEventArgs : IConfigKeyChangedEventArgs + public sealed class ConfigKeyChangedEventArgs : ValueChangedEventArgs, IConfigKeyChangedEventArgs { - /// - public NotifyCollectionChangedEventArgs? ChangedCollection { get; } - - /// - public string? ChangedProperty { get; } - /// public Config Config { get; } @@ -45,14 +39,6 @@ public sealed class ConfigKeyChangedEventArgs : IConfigKeyChangedEventArgs /// public bool HasValue { get; } - /// - [MemberNotNullWhen(true, nameof(ChangedCollection))] - public bool IsChangedCollection => ChangedCollection is not null; - - /// - [MemberNotNullWhen(true, nameof(ChangedProperty))] - public bool IsChangedProperty => ChangedProperty is not null; - /// /// Gets the configuration item who's value changed. /// @@ -63,22 +49,6 @@ public sealed class ConfigKeyChangedEventArgs : IConfigKeyChangedEventArgs /// public string? Label { get; } - /// - /// Gets the new value of the .
- /// This can be the default value. - ///
- public T? NewValue { get; } - - object? IConfigKeyChangedEventArgs.NewValue => NewValue; - - /// - /// Gets the old value of the .
- /// This can be the default value. - ///
- public T? OldValue { get; } - - object? IConfigKeyChangedEventArgs.OldValue => OldValue; - /// /// Creates a new event args instance for a changed config item. /// @@ -94,41 +64,23 @@ public sealed class ConfigKeyChangedEventArgs : IConfigKeyChangedEventArgs public ConfigKeyChangedEventArgs(Config config, IDefiningConfigKey key, bool hadValue, T? oldValue, bool hasValue, T? newValue, string? label, string? changedProperty, NotifyCollectionChangedEventArgs? changedCollection) + : base(oldValue, newValue, changedProperty, changedCollection) { Config = config; Key = key; - OldValue = oldValue; HadValue = hadValue; - NewValue = newValue; HasValue = hasValue; Label = label; - - ChangedProperty = changedProperty; - ChangedCollection = changedCollection; } } /// /// Represents a non-generic . /// - public interface IConfigKeyChangedEventArgs + public interface IConfigKeyChangedEventArgs : IValueChangedEventArgs { - /// - /// Gets the changed collection event arguments, - /// if this configuration item's changed value originated - /// from an event. - /// - public NotifyCollectionChangedEventArgs? ChangedCollection { get; } - - /// - /// Gets the name of the property that changed, - /// if this configuration item's changed value originated - /// from an event. - /// - public string? ChangedProperty { get; } - /// /// Gets the in which the change occured. /// @@ -153,26 +105,6 @@ public interface IConfigKeyChangedEventArgs /// public bool HasValue { get; } - /// - /// Gets whether this configuration item's changed value originated - /// from an event. - /// - /// - /// true if ChangedCollection is not null; otherwise, false. - /// - [MemberNotNullWhen(true, nameof(ChangedCollection))] - public bool IsChangedCollection { get; } - - /// - /// Gets whether this configuration item's changed value originated - /// from an event. - /// - /// - /// true if ChangedProperty is not null; otherwise, false. - /// - [MemberNotNullWhen(true, nameof(ChangedProperty))] - public bool IsChangedProperty { get; } - /// /// Gets the configuration item who's value changed. /// @@ -182,17 +114,5 @@ public interface IConfigKeyChangedEventArgs /// Gets a custom label that may be set by whoever changed the configuration. /// public string? Label { get; } - - /// - /// Gets the new value of the configuration item.
- /// This can be the default value. - ///
- public object? NewValue { get; } - - /// - /// Gets the old value of the configuration item.
- /// This can be the default value. - ///
- public object? OldValue { get; } } } \ No newline at end of file diff --git a/MonkeyLoader/Configuration/DefiningConfigKey.cs b/MonkeyLoader/Configuration/DefiningConfigKey.cs index 00d05bf..128e7e7 100644 --- a/MonkeyLoader/Configuration/DefiningConfigKey.cs +++ b/MonkeyLoader/Configuration/DefiningConfigKey.cs @@ -29,13 +29,16 @@ namespace MonkeyLoader.Configuration /// The type of the config item's value. public sealed class DefiningConfigKey : Entity>, IDefiningConfigKey { - private readonly bool _canAlwaysHaveChanges; + private static readonly Type _valueType = typeof(T); + private readonly bool _canAlwaysHaveChanges; private readonly Lazy _fullId; private ConfigSection? _configSection; private bool _hasChanges; private ConfigKeyChangedEventHandler? _untypedChanged; + private ValueChangedEventHandler? _untypedValueChanged; private T? _value; + private ValueChangedEventHandler? _valueChanged; /// public IConfigKey AsUntyped { get; } @@ -117,7 +120,7 @@ public ConfigSection Section IDefiningConfigKey IEntity>.Self => this; /// - public Type ValueType { get; } = typeof(T); + public Type ValueType => _valueType; /// /// Gets the logger of the config this item belongs to. @@ -367,27 +370,34 @@ public bool Validate(T value) /// The collection change arguments for the value. private void OnChanged(bool hadValue, T? oldValue, string? eventLabel, string? changedProperty = null, NotifyCollectionChangedEventArgs? changedCollection = null) { - // Add notify changed integration - if (hadValue) + var sameReferences = ReferenceEquals(oldValue, _value); + + if (!sameReferences) { - if (oldValue is INotifyPropertyChanged oldPropertyChanged) - oldPropertyChanged.PropertyChanged -= ValuePropertyChanged; + // Remove NotifyChanged integration from old value + if (hadValue) + { + if (oldValue is INotifyPropertyChanged oldPropertyChanged) + oldPropertyChanged.PropertyChanged -= ValuePropertyChanged; - if (oldValue is INotifyCollectionChanged oldCollectionChanged) - oldCollectionChanged.CollectionChanged -= ValueCollectionChanged; - } + if (oldValue is INotifyCollectionChanged oldCollectionChanged) + oldCollectionChanged.CollectionChanged -= ValueCollectionChanged; + } - if (HasValue) - { - if (_value is INotifyPropertyChanged newPropertyChanged) - newPropertyChanged.PropertyChanged += ValuePropertyChanged; + // Add NotifyChanged integration to new value + if (HasValue) + { + if (_value is INotifyPropertyChanged newPropertyChanged) + newPropertyChanged.PropertyChanged += ValuePropertyChanged; - if (_value is INotifyCollectionChanged newCollectionChanged) - newCollectionChanged.CollectionChanged += ValueCollectionChanged; + if (_value is INotifyCollectionChanged newCollectionChanged) + newCollectionChanged.CollectionChanged += ValueCollectionChanged; + } } - // Don't fire event if value didn't change - if (ReferenceEquals(oldValue, _value) || (oldValue is not null && _value is not null && _value.Equals(oldValue))) + // Don't fire event if it wasn't triggered by event and the value didn't change + if ((sameReferences && changedProperty is null && changedCollection is null) + || (oldValue is not null && _value is not null && _value.Equals(oldValue))) return; HasChanges = true; @@ -411,6 +421,24 @@ private void OnChanged(bool hadValue, T? oldValue, string? eventLabel, string? c Logger.Error(ex.LogFormat($"Some untyped {nameof(Changed)} event subscriber(s) of key [{Id}] threw an exception:")); } + try + { + _valueChanged?.TryInvokeAll(this, eventArgs); + } + catch (AggregateException ex) + { + Logger.Error(ex.LogFormat($"Some typed {nameof(INotifyValueChanged)}.{nameof(Changed)} event subscriber(s) of key [{Id}] threw an exception:")); + } + + try + { + _untypedValueChanged?.TryInvokeAll(this, eventArgs); + } + catch (AggregateException ex) + { + Logger.Error(ex.LogFormat($"Some untyped {nameof(INotifyValueChanged)}.{nameof(Changed)} event subscriber(s) of key [{Id}] threw an exception:")); + } + Section.OnItemChanged(eventArgs); } @@ -431,13 +459,25 @@ event ConfigKeyChangedEventHandler? IDefiningConfigKey.Changed add => _untypedChanged += value; remove => _untypedChanged -= value; } + + event ValueChangedEventHandler? INotifyValueChanged.Changed + { + add => _untypedValueChanged += value; + remove => _untypedValueChanged -= value; + } + + event ValueChangedEventHandler? INotifyValueChanged.Changed + { + add => _valueChanged += value; + remove => _valueChanged -= value; + } } /// /// Defines the definition for a config item. /// public interface IDefiningConfigKey : ITypedConfigKey, IEntity, - INestedIdentifiable, IPrioritizable + INestedIdentifiable, IPrioritizable, INotifyValueChanged { /// /// Gets the config this item belongs to. @@ -541,14 +581,15 @@ public interface IDefiningConfigKey : ITypedConfigKey, IEntity /// Triggered when the internal value of this config item changes. /// - public event ConfigKeyChangedEventHandler? Changed; + public new event ConfigKeyChangedEventHandler? Changed; } /// /// Defines the typed definition for a config item. /// /// The type of the config item's value. - public interface IDefiningConfigKey : IDefiningConfigKey, ITypedConfigKey, IEntity> + public interface IDefiningConfigKey : IDefiningConfigKey, ITypedConfigKey, + IEntity>, INotifyValueChanged { /// /// Gets this config item's set value, falling back to the computed default. diff --git a/MonkeyLoader/Meta/NotifyValueChangedEvent.cs b/MonkeyLoader/Meta/NotifyValueChangedEvent.cs new file mode 100644 index 0000000..d641543 --- /dev/null +++ b/MonkeyLoader/Meta/NotifyValueChangedEvent.cs @@ -0,0 +1,159 @@ +using MonkeyLoader.Configuration; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace MonkeyLoader.Meta +{ + /// + /// The delegate that is called for an 's + /// changed event. + /// + /// The object that sent the event. + /// The event containing details about the change. + public delegate void ValueChangedEventHandler(object sender, IValueChangedEventArgs valueChangedEventArgs); + + /// + /// The delegate that is called for an 's + /// changed event. + /// + /// The type of the key's value. + /// The object that sent the event. + /// The event containing details about the change. + public delegate void ValueChangedEventHandler(object sender, ValueChangedEventArgs valueChangedEventArgs); + + /// + /// Defines the generic interface for objects that wrap another value + /// and notify others about changes to it. + /// + /// The type of the wrapped value. + public interface INotifyValueChanged : INotifyValueChanged + { + /// + /// Triggered when the internal value wrapped by this object changes. + /// + public new event ValueChangedEventHandler? Changed; + } + + /// + /// Defines the non-generic interface for objects that wrap another value + /// and notify others about changes to it. + /// + public interface INotifyValueChanged + { + /// + /// Triggered when the internal value wrapped by this object changes. + /// + public event ValueChangedEventHandler? Changed; + } + + /// + /// Represents a non-generic . + /// + public interface IValueChangedEventArgs + { + /// + /// Gets the changed collection event arguments, + /// if this configuration item's changed value originated + /// from an event. + /// + public NotifyCollectionChangedEventArgs? ChangedCollection { get; } + + /// + /// Gets the name of the property that changed, + /// if this configuration item's changed value originated + /// from an event. + /// + public string? ChangedProperty { get; } + + /// + /// Gets whether this configuration item's changed value originated + /// from an event. + /// + /// + /// true if ChangedCollection is not null; otherwise, false. + /// + [MemberNotNullWhen(true, nameof(ChangedCollection))] + public bool IsChangedCollection { get; } + + /// + /// Gets whether this configuration item's changed value originated + /// from an event. + /// + /// + /// true if ChangedProperty is not null; otherwise, false. + /// + [MemberNotNullWhen(true, nameof(ChangedProperty))] + public bool IsChangedProperty { get; } + + /// + /// Gets the new value of the configuration item.
+ /// This can be the default value. + ///
+ public object? NewValue { get; } + + /// + /// Gets the old value of the configuration item.
+ /// This can be the default value. + ///
+ public object? OldValue { get; } + } + + /// + /// Represents the data for the + /// and events. + /// + /// The type of the wrapped value. + public class ValueChangedEventArgs : IValueChangedEventArgs + { + /// + public NotifyCollectionChangedEventArgs? ChangedCollection { get; } + + /// + public string? ChangedProperty { get; } + + /// + [MemberNotNullWhen(true, nameof(ChangedCollection))] + public bool IsChangedCollection => ChangedCollection is not null; + + /// + [MemberNotNullWhen(true, nameof(ChangedProperty))] + public bool IsChangedProperty => ChangedProperty is not null; + + /// + /// Gets the new value of the .
+ /// This can be the default value. + ///
+ public T? NewValue { get; } + + object? IValueChangedEventArgs.NewValue => NewValue; + + /// + /// Gets the old value of the .
+ /// This can be the default value. + ///
+ public T? OldValue { get; } + + object? IValueChangedEventArgs.OldValue => OldValue; + + /// + /// Creates a new event args instance for a changed config item. + /// + /// The optional old value. + /// The optional new value. + /// The name of the changed property on the value. + /// The collection change arguments for the value. + public ValueChangedEventArgs(T? oldValue, T? newValue, + string? changedProperty, NotifyCollectionChangedEventArgs? changedCollection) + { + OldValue = oldValue; + NewValue = newValue; + + ChangedProperty = changedProperty; + ChangedCollection = changedCollection; + } + } +} \ No newline at end of file diff --git a/MonkeyLoader/MonkeyLoader.csproj b/MonkeyLoader/MonkeyLoader.csproj index 7c6495b..c1df3f2 100644 --- a/MonkeyLoader/MonkeyLoader.csproj +++ b/MonkeyLoader/MonkeyLoader.csproj @@ -11,7 +11,7 @@ True MonkeyLoader Banane9 - 0.25.0-beta + 0.24.14-MonkeySync-beta A convenience and extendability focused mod loader using NuGet packages. README.md LGPL-3.0-or-later @@ -77,6 +77,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/MonkeyLoader/Sync/IgnoreSyncValueAttribute.cs b/MonkeyLoader/Sync/IgnoreSyncValueAttribute.cs new file mode 100644 index 0000000..8fd5cd0 --- /dev/null +++ b/MonkeyLoader/Sync/IgnoreSyncValueAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace MonkeyLoader.Sync +{ + /// + /// Marks a field or property with a compatible type + /// on a class deriving from + /// to be excluded from the automatically detected fields and properties. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public sealed class IgnoreSyncValueAttribute : MonkeyLoaderAttribute + { } +} \ No newline at end of file diff --git a/MonkeyLoader/Sync/MonkeySyncMethod.cs b/MonkeyLoader/Sync/MonkeySyncMethod.cs new file mode 100644 index 0000000..56789a3 --- /dev/null +++ b/MonkeyLoader/Sync/MonkeySyncMethod.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MonkeyLoader.Sync +{ + /// + /// The delegate that is represented by MonkeySync methods. + /// + public delegate void MonkeySyncAction(); + + /// + /// The delegate that is represented by MonkeySync methods. + /// + /// The value that triggered this method. + /// + public delegate void MonkeySyncFunc(T trigger); + + /// + /// Defines the non-generic interface for s. + /// + /// + public interface ILinkedMonkeySyncMethod : ILinkedMonkeySyncValue + where TSyncObject : ILinkedMonkeySyncObject + { + } + + /// + /// Defines the generic interface for linked s. + /// + /// The type of the link object used by the sync object that this sync value links to. + /// The type of the sync object that may contain this sync value. + /// The type of the Value that can trigger this sync method. + public interface ILinkedMonkeySyncMethod : ILinkedMonkeySyncMethod, ILinkedMonkeySyncValue + where TSyncObject : ILinkedMonkeySyncObject + { + /// + /// Gets the delegate that can be triggered by this sync method. + /// + public MonkeySyncFunc Function { get; } + } + + /// + /// Defines the interface for not yet linked s. + /// + /// + public interface IUnlinkedMonkeySyncMethod : IUnlinkedMonkeySyncValue + where TSyncObject : ILinkedMonkeySyncObject + where TLinkedSyncValue : ILinkedMonkeySyncValue + { } + + /// + /// Implements an abstract example for s. + /// + /// + /// This class is in all likelihood not useful to actually derive from.
+ /// It mainly serves as an example for how an implementation could look. + ///
+ /// + public abstract class MonkeySyncMethod : MonkeySyncValue, + IUnlinkedMonkeySyncMethod, + ILinkedMonkeySyncMethod + where TSyncObject : class, ILinkedMonkeySyncObject + where TSyncValue : MonkeySyncMethod + { + /// + public MonkeySyncFunc Function { get; } + + /// + /// Creates a new sync method instance that wraps the given , + /// changes of which can trigger the target . + /// + /// The delegate that can be triggered by this sync method. + /// The value to wrap. + protected MonkeySyncMethod(MonkeySyncFunc function, T value) : base(value) + { + Function = function; + } + + /// + /// Creates a new sync method instance that wraps the given , + /// changes of which can trigger the target . + /// + /// The delegate that can be triggered by this sync method. + /// The value to wrap. + protected MonkeySyncMethod(MonkeySyncAction action, T value) + : this(_ => action(), value) + { } + + TSyncValue? IUnlinkedMonkeySyncValue.EstablishLinkFor(TSyncObject syncObject, string name, bool fromRemote) + => EstablishLinkFor(syncObject, name, fromRemote); + + /// + /// Creates structures that make this method triggerable by others locally or in shared environments, + /// whether they have the mod implementing the method or not. + /// + public abstract void MakeInvocation(); + } +} \ No newline at end of file diff --git a/MonkeyLoader/Sync/MonkeySyncObject.cs b/MonkeyLoader/Sync/MonkeySyncObject.cs new file mode 100644 index 0000000..76418b9 --- /dev/null +++ b/MonkeyLoader/Sync/MonkeySyncObject.cs @@ -0,0 +1,393 @@ +using EnumerableToolkit; +using HarmonyLib; +using MonkeyLoader.Meta; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace MonkeyLoader.Sync +{ + /// + /// Represents the method that will create new instances + /// of sync objects linking via . + /// + /// The type of the link object used by the sync object. + /// The created but not yet linked sync object. + public delegate IUnlinkedMonkeySyncObject SyncObjectFactory() + where TLink : class; + + /// + /// Defines the generic interface for MonkeySync objects that have been linked. + /// + /// The type of the link object used by the sync object. + public interface ILinkedMonkeySyncObject : IMonkeySyncObject + { + /// + public new TLink LinkObject { get; } + } + + /// + /// Defines the non-generic interface for MonkeySync objects. + /// + public interface IMonkeySyncObject : INotifyPropertyChanged, IDisposable + { + /// + /// Gets whether this sync object has a link object. + /// + [MemberNotNullWhen(true, nameof(LinkObject))] + public bool HasLinkObject { get; } + + /// + /// Gets whether this sync object has a valid link. + /// + [MemberNotNullWhen(true, nameof(LinkObject))] + public bool IsLinkValid { get; } + + /// + /// Gets the link object used by this sync object. + /// + public object LinkObject { get; } + } + + /// + /// Defines the generic interface for MonkeySync objects that are yet to be linked. + /// + /// + public interface IUnlinkedMonkeySyncObject : ILinkedMonkeySyncObject + where TLink : class + { + /// + /// Establishes this sync object's link with the given object.
+ /// If the link is successfully created, the now linked sync object will be + /// added + /// to the . + ///
+ /// + /// If the link fails or gets broken, a new instance has to be created. + /// + /// The link object to be used by this sync object. + /// Whether the link is being established from the remote side. + /// true if the established link is valid; otherwise, false. + public bool LinkWith(TLink linkObject, bool fromRemote = false); + } + + /// + /// Implements the abstract base for MonkeySync objects. + /// + /// + /// Automatically detects any instance fields / properties containing + /// MonkeySync values or methods compatible with .
+ /// Property names take precedence over their backing fields.
+ /// Use the to mark fields / properties that should be ignored. + /// If a property is marked, its backing field will be ignored too. + ///
+ /// The concrete type of the MonkeySync object. + /// + /// The -derived interface + /// that the unlinked MonkeySync values of this object must implement. + /// + /// + /// The -derived interface + /// that the linked MonkeySync values of this object must implement. + /// + /// The type of the link object used by the sync object. + public abstract class MonkeySyncObject : IUnlinkedMonkeySyncObject + where TSyncObject : MonkeySyncObject + where TLinkedSyncValue : ILinkedMonkeySyncValue> + where TUnlinkedSyncValue : IUnlinkedMonkeySyncValue, TLinkedSyncValue> + where TLink : class + { + /// + /// The getters for the automatically detected instance fields / properties by their name. + /// + /// + /// Property names take precedence over their backing fields.
+ /// Use the to mark fields / properties that should be ignored. + /// If a property is marked, its backing field will be ignored too. + ///
+ protected static readonly Dictionary> syncValueAccessorsByName = new(StringComparer.Ordinal); + + /// + /// The instances associated with this sync object. + /// + protected readonly HashSet syncValues = []; + + private bool _disposedValue; + + /// + [MemberNotNullWhen(true, nameof(LinkObject))] + public bool HasLinkObject => LinkObject is not null; + + /// + [MemberNotNullWhen(true, nameof(LinkObject))] + public abstract bool IsLinkValid { get; } + + /// + public TLink LinkObject { get; private set; } = null!; + + object IMonkeySyncObject.LinkObject => LinkObject; + + static MonkeySyncObject() + { + var syncValueType = typeof(TUnlinkedSyncValue); + var syncValueFields = typeof(TSyncObject).GetFields(AccessTools.all) + .Where(field => !field.IsStatic && syncValueType.IsAssignableFrom(field.FieldType) && field.GetCustomAttribute() is null); + + var syncValueProperties = typeof(TSyncObject).GetProperties(AccessTools.all) + .Where(property => syncValueType.IsAssignableFrom(property.PropertyType) && (!(property.GetGetMethod()?.IsStatic ?? true))); + + foreach (var field in syncValueFields) + syncValueAccessorsByName.Add(field.Name, (TSyncObject instance) => (TUnlinkedSyncValue)field.GetValue(instance)); + + foreach (var property in syncValueProperties) + { + var fieldName = $"<{property.Name}>"; + var potentialField = syncValueAccessorsByName.FirstOrDefault(entry => entry.Key.Contains(fieldName)); + var hasIgnoreAttribute = property.GetCustomAttribute() is null; + + if (potentialField.Value is not null) + { + syncValueAccessorsByName.Remove(potentialField.Key); + + if (!hasIgnoreAttribute) + syncValueAccessorsByName.Add(property.Name, potentialField.Value); + + continue; + } + + if (!hasIgnoreAttribute) + syncValueAccessorsByName.Add(property.Name, (TSyncObject instance) => (TUnlinkedSyncValue)property.GetValue(instance)); + } + } + + /// + /// Initializes a new instance of this MonkeySync object. + /// + /// + /// When is not the type being instantiated. + /// + protected MonkeySyncObject() + { + if (GetType() != typeof(TSyncObject)) + throw new InvalidOperationException("TSyncObject must be the concrete Type being instantiated!"); + } + + /// + /// Ensures any unmanaged resources are disposed. + /// + ~MonkeySyncObject() + { + Dispose(false); + } + + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in the 'OnDisposing()' or 'OnFinalizing()' methods + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public bool LinkWith(TLink linkObject, bool fromRemote = false) + { + if (HasLinkObject) + throw new InvalidOperationException("Can only assign a link object once!"); + + LinkObject = linkObject; + + if (!EstablishLink(fromRemote)) + return false; + + MonkeySyncRegistry.RegisterLinkedSyncObject(this); + return true; + } + + /// + /// By default: Sets up the event handlers + /// and calls EstablishLinkFor + /// for every instance field / property on . + /// + /// The detected fields / properties are stored in syncValueAccessorsByName. + /// + /// This method is called by LinkWith + /// after the LinkObject has been assigned.
+ /// It should ensure that a link object created from the remote side + /// is handled appropriately and without duplications as well. + ///
+ ///
+ /// + protected virtual bool EstablishLink(bool fromRemote) + { + var success = true; + + foreach (var syncValueAccessor in syncValueAccessorsByName) + { + var syncValue = syncValueAccessor.Value((TSyncObject)this); + + success &= EstablishLinkFor(syncValue, syncValueAccessor.Key, fromRemote); + } + + return success; + } + + /// + /// Creates a link for the given sync value of the given name. + /// + /// + /// By default: Adds the given sync value to the set of instances and calls + /// .EstablishLinkFor(…).
+ /// If successful, the handler is subscribed to + /// the event of the linked value as well. + ///
+ /// The sync value to link. + /// The name of the sync value to link. + /// Whether the link is being established from the remote side. + /// true if the link was successfully created; otherwise, false. + protected virtual bool EstablishLinkFor(TUnlinkedSyncValue unlinkedSyncValue, string propertyName, bool fromRemote) + { + var linkedValue = unlinkedSyncValue.EstablishLinkFor((TSyncObject)this, propertyName, fromRemote); + + if (linkedValue is null) + return false; + + syncValues.Add(linkedValue); + linkedValue.Changed += (sender, changedArgs) + => OnPropertyChanged(propertyName); + + return true; + } + + /// + /// Cleans up any managed resources as part of disposing. + /// + /// + /// By default: Disposes all instances + /// that were added to the set of instances, + /// and the LinkObject if it's . + /// + protected virtual void OnDisposing() + { + foreach (var syncValue in syncValues) + syncValue.Dispose(); + + if (LinkObject is IDisposable disposable) + disposable.Dispose(); + } + + /// + /// Cleans up any unmanaged resources as part of + /// disposing or finalization. + /// + protected virtual void OnFinalizing() + { } + + /// + /// Tries to restore the link when it becomes invalidated + /// and triggers the Invalidated when that fails.
+ /// Afterwards, the object is automatically disposed. + ///
+ /// + /// Should be called from a derived class when something happens + /// that makes IsLinkValid false. + /// + protected void OnLinkInvalidated() + { + if (!IsLinkValid && TryRestoreLink() && IsLinkValid) + return; + + try + { + Invalidated.TryInvokeAll(); + } + catch { } + + Dispose(); + } + + /// + /// Triggers the PropertyChanged + /// event with the given . + /// + /// + /// This is automatically called for any fields / properties. + /// + /// The name of the property that changed. + protected void OnPropertyChanged(string propertyName) + { + var eventData = new PropertyChangedEventArgs(propertyName); + + PropertyChanged?.Invoke(this, eventData); + } + + /// + /// By default: Calls TryRestoreLinkFor + /// for every in . + /// + /// This method is called by OnLinkInvalidated + /// if IsLinkValid has become false.
+ /// It should ensure that any still valid links are + /// handled appropriately and without duplications as well. + ///
+ ///
+ /// + protected virtual bool TryRestoreLink() + { + var success = true; + + foreach (var syncValue in syncValues) + success &= TryRestoreLinkFor(syncValue); + + return success; + } + + /// + /// Tries to restore the link for the given sync value. + /// + /// + /// By default: Calls + /// .TryRestoreLink(). + /// + /// The sync value to link. + /// true if the link was successfully restored; otherwise, false. + protected virtual bool TryRestoreLinkFor(TLinkedSyncValue syncValue) + => syncValue.TryRestoreLink(); + + private void Dispose(bool disposing) + { + if (_disposedValue) + return; + + if (disposing) + OnDisposing(); + + OnFinalizing(); + + _disposedValue = true; + } + + /// + /// Occurs when IsLinkValid becomes false + /// and it could not be restored. + /// + public event InvalidatedHandler? Invalidated; + + /// + /// Represents the method that will handle the Invalidated + /// event raised when IsLinkValid becomes false + /// and it could not be restored. + /// + /// The sync object that got invalidated. + public delegate void InvalidatedHandler(TSyncObject syncObject); + + /// + public event PropertyChangedEventHandler? PropertyChanged; + } +} \ No newline at end of file diff --git a/MonkeyLoader/Sync/MonkeySyncObjectRegistration.cs b/MonkeyLoader/Sync/MonkeySyncObjectRegistration.cs new file mode 100644 index 0000000..7c4b2ae --- /dev/null +++ b/MonkeyLoader/Sync/MonkeySyncObjectRegistration.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MonkeyLoader.Sync +{ + /// + /// A simple data class that allows the to track the known + /// MonkeySync object types. + /// + /// The type of the link object used by the sync object. + public sealed class MonkeySyncObjectRegistration + where TLink : class + { + private readonly SyncObjectFactory _createSyncObject; + + /// + /// Gets the -unique name for the registered + /// MonkeySync object type. + /// + public string Name { get; } + + /// + /// Gets the of the registered + /// MonkeySync object. + /// + public Type SyncObjectType { get; } + + /// + /// Creates a new instance of this data class with the given details for a + /// MonkeySync object type. + /// + /// The -unique name for the sync object type. + /// The type of the sync object. + /// A factory method that creates new instances of this sync object type. + public MonkeySyncObjectRegistration(string name, Type syncObjectType, SyncObjectFactory createSyncObject) + { + Name = name; + SyncObjectType = syncObjectType; + _createSyncObject = createSyncObject; + } + + /// + /// Creates a new instance of the registered + /// MonkeySync object. + /// + /// The created but not yet linked sync object. + public IUnlinkedMonkeySyncObject CreateSyncObject() + => _createSyncObject(); + } +} \ No newline at end of file diff --git a/MonkeyLoader/Sync/MonkeySyncObjectRegistry.cs b/MonkeyLoader/Sync/MonkeySyncObjectRegistry.cs new file mode 100644 index 0000000..832757e --- /dev/null +++ b/MonkeyLoader/Sync/MonkeySyncObjectRegistry.cs @@ -0,0 +1,266 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text; + +namespace MonkeyLoader.Sync +{ + /// + /// Handles globally registering MonkeySync objects for particular link types, + /// so that the link implementations can create and reference them. + /// + public static class MonkeySyncRegistry + { + /// + /// Gets the MonkeySync + /// object linked with the given link object. + /// + /// The found MonkeySync object. + /// When no sync object is registered for the link object. + /// + public static ILinkedMonkeySyncObject GetLinkedSyncObject(TLink linkObject) + where TLink : class + { + if (Library.TryGetLinkedSyncObject(linkObject, out var syncObject)) + return syncObject; + + throw new KeyNotFoundException("No sync object found for the given link object!"); + } + + /// + /// Gets the for the given . + /// + /// The type of registered sync object. + /// + public static MonkeySyncObjectRegistration GetSyncObjectRegistration(Type syncObjectType) + where TLink : class + => Library.GetSyncObjectRegistration(syncObjectType); + + /// + /// Gets the for the given . + /// + /// The type of the link object used by the sync object. + /// The name of the registered sync object. + /// The found . + /// When no sync object with the given has been registered. + public static MonkeySyncObjectRegistration GetSyncObjectRegistration(string name) + where TLink : class + => Library.GetSyncObjectRegistration(name); + + /// + /// Determines whether there is a registered MonkeySync + /// object that is linked to the given link object. + /// + /// + public static bool HasLinkedSyncObject(TLink linkObject) + where TLink : class + => Library.TryGetLinkedSyncObject(linkObject, out _); + + /// + /// Registers a linked MonkeySync + /// object so that it can be accessed + /// through a reference to its link object. + /// + /// + /// The link is held in a - + /// as such, the sync object may be garbage collected when the link object is. + /// + /// The type of the link object used by the sync object. + /// The linked sync object to register. + public static void RegisterLinkedSyncObject(ILinkedMonkeySyncObject syncObject) + where TLink : class + => Library.RegisterLinkedSyncObject(syncObject); + + /// + /// Registers a MonkeySync object type with the given details for it. + /// + /// The type of the link object used by the sync object. + /// The -unique name for the sync object type. + /// The type of the sync object. + /// A factory method that creates new instances of this sync object type. + /// The data of the newly registered sync object type. + /// + /// When the or one with the same + /// has already been registered. + /// + public static MonkeySyncObjectRegistration RegisterSyncObject(string name, Type syncObjectType, SyncObjectFactory createSyncObject) + where TLink : class + => Library.RegisterSyncObject(name, syncObjectType, createSyncObject); + + /// + /// Tries to get the MonkeySync + /// object linked with the given link object. + /// + /// The type of the link object used by the sync object. + /// The link object used by the sync object. + /// The found MonkeySync object; otherwise, null. + /// true if a sync object is registered for the given link object; otherwise, false. + public static bool TryGetLinkedSyncObject(TLink linkObject, [NotNullWhen(true)] out ILinkedMonkeySyncObject? syncObject) + where TLink : class + => Library.TryGetLinkedSyncObject(linkObject, out syncObject); + + /// + /// Tries to get the for the given . + /// + /// The type of the link object used by the sync object. + /// The name of the registered sync object. + /// The found ; otherwise, null. + /// true if the registered sync object type was found; otherwise, false. + public static bool TryGetSyncObjectRegistration(string name, [NotNullWhen(true)] out MonkeySyncObjectRegistration? registeredSyncObject) + where TLink : class + => Library.TryGetSyncObjectRegistration(name, out registeredSyncObject); + + /// + /// Tries to get the for the given . + /// + /// The type of the sync object. + /// The found ; otherwise, null. + /// true if the registered sync object type was found; otherwise, false. + public static bool TryGetSyncObjectRegistration(Type syncObjectType, [NotNullWhen(true)] out MonkeySyncObjectRegistration? registeredSyncObject) + where TLink : class + => Library.TryGetSyncObjectRegistration(syncObjectType, out registeredSyncObject); + + /// + /// Unregisters a linked MonkeySync + /// object so that it can't be accessed + /// through a reference to its link object anymore. + /// + /// The type of the link object used by the sync object. + /// The linked sync object to unregister. + public static bool UnregisterLinkedSyncObject(ILinkedMonkeySyncObject syncObject) + where TLink : class + => Library.UnregisterLinkedSyncObject(syncObject); + + /// + /// Removes the registered MonkeySync + /// object that was linked to the given link object, + /// so that it can't be accessed through a reference to it anymore.
+ /// Optionally disposes of the sync object if necessary. + ///
+ /// The type of the link object used by the sync object. + /// The link object used by the sync object. + /// Whether to dispose the dispose of the sync object if necessary. + /// true if a sync object was registered for the given link object; otherwise, false. + public static bool UnregisterLinkedSyncObject(TLink linkObject, bool dispose = false) + where TLink : class + => Library.UnregisterLinkedSyncObject(linkObject, dispose); + + /// + /// Removes the sync object type with the given . + /// + /// The type of the link object used by the sync object. + /// The name of the registered sync object. + /// + /// true if the registered sync object type with the given was removed; otherwise, false. + /// + /// + /// When there's a registered sync object type with the given , + /// but none with the associated - or there's a Name mismatch. + /// + public static bool UnregisterSyncObject(string name) + where TLink : class + => Library.UnregisterSyncObject(name); + + /// + /// Removes the sync object type for the given . + /// + /// The type of the link object used by the sync object. + /// The type of the registered sync object. + /// + /// true if the registered sync object type for the given was removed; otherwise, false. + /// + /// + /// When there's a registered sync object type for the given , + /// but none with the associated Name - or there's a Type mismatch. + /// + public static bool UnregisterSyncObject(Type syncObjectType) + where TLink : class + => Library.UnregisterSyncObject(syncObjectType); + + private static class Library + where TLink : class + { + private static readonly Dictionary> _registeredObjectByType = []; + private static readonly Dictionary> _registeredObjectsByName = new(StringComparer.Ordinal); + private static readonly ConditionalWeakTable> _syncObjectsByLinkObject = new(); + + public static MonkeySyncObjectRegistration GetSyncObjectRegistration(Type type) + => _registeredObjectByType[type]; + + public static MonkeySyncObjectRegistration GetSyncObjectRegistration(string name) + => _registeredObjectsByName[name]; + + public static void RegisterLinkedSyncObject(ILinkedMonkeySyncObject syncObject) + => _syncObjectsByLinkObject.Add(syncObject.LinkObject, syncObject); + + public static MonkeySyncObjectRegistration RegisterSyncObject(string name, Type syncObjectType, SyncObjectFactory createSyncObject) + { + if (_registeredObjectsByName.ContainsKey(name) || _registeredObjectByType.ContainsKey(syncObjectType)) + throw new ArgumentException($"Sync Object type [{syncObjectType.CompactDescription()}] with name [{name}] has already been registered!"); + + var registeredObject = new MonkeySyncObjectRegistration(name, syncObjectType, createSyncObject); + + _registeredObjectsByName.Add(name, registeredObject); + _registeredObjectByType.Add(syncObjectType, registeredObject); + + return registeredObject; + } + + public static bool TryGetLinkedSyncObject(TLink linkObject, [NotNullWhen(true)] out ILinkedMonkeySyncObject? syncObject) + => _syncObjectsByLinkObject.TryGetValue(linkObject, out syncObject); + + public static bool TryGetSyncObjectRegistration(string name, [NotNullWhen(true)] out MonkeySyncObjectRegistration? registeredSyncObject) + => _registeredObjectsByName.TryGetValue(name, out registeredSyncObject); + + public static bool TryGetSyncObjectRegistration(Type type, [NotNullWhen(true)] out MonkeySyncObjectRegistration? registeredSyncObject) + => _registeredObjectByType.TryGetValue(type, out registeredSyncObject); + + public static bool UnregisterLinkedSyncObject(TLink linkObject, bool dispose) + { + if (!dispose) + return _syncObjectsByLinkObject.Remove(linkObject); + + if (!_syncObjectsByLinkObject.TryGetValue(linkObject, out var syncObject)) + return false; + + (syncObject as IDisposable)?.Dispose(); + + return true; + } + + public static bool UnregisterLinkedSyncObject(ILinkedMonkeySyncObject syncObject) + => _syncObjectsByLinkObject.Remove(syncObject.LinkObject); + + public static bool UnregisterSyncObject(string name) + { + if (!TryGetSyncObjectRegistration(name, out var registeredObject)) + return false; + + if (!TryGetSyncObjectRegistration(registeredObject.SyncObjectType, out var registeredObject2) || registeredObject.Name != registeredObject2.Name) + throw new InvalidOperationException($"No Sync Object type found using type [{registeredObject.SyncObjectType.CompactDescription()} based on the name [{name}], or name [{registeredObject2?.Name ?? "N/A"}] doesn't match!"); + + _registeredObjectsByName.Remove(name); + _registeredObjectByType.Remove(registeredObject.SyncObjectType); + + return true; + } + + public static bool UnregisterSyncObject(Type syncObjectType) + { + if (!TryGetSyncObjectRegistration(syncObjectType, out var registeredObject)) + return false; + + if (!TryGetSyncObjectRegistration(registeredObject.Name, out var registeredObject2) || registeredObject.SyncObjectType != registeredObject2.SyncObjectType) + throw new InvalidOperationException($"No Sync Object type found using name [{registeredObject.Name} based on the type [{syncObjectType.CompactDescription()}], or type [{registeredObject2?.SyncObjectType.CompactDescription() ?? "N/A"}] doesn't match!"); + + _registeredObjectByType.Remove(syncObjectType); + _registeredObjectsByName.Remove(registeredObject.Name); + + return true; + } + } + } +} \ No newline at end of file diff --git a/MonkeyLoader/Sync/MonkeySyncValue.cs b/MonkeyLoader/Sync/MonkeySyncValue.cs new file mode 100644 index 0000000..e1d0b91 --- /dev/null +++ b/MonkeyLoader/Sync/MonkeySyncValue.cs @@ -0,0 +1,338 @@ +using EnumerableToolkit; +using MonkeyLoader.Meta; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace MonkeyLoader.Sync +{ + /// + /// Defines the non-generic interface for s. + /// + /// + public interface ILinkedMonkeySyncValue : INotifyValueChanged, IDisposable + where TSyncObject : ILinkedMonkeySyncObject + { + /// + /// Gets the LinkObject + /// of the SyncObject that this value belongs to. + /// + public TLink LinkObject { get; } + + /// + /// Gets the property name of this sync value. + /// + public string Name { get; } + + /// + /// Gets the sync object that this value belongs to. + /// + public TSyncObject SyncObject { get; } + + /// + /// Gets or sets the internal value of this sync value. + /// + public object? Value { get; set; } + + /// + /// Gets the concrete of + /// the wrapped Value. + /// + public Type ValueType { get; } + + /// + /// Tries to restore the link of this sync value. + /// + /// true if the link was successfully restored; otherwise, false. + public bool TryRestoreLink(); + } + + /// + /// Defines the generic interface for linked s. + /// + /// The type of the link object used by the sync object that this sync value links to. + /// The type of the sync object that may contain this sync value. + /// The type of the Value. + public interface ILinkedMonkeySyncValue : INotifyValueChanged, + IReadOnlyMonkeySyncValue, IWriteOnlyMonkeySyncValue + where TSyncObject : ILinkedMonkeySyncObject + { + /// + public new T Value { get; set; } + } + + /// + /// Defines the interface for readonly s. + /// + /// + /// This interface exists purely to facilitate keeping a covariant list of sync values. + /// + /// + public interface IReadOnlyMonkeySyncValue : ILinkedMonkeySyncValue + where TSyncObject : ILinkedMonkeySyncObject + { + /// + /// Gets the internal value of this sync value. + /// + public new T Value { get; } + } + + /// + /// Defines the interface for not yet linked s. + /// + /// + public interface IUnlinkedMonkeySyncValue + where TSyncObject : ILinkedMonkeySyncObject + where TLinkedSyncValue : ILinkedMonkeySyncValue + { + /// + /// Establishes this sync value's association and link through the given sync object. + /// + /// The sync object that this value belongs to. + /// The name of this sync value. + /// Whether the link is being established from the remote side. + /// The linked sync value if the established link is valid; otherwise, null. + public TLinkedSyncValue? EstablishLinkFor(TSyncObject syncObject, string name, bool fromRemote); + } + + /// + /// Defines the interface for writeonly s. + /// + /// + /// This interface exists purely to facilitate keeping a contravariant list of sync values. + /// + /// + public interface IWriteOnlyMonkeySyncValue : ILinkedMonkeySyncValue + where TSyncObject : ILinkedMonkeySyncObject + { + /// + /// Sets the internal value of this sync value. + /// + public new T Value { set; } + } + + /// + /// Implements an abstract base for s. + /// + /// The type of the link object used by the sync object that this sync value links to. + /// The type of the sync object that may contain this sync value. + /// The concrete type of the sync value. + /// The type of the Value. + public abstract class MonkeySyncValue + : IUnlinkedMonkeySyncValue, + ILinkedMonkeySyncValue + where TSyncObject : class, ILinkedMonkeySyncObject + where TSyncValue : MonkeySyncValue + { + private static readonly Type _valueType = typeof(T); + + private bool _disposedValue; + private ValueChangedEventHandler? _untypedChanged; + private T _value; + + /// + public TLink LinkObject => SyncObject.LinkObject; + + /// + public string Name { get; private set; } = null!; + + /// + public TSyncObject SyncObject { get; private set; } = null!; + + /// + public virtual T Value + { + get => _value; + + [MemberNotNull(nameof(_value))] + set + { + var oldValue = _value; + _value = value!; + + OnChanged(oldValue); + } + } + + object? ILinkedMonkeySyncValue.Value + { + get => Value; + set => Value = (T)value!; + } + + /// + public Type ValueType => _valueType; + + /// + /// Creates a new sync value instance that wraps the given . + /// + /// The value to wrap. + public MonkeySyncValue(T value) + { + Value = value; + } + + /// + /// Ensures any unmanaged resources are disposed. + /// + ~MonkeySyncValue() + { + Dispose(false); + } + + /// + /// Unwraps the Value from the given sync object. + /// + /// The sync object to unwrap. + public static implicit operator T(MonkeySyncValue syncValue) => syncValue.Value; + + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in the 'OnDisposing()' or 'OnFinalizing()' methods + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Sets this sync value's SyncObject + /// and Name to the ones provided.
+ /// Then calls the internal link method. + ///
+ /// + public TSyncValue? EstablishLinkFor(TSyncObject syncObject, string propertyName, bool fromRemote) + { + SyncObject = syncObject; + Name = propertyName; + + return (TSyncValue?)(EstablishLinkInternal(fromRemote) ? this : null); + } + + /// + public override string ToString() => Value?.ToString() ?? ""; + + /// + public abstract bool TryRestoreLink(); + + /// + /// Handles the aspects of establishing a link that are + /// particular to s as a link object.
+ /// The SyncObject and Name + /// have already been assigned by the EstablishLinkFor + /// method that calls this. + ///
+ /// + protected abstract bool EstablishLinkInternal(bool fromRemote); + + /// + /// Cleans up any managed resources as part of disposing. + /// + /// + /// By default: Sets the SyncObject to null + /// and the internal value to its default. + /// + protected virtual void OnDisposing() + { + SyncObject = null!; + _value = default!; + } + + /// + /// Cleans up any unmanaged resources as part of + /// disposing or finalization. + /// + protected virtual void OnFinalizing() + { } + + private void Dispose(bool disposing) + { + if (_disposedValue) + return; + + if (disposing) + OnDisposing(); + + OnFinalizing(); + + _disposedValue = true; + } + + /// + /// Handles the value of this config item potentially having changed. + /// If the and new value are different:
+ /// Triggers this sync object's typed and untyped Changed events. + ///
+ /// The old value. + /// The name of the changed property on the value. + /// The collection change arguments for the value. + private void OnChanged(T? oldValue, string? changedProperty = null, NotifyCollectionChangedEventArgs? changedCollection = null) + { + var sameReferences = ReferenceEquals(oldValue, _value); + + if (!sameReferences) + { + // Remove NotifyChanged integration from old value + if (oldValue is INotifyPropertyChanged oldPropertyChanged) + oldPropertyChanged.PropertyChanged -= ValuePropertyChanged; + + if (oldValue is INotifyCollectionChanged oldCollectionChanged) + oldCollectionChanged.CollectionChanged -= ValueCollectionChanged; + + // Add NotifyChanged integration to new value + if (_value is INotifyPropertyChanged newPropertyChanged) + newPropertyChanged.PropertyChanged += ValuePropertyChanged; + + if (_value is INotifyCollectionChanged newCollectionChanged) + newCollectionChanged.CollectionChanged += ValueCollectionChanged; + } + + // Don't fire event if it wasn't triggered by event and the value didn't change + if ((sameReferences && changedProperty is null && changedCollection is null) + || (oldValue is not null && _value is not null && _value.Equals(oldValue))) + return; + + var eventArgs = new ValueChangedEventArgs(oldValue, _value, changedProperty, changedCollection); + + var exceptions = new List(); + + try + { + Changed?.TryInvokeAll(this, eventArgs); + } + catch (AggregateException ex) + { + exceptions.Add(ex); + } + + try + { + _untypedChanged?.TryInvokeAll(this, eventArgs); + } + catch (AggregateException ex) + { + exceptions.Add(ex); + } + + if (exceptions.Count > 0) + throw new AggregateException($"Some {nameof(Changed)} event subscriber(s) of this MonkeySyncValue threw an exception.", exceptions); + } + + private void ValueCollectionChanged(object sender, NotifyCollectionChangedEventArgs eventArgs) + => OnChanged(_value, null, eventArgs); + + private void ValuePropertyChanged(object sender, PropertyChangedEventArgs eventArgs) + => OnChanged(_value, eventArgs.PropertyName); + + /// + public event ValueChangedEventHandler? Changed; + + event ValueChangedEventHandler? INotifyValueChanged.Changed + { + add => _untypedChanged += value; + remove => _untypedChanged -= value; + } + } +} \ No newline at end of file