From 58192240ffbaafd943234d1bc3af1b32ec5ffe91 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Oct 2022 18:51:56 +0200 Subject: [PATCH] Add DataShare. --- Dalamud/Plugin/DalamudPluginInterface.cs | 17 +++ .../Ipc/Exceptions/DataCacheCreationError.cs | 21 ++++ .../Exceptions/DataCacheTypeMismatchError.cs | 21 ++++ .../Ipc/Exceptions/DataCacheValueNullError.cs | 19 +++ .../Ipc/Exceptions/IpcValueNullError.cs | 2 +- Dalamud/Plugin/Ipc/Internal/DataCache.cs | 20 ++++ Dalamud/Plugin/Ipc/Internal/DataShare.cs | 110 ++++++++++++++++++ 7 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 Dalamud/Plugin/Ipc/Exceptions/DataCacheCreationError.cs create mode 100644 Dalamud/Plugin/Ipc/Exceptions/DataCacheTypeMismatchError.cs create mode 100644 Dalamud/Plugin/Ipc/Exceptions/DataCacheValueNullError.cs create mode 100644 Dalamud/Plugin/Ipc/Internal/DataCache.cs create mode 100644 Dalamud/Plugin/Ipc/Internal/DataShare.cs diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index e0fa641cc..95b2e3e80 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; @@ -174,6 +175,22 @@ namespace Dalamud.Plugin #region IPC + /// + public T GetOrCreateData(string tag, Func dataGenerator) where T : class + => Service.Get().GetOrCreateData(tag, dataGenerator); + + /// + public void RelinquishData(string tag) + => Service.Get().RelinquishData(tag); + + /// + public bool TryGetData(string tag, [NotNullWhen(true)] out T? data) where T : class + => Service.Get().TryGetData(tag, out data); + + /// + public T? GetData(string tag) where T : class + => Service.Get().GetData(tag); + /// /// Gets an IPC provider. /// diff --git a/Dalamud/Plugin/Ipc/Exceptions/DataCacheCreationError.cs b/Dalamud/Plugin/Ipc/Exceptions/DataCacheCreationError.cs new file mode 100644 index 000000000..0dafc88aa --- /dev/null +++ b/Dalamud/Plugin/Ipc/Exceptions/DataCacheCreationError.cs @@ -0,0 +1,21 @@ +using System; + +namespace Dalamud.Plugin.Ipc.Exceptions; + +/// +/// This exception is thrown when a null value is provided for a data cache or it does not implement the expected type. +/// +public class DataCacheCreationError : IpcError +{ + /// + /// Initializes a new instance of the class. + /// + /// Tag of the data cache. + /// The assembly name of the caller. + /// The type expected. + /// The thrown exception. + public DataCacheCreationError(string tag, string creator, Type expectedType, Exception ex) + : base($"The creation of the {expectedType} data cache {tag} initialized by {creator} was unsuccessful.", ex) + { + } +} diff --git a/Dalamud/Plugin/Ipc/Exceptions/DataCacheTypeMismatchError.cs b/Dalamud/Plugin/Ipc/Exceptions/DataCacheTypeMismatchError.cs new file mode 100644 index 000000000..4db731687 --- /dev/null +++ b/Dalamud/Plugin/Ipc/Exceptions/DataCacheTypeMismatchError.cs @@ -0,0 +1,21 @@ +using System; + +namespace Dalamud.Plugin.Ipc.Exceptions; + +/// +/// This exception is thrown when a data cache is accessed with the wrong type. +/// +public class DataCacheTypeMismatchError : IpcError +{ + /// + /// Initializes a new instance of the class. + /// + /// Tag of the data cache. + /// Assembly name of the plugin creating the cache. + /// The requested type. + /// The stored type. + public DataCacheTypeMismatchError(string tag, string creator, Type requestedType, Type actualType) + : base($"Data cache {tag} was requested with type {requestedType}, but {creator} created type {actualType}.") + { + } +} diff --git a/Dalamud/Plugin/Ipc/Exceptions/DataCacheValueNullError.cs b/Dalamud/Plugin/Ipc/Exceptions/DataCacheValueNullError.cs new file mode 100644 index 000000000..daa8bf509 --- /dev/null +++ b/Dalamud/Plugin/Ipc/Exceptions/DataCacheValueNullError.cs @@ -0,0 +1,19 @@ +using System; + +namespace Dalamud.Plugin.Ipc.Exceptions; + +/// +/// This exception is thrown when a null value is provided for a data cache or it does not implement the expected type. +/// +public class DataCacheValueNullError : IpcError +{ + /// + /// Initializes a new instance of the class. + /// + /// Tag of the data cache. + /// The type expected. + public DataCacheValueNullError(string tag, Type expectedType) + : base($"The data cache {tag} expects a type of {expectedType} but does not implement it.") + { + } +} diff --git a/Dalamud/Plugin/Ipc/Exceptions/IpcValueNullError.cs b/Dalamud/Plugin/Ipc/Exceptions/IpcValueNullError.cs index 04d5550a9..f5a764387 100644 --- a/Dalamud/Plugin/Ipc/Exceptions/IpcValueNullError.cs +++ b/Dalamud/Plugin/Ipc/Exceptions/IpcValueNullError.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Dalamud.Plugin.Ipc.Exceptions; diff --git a/Dalamud/Plugin/Ipc/Internal/DataCache.cs b/Dalamud/Plugin/Ipc/Internal/DataCache.cs new file mode 100644 index 000000000..d404cbba2 --- /dev/null +++ b/Dalamud/Plugin/Ipc/Internal/DataCache.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; + +namespace Dalamud.Plugin.Ipc.Internal; + +internal class DataCache +{ + internal readonly string CreatorAssemblyName; + internal readonly List UserAssemblyNames; + internal readonly Type Type; + internal readonly object? Data; + + internal DataCache(string creatorAssemblyName, object? data, Type type) + { + this.CreatorAssemblyName = creatorAssemblyName; + this.UserAssemblyNames = new List{ creatorAssemblyName }; + this.Data = data; + this.Type = type; + } +} diff --git a/Dalamud/Plugin/Ipc/Internal/DataShare.cs b/Dalamud/Plugin/Ipc/Internal/DataShare.cs new file mode 100644 index 000000000..f22d9848c --- /dev/null +++ b/Dalamud/Plugin/Ipc/Internal/DataShare.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Dalamud.Plugin.Ipc.Exceptions; + +namespace Dalamud.Plugin.Ipc.Internal; + +[ServiceManager.EarlyLoadedService] +internal class DataShare : IServiceType +{ + private readonly Dictionary caches = new(); + + [ServiceManager.ServiceConstructor] + private DataShare() + { + } + + /// + /// If a data cache for exists, return the data. + /// Otherwise, call the function to create data and store it as a new cache. + /// In either case, the calling assembly will be added to the current consumers on success. + /// + /// The type of the stored data - needs to be a reference type. + /// The name for the data cache. + /// The function that generates the data if it does not already exist. + /// Either the existing data for or the data generated by . + /// Thrown if a cache for exists, but contains data of a type not assignable to . + /// Thrown if the stored data for a cache is null. + /// Thrown if throws an exception or returns null. + public T GetOrCreateData(string tag, Func dataGenerator) where T : class + { + var callerName = Assembly.GetCallingAssembly().GetName().Name ?? string.Empty; + if (this.caches.TryGetValue(tag, out var cache)) + { + if (!cache.Type.IsAssignableTo(typeof(T))) + throw new DataCacheTypeMismatchError(tag, cache.CreatorAssemblyName, typeof(T), cache.Type); + cache.UserAssemblyNames.Add(callerName); + return cache.Data as T ?? throw new DataCacheValueNullError(tag, cache.Type); + } + + try + { + var obj = dataGenerator.Invoke(); + if (obj == null) + throw new Exception("Returned data was null."); + + cache = new DataCache(callerName, obj, typeof(T)); + this.caches[tag] = cache; + return obj; + } + catch (Exception e) + { + throw new DataCacheCreationError(tag, callerName, typeof(T), e); + } + } + + /// + /// Notifies the DataShare that the calling assembly no longer uses the data stored for (or uses it one time fewer). + /// If no assembly uses the data anymore, the cache will be removed from the data share and if it is an IDisposable, Dispose will be called on it. + /// + /// The name for the data cache. + public void RelinquishData(string tag) + { + if (!this.caches.TryGetValue(tag, out var cache)) + return; + + var callerName = Assembly.GetCallingAssembly().GetName().Name ?? string.Empty; + if (!cache.UserAssemblyNames.Remove(callerName) || cache.UserAssemblyNames.Count > 0) + return; + + this.caches.Remove(tag); + if (cache.Data is IDisposable disposable) + disposable.Dispose(); + } + + /// + /// Obtain the data for the given , if it exists and has the correct type. + /// Add the calling assembly to the current consumers if true is returned. + /// + /// The type for the requested data - needs to be a reference type. + /// The name for the data cache. + /// The requested data on success, null otherwise. + /// True if the requested data exists and is assignable to the requested type. + public bool TryGetData(string tag, [NotNullWhen(true)] out T? data) where T : class + { + data = null; + if (!this.caches.TryGetValue(tag, out var cache) || !cache.Type.IsAssignableTo(typeof(T))) + return false; + + var callerName = Assembly.GetCallingAssembly().GetName().Name ?? string.Empty; + data = cache.Data as T; + if (data == null) + return false; + + cache.UserAssemblyNames.Add(callerName); + return true; + + } + + /// + /// Obtain the data for the given , if it exists and has the correct type. + /// Add the calling assembly to the current consumers if non-null is returned. + /// + /// The type for the requested data - needs to be a reference type. + /// The name for the data cache. + /// The requested data on success or null. + public T? GetData(string tag) where T : class + => TryGetData(tag, out var data) ? data : null; +}