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