mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-02-21 07:17:45 +01:00
Merge pull request #1552 from goatcorp/net8-rollup
Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> Co-authored-by: github-actions[bot] <noreply@github.com> Co-authored-by: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Co-authored-by: Soreepeong <soreepeong@gmail.com> Co-authored-by: Sirius902 <10891979+Sirius902@users.noreply.github.com> Co-authored-by: Ottermandias <70807659+Ottermandias@users.noreply.github.com> Co-authored-by: Haselnussbomber <mail@haselnussbomber.de> Co-authored-by: grittyfrog <148605153+grittyfrog@users.noreply.github.com> Fix Dalamud trying to unload IServiceType and crashing (#1557) Fix Dalamud trying to unload IServiceType and crashing (#1557)" (#1559) Fix ChatGui race condition (#1563) Fix multi-line copy/paste between ImGui and XIV (#1525) fix for new message sounds and interactable links (#1568) Fix AddonLifecycle ABI; deprecate arg class public ctors (#1570) fix for AddonArgs infinite loop (#1571) fix thread safety (#1576) Fix DataShare race condition, and add debug features (#1573)
This commit is contained in:
commit
68ffaf2440
69 changed files with 3619 additions and 915 deletions
|
|
@ -104,13 +104,14 @@ resharper_can_use_global_alias = false
|
||||||
resharper_csharp_align_multiline_parameter = true
|
resharper_csharp_align_multiline_parameter = true
|
||||||
resharper_csharp_align_multiple_declaration = true
|
resharper_csharp_align_multiple_declaration = true
|
||||||
resharper_csharp_empty_block_style = multiline
|
resharper_csharp_empty_block_style = multiline
|
||||||
resharper_csharp_int_align_comments = true
|
resharper_csharp_int_align_comments = false
|
||||||
resharper_csharp_new_line_before_while = true
|
resharper_csharp_new_line_before_while = true
|
||||||
resharper_csharp_wrap_after_declaration_lpar = true
|
resharper_csharp_wrap_after_declaration_lpar = true
|
||||||
resharper_csharp_wrap_after_invocation_lpar = true
|
resharper_csharp_wrap_after_invocation_lpar = true
|
||||||
resharper_csharp_wrap_arguments_style = chop_if_long
|
resharper_csharp_wrap_arguments_style = chop_if_long
|
||||||
resharper_enforce_line_ending_style = true
|
resharper_enforce_line_ending_style = true
|
||||||
resharper_instance_members_qualify_declared_in = this_class, base_class
|
resharper_instance_members_qualify_declared_in = this_class, base_class
|
||||||
|
resharper_int_align = false
|
||||||
resharper_member_can_be_private_global_highlighting = none
|
resharper_member_can_be_private_global_highlighting = none
|
||||||
resharper_member_can_be_private_local_highlighting = none
|
resharper_member_can_be_private_local_highlighting = none
|
||||||
resharper_new_line_before_finally = true
|
resharper_new_line_before_finally = true
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,7 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
config.CrashHandlerShow = json.value("CrashHandlerShow", config.CrashHandlerShow);
|
config.CrashHandlerShow = json.value("CrashHandlerShow", config.CrashHandlerShow);
|
||||||
|
config.NoExceptionHandlers = json.value("NoExceptionHandlers", config.NoExceptionHandlers);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DalamudStartInfo::from_envvars() {
|
void DalamudStartInfo::from_envvars() {
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ struct DalamudStartInfo {
|
||||||
std::set<std::string> BootUnhookDlls{};
|
std::set<std::string> BootUnhookDlls{};
|
||||||
|
|
||||||
bool CrashHandlerShow = false;
|
bool CrashHandlerShow = false;
|
||||||
|
bool NoExceptionHandlers = false;
|
||||||
|
|
||||||
friend void from_json(const nlohmann::json&, DalamudStartInfo&);
|
friend void from_json(const nlohmann::json&, DalamudStartInfo&);
|
||||||
void from_envvars();
|
void from_envvars();
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,9 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
|
||||||
// ============================== VEH ======================================== //
|
// ============================== VEH ======================================== //
|
||||||
|
|
||||||
logging::I("Initializing VEH...");
|
logging::I("Initializing VEH...");
|
||||||
if (utils::is_running_on_wine()) {
|
if (g_startInfo.NoExceptionHandlers) {
|
||||||
|
logging::W("=> Exception handlers are disabled from DalamudStartInfo.");
|
||||||
|
} else if (utils::is_running_on_wine()) {
|
||||||
logging::I("=> VEH was disabled, running on wine");
|
logging::I("=> VEH was disabled, running on wine");
|
||||||
} else if (g_startInfo.BootVehEnabled) {
|
} else if (g_startInfo.BootVehEnabled) {
|
||||||
if (veh::add_handler(g_startInfo.BootVehFull, g_startInfo.WorkingDirectory))
|
if (veh::add_handler(g_startInfo.BootVehFull, g_startInfo.WorkingDirectory))
|
||||||
|
|
|
||||||
|
|
@ -17,38 +17,6 @@ public record DalamudStartInfo
|
||||||
// ignored
|
// ignored
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="DalamudStartInfo"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other">Object to copy values from.</param>
|
|
||||||
public DalamudStartInfo(DalamudStartInfo other)
|
|
||||||
{
|
|
||||||
this.WorkingDirectory = other.WorkingDirectory;
|
|
||||||
this.ConfigurationPath = other.ConfigurationPath;
|
|
||||||
this.LogPath = other.LogPath;
|
|
||||||
this.LogName = other.LogName;
|
|
||||||
this.PluginDirectory = other.PluginDirectory;
|
|
||||||
this.AssetDirectory = other.AssetDirectory;
|
|
||||||
this.Language = other.Language;
|
|
||||||
this.GameVersion = other.GameVersion;
|
|
||||||
this.DelayInitializeMs = other.DelayInitializeMs;
|
|
||||||
this.TroubleshootingPackData = other.TroubleshootingPackData;
|
|
||||||
this.NoLoadPlugins = other.NoLoadPlugins;
|
|
||||||
this.NoLoadThirdPartyPlugins = other.NoLoadThirdPartyPlugins;
|
|
||||||
this.BootLogPath = other.BootLogPath;
|
|
||||||
this.BootShowConsole = other.BootShowConsole;
|
|
||||||
this.BootDisableFallbackConsole = other.BootDisableFallbackConsole;
|
|
||||||
this.BootWaitMessageBox = other.BootWaitMessageBox;
|
|
||||||
this.BootWaitDebugger = other.BootWaitDebugger;
|
|
||||||
this.BootVehEnabled = other.BootVehEnabled;
|
|
||||||
this.BootVehFull = other.BootVehFull;
|
|
||||||
this.BootEnableEtw = other.BootEnableEtw;
|
|
||||||
this.BootDotnetOpenProcessHookMode = other.BootDotnetOpenProcessHookMode;
|
|
||||||
this.BootEnabledGameFixes = other.BootEnabledGameFixes;
|
|
||||||
this.BootUnhookDlls = other.BootUnhookDlls;
|
|
||||||
this.CrashHandlerShow = other.CrashHandlerShow;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the working directory of the XIVLauncher installations.
|
/// Gets or sets the working directory of the XIVLauncher installations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -169,4 +137,9 @@ public record DalamudStartInfo
|
||||||
/// Gets or sets a value indicating whether to show crash handler console window.
|
/// Gets or sets a value indicating whether to show crash handler console window.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool CrashHandlerShow { get; set; }
|
public bool CrashHandlerShow { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to disable all kinds of global exception handlers.
|
||||||
|
/// </summary>
|
||||||
|
public bool NoExceptionHandlers { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ namespace Dalamud.Injector
|
||||||
args.Remove("--no-plugin");
|
args.Remove("--no-plugin");
|
||||||
args.Remove("--no-3rd-plugin");
|
args.Remove("--no-3rd-plugin");
|
||||||
args.Remove("--crash-handler-console");
|
args.Remove("--crash-handler-console");
|
||||||
|
args.Remove("--no-exception-handlers");
|
||||||
|
|
||||||
var mainCommand = args[1].ToLowerInvariant();
|
var mainCommand = args[1].ToLowerInvariant();
|
||||||
if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "inject"[..mainCommand.Length] == mainCommand)
|
if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "inject"[..mainCommand.Length] == mainCommand)
|
||||||
|
|
@ -393,6 +394,7 @@ namespace Dalamud.Injector
|
||||||
startInfo.NoLoadThirdPartyPlugins = args.Contains("--no-3rd-plugin");
|
startInfo.NoLoadThirdPartyPlugins = args.Contains("--no-3rd-plugin");
|
||||||
// startInfo.BootUnhookDlls = new List<string>() { "kernel32.dll", "ntdll.dll", "user32.dll" };
|
// startInfo.BootUnhookDlls = new List<string>() { "kernel32.dll", "ntdll.dll", "user32.dll" };
|
||||||
startInfo.CrashHandlerShow = args.Contains("--crash-handler-console");
|
startInfo.CrashHandlerShow = args.Contains("--crash-handler-console");
|
||||||
|
startInfo.NoExceptionHandlers = args.Contains("--no-exception-handlers");
|
||||||
|
|
||||||
return startInfo;
|
return startInfo;
|
||||||
}
|
}
|
||||||
|
|
@ -434,7 +436,7 @@ namespace Dalamud.Injector
|
||||||
Console.WriteLine("Verbose logging:\t[-v]");
|
Console.WriteLine("Verbose logging:\t[-v]");
|
||||||
Console.WriteLine("Show Console:\t[--console] [--crash-handler-console]");
|
Console.WriteLine("Show Console:\t[--console] [--crash-handler-console]");
|
||||||
Console.WriteLine("Enable ETW:\t[--etw]");
|
Console.WriteLine("Enable ETW:\t[--etw]");
|
||||||
Console.WriteLine("Enable VEH:\t[--veh], [--veh-full]");
|
Console.WriteLine("Enable VEH:\t[--veh], [--veh-full], [--no-exception-handlers]");
|
||||||
Console.WriteLine("Show messagebox:\t[--msgbox1], [--msgbox2], [--msgbox3]");
|
Console.WriteLine("Show messagebox:\t[--msgbox1], [--msgbox2], [--msgbox3]");
|
||||||
Console.WriteLine("No plugins:\t[--no-plugin] [--no-3rd-plugin]");
|
Console.WriteLine("No plugins:\t[--no-plugin] [--no-3rd-plugin]");
|
||||||
Console.WriteLine("Logging:\t[--logname=<logfile suffix>] [--logpath=<log base directory>]");
|
Console.WriteLine("Logging:\t[--logname=<logfile suffix>] [--logpath=<log base directory>]");
|
||||||
|
|
@ -889,7 +891,7 @@ namespace Dalamud.Injector
|
||||||
var gameVerStr = File.ReadAllText(Path.Combine(ffxivDir, "ffxivgame.ver"));
|
var gameVerStr = File.ReadAllText(Path.Combine(ffxivDir, "ffxivgame.ver"));
|
||||||
var gameVer = GameVersion.Parse(gameVerStr);
|
var gameVer = GameVersion.Parse(gameVerStr);
|
||||||
|
|
||||||
return new DalamudStartInfo(startInfo)
|
return startInfo with
|
||||||
{
|
{
|
||||||
GameVersion = gameVer,
|
GameVersion = gameVer,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Label="Feature">
|
<PropertyGroup Label="Feature">
|
||||||
<DalamudVersion>9.0.0.13</DalamudVersion>
|
<DalamudVersion>9.0.0.14</DalamudVersion>
|
||||||
<Description>XIV Launcher addon framework</Description>
|
<Description>XIV Launcher addon framework</Description>
|
||||||
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
|
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
|
||||||
<Version>$(DalamudVersion)</Version>
|
<Version>$(DalamudVersion)</Version>
|
||||||
|
|
@ -89,6 +89,7 @@
|
||||||
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
||||||
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="7.0.0" />
|
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="7.0.0" />
|
||||||
<PackageReference Include="System.Resources.Extensions" Version="7.0.0" />
|
<PackageReference Include="System.Resources.Extensions" Version="7.0.0" />
|
||||||
|
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.22621.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Dalamud.Common\Dalamud.Common.csproj" />
|
<ProjectReference Include="..\Dalamud.Common\Dalamud.Common.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,8 @@ public sealed class EntryPoint
|
||||||
LogLevelSwitch.MinimumLevel = configuration.LogLevel;
|
LogLevelSwitch.MinimumLevel = configuration.LogLevel;
|
||||||
|
|
||||||
// Log any unhandled exception.
|
// Log any unhandled exception.
|
||||||
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
|
if (!info.NoExceptionHandlers)
|
||||||
|
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
|
||||||
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
|
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
|
||||||
|
|
||||||
var unloadFailed = false;
|
var unloadFailed = false;
|
||||||
|
|
@ -196,7 +197,8 @@ public sealed class EntryPoint
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException;
|
TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException;
|
||||||
AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException;
|
if (!info.NoExceptionHandlers)
|
||||||
|
AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException;
|
||||||
|
|
||||||
Log.Information("Session has ended.");
|
Log.Information("Session has ended.");
|
||||||
Log.CloseAndFlush();
|
Log.CloseAndFlush();
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ using Dalamud.IoC.Internal;
|
||||||
using Dalamud.Logging.Internal;
|
using Dalamud.Logging.Internal;
|
||||||
using Dalamud.Plugin.Internal.Types;
|
using Dalamud.Plugin.Internal.Types;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
|
using Dalamud.Utility;
|
||||||
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
|
|
||||||
|
|
@ -31,6 +33,9 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
|
||||||
[ServiceManager.ServiceDependency]
|
[ServiceManager.ServiceDependency]
|
||||||
private readonly AddonLifecycle addonLifecycle = Service<AddonLifecycle>.Get();
|
private readonly AddonLifecycle addonLifecycle = Service<AddonLifecycle>.Get();
|
||||||
|
|
||||||
|
[ServiceManager.ServiceDependency]
|
||||||
|
private readonly Framework framework = Service<Framework>.Get();
|
||||||
|
|
||||||
private readonly AddonLifecycleEventListener finalizeEventListener;
|
private readonly AddonLifecycleEventListener finalizeEventListener;
|
||||||
|
|
||||||
private readonly AddonEventManagerAddressResolver address;
|
private readonly AddonEventManagerAddressResolver address;
|
||||||
|
|
@ -57,6 +62,8 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
|
||||||
|
|
||||||
this.finalizeEventListener = new AddonLifecycleEventListener(AddonEvent.PreFinalize, string.Empty, this.OnAddonFinalize);
|
this.finalizeEventListener = new AddonLifecycleEventListener(AddonEvent.PreFinalize, string.Empty, this.OnAddonFinalize);
|
||||||
this.addonLifecycle.RegisterListener(this.finalizeEventListener);
|
this.addonLifecycle.RegisterListener(this.finalizeEventListener);
|
||||||
|
|
||||||
|
this.onUpdateCursor.Enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
private delegate nint UpdateCursorDelegate(RaptureAtkModule* module);
|
private delegate nint UpdateCursorDelegate(RaptureAtkModule* module);
|
||||||
|
|
@ -85,6 +92,8 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
|
||||||
/// <returns>IAddonEventHandle used to remove the event.</returns>
|
/// <returns>IAddonEventHandle used to remove the event.</returns>
|
||||||
internal IAddonEventHandle? AddEvent(string pluginId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler)
|
internal IAddonEventHandle? AddEvent(string pluginId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler)
|
||||||
{
|
{
|
||||||
|
if (!ThreadSafety.IsMainThread) throw new InvalidOperationException("This should be done only from the main thread. Modifying active native code on non-main thread is not supported.");
|
||||||
|
|
||||||
if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } eventController)
|
if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } eventController)
|
||||||
{
|
{
|
||||||
return eventController.AddEvent(atkUnitBase, atkResNode, eventType, eventHandler);
|
return eventController.AddEvent(atkUnitBase, atkResNode, eventType, eventHandler);
|
||||||
|
|
@ -101,6 +110,8 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
|
||||||
/// <param name="eventHandle">The Unique Id for this event.</param>
|
/// <param name="eventHandle">The Unique Id for this event.</param>
|
||||||
internal void RemoveEvent(string pluginId, IAddonEventHandle eventHandle)
|
internal void RemoveEvent(string pluginId, IAddonEventHandle eventHandle)
|
||||||
{
|
{
|
||||||
|
if (!ThreadSafety.IsMainThread) throw new InvalidOperationException("This should be done only from the main thread. Modifying active native code on non-main thread is not supported.");
|
||||||
|
|
||||||
if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } eventController)
|
if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } eventController)
|
||||||
{
|
{
|
||||||
eventController.RemoveEvent(eventHandle);
|
eventController.RemoveEvent(eventHandle);
|
||||||
|
|
@ -128,11 +139,14 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
|
||||||
/// <param name="pluginId">Unique ID for this plugin.</param>
|
/// <param name="pluginId">Unique ID for this plugin.</param>
|
||||||
internal void AddPluginEventController(string pluginId)
|
internal void AddPluginEventController(string pluginId)
|
||||||
{
|
{
|
||||||
if (this.pluginEventControllers.All(entry => entry.PluginId != pluginId))
|
this.framework.RunOnFrameworkThread(() =>
|
||||||
{
|
{
|
||||||
Log.Verbose($"Creating new PluginEventController for: {pluginId}");
|
if (this.pluginEventControllers.All(entry => entry.PluginId != pluginId))
|
||||||
this.pluginEventControllers.Add(new PluginEventController(pluginId));
|
{
|
||||||
}
|
Log.Verbose($"Creating new PluginEventController for: {pluginId}");
|
||||||
|
this.pluginEventControllers.Add(new PluginEventController(pluginId));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -141,18 +155,15 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
|
||||||
/// <param name="pluginId">Unique ID for this plugin.</param>
|
/// <param name="pluginId">Unique ID for this plugin.</param>
|
||||||
internal void RemovePluginEventController(string pluginId)
|
internal void RemovePluginEventController(string pluginId)
|
||||||
{
|
{
|
||||||
if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } controller)
|
this.framework.RunOnFrameworkThread(() =>
|
||||||
{
|
{
|
||||||
Log.Verbose($"Removing PluginEventController for: {pluginId}");
|
if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } controller)
|
||||||
this.pluginEventControllers.Remove(controller);
|
{
|
||||||
controller.Dispose();
|
Log.Verbose($"Removing PluginEventController for: {pluginId}");
|
||||||
}
|
this.pluginEventControllers.Remove(controller);
|
||||||
}
|
controller.Dispose();
|
||||||
|
}
|
||||||
[ServiceManager.CallWhenServicesReady]
|
});
|
||||||
private void ContinueConstruction()
|
|
||||||
{
|
|
||||||
this.onUpdateCursor.Enable();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using Dalamud.Memory;
|
using Dalamud.Memory;
|
||||||
|
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
|
|
||||||
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||||
|
|
@ -12,24 +13,62 @@ public abstract unsafe class AddonArgs
|
||||||
/// Constant string representing the name of an addon that is invalid.
|
/// Constant string representing the name of an addon that is invalid.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string InvalidAddon = "NullAddon";
|
public const string InvalidAddon = "NullAddon";
|
||||||
|
|
||||||
private string? addonName;
|
private string? addonName;
|
||||||
|
private IntPtr addon;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the name of the addon this args referrers to.
|
/// Gets the name of the addon this args referrers to.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string AddonName => this.GetAddonName();
|
public string AddonName => this.GetAddonName();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the pointer to the addons AtkUnitBase.
|
/// Gets the pointer to the addons AtkUnitBase.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public nint Addon { get; init; }
|
public nint Addon
|
||||||
|
{
|
||||||
|
get => this.AddonInternal;
|
||||||
|
init => this.AddonInternal = value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the type of these args.
|
/// Gets the type of these args.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract AddonArgsType Type { get; }
|
public abstract AddonArgsType Type { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the pointer to the addons AtkUnitBase.
|
||||||
|
/// </summary>
|
||||||
|
internal nint AddonInternal
|
||||||
|
{
|
||||||
|
get => this.addon;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (this.addon == value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.addon = value;
|
||||||
|
this.addonName = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if addon name matches the given span of char.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The name to check.</param>
|
||||||
|
/// <returns>Whether it is the case.</returns>
|
||||||
|
internal bool IsAddon(ReadOnlySpan<char> name)
|
||||||
|
{
|
||||||
|
if (this.Addon == nint.Zero) return false;
|
||||||
|
if (name.Length is 0 or > 0x20)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var addonPointer = (AtkUnitBase*)this.Addon;
|
||||||
|
if (addonPointer->Name is null) return false;
|
||||||
|
|
||||||
|
return MemoryHelper.EqualsZeroTerminatedString(name, (nint)addonPointer->Name, null, 0x20);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Helper method for ensuring the name of the addon is valid.
|
/// Helper method for ensuring the name of the addon is valid.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,22 @@
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Addon argument data for Draw events.
|
/// Addon argument data for Draw events.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AddonDrawArgs : AddonArgs
|
public class AddonDrawArgs : AddonArgs, ICloneable
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AddonDrawArgs"/> class.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Not intended for public construction.", false)]
|
||||||
|
public AddonDrawArgs()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override AddonArgsType Type => AddonArgsType.Draw;
|
public override AddonArgsType Type => AddonArgsType.Draw;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||||
|
public AddonDrawArgs Clone() => (AddonDrawArgs)this.MemberwiseClone();
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Clone"/>
|
||||||
|
object ICloneable.Clone() => this.Clone();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,22 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Addon argument data for ReceiveEvent events.
|
/// Addon argument data for ReceiveEvent events.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AddonFinalizeArgs : AddonArgs
|
public class AddonFinalizeArgs : AddonArgs, ICloneable
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AddonFinalizeArgs"/> class.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Not intended for public construction.", false)]
|
||||||
|
public AddonFinalizeArgs()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override AddonArgsType Type => AddonArgsType.Finalize;
|
public override AddonArgsType Type => AddonArgsType.Finalize;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||||
|
public AddonFinalizeArgs Clone() => (AddonFinalizeArgs)this.MemberwiseClone();
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Clone"/>
|
||||||
|
object ICloneable.Clone() => this.Clone();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,28 +3,42 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Addon argument data for ReceiveEvent events.
|
/// Addon argument data for ReceiveEvent events.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AddonReceiveEventArgs : AddonArgs
|
public class AddonReceiveEventArgs : AddonArgs, ICloneable
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AddonReceiveEventArgs"/> class.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Not intended for public construction.", false)]
|
||||||
|
public AddonReceiveEventArgs()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override AddonArgsType Type => AddonArgsType.ReceiveEvent;
|
public override AddonArgsType Type => AddonArgsType.ReceiveEvent;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the AtkEventType for this event message.
|
/// Gets or sets the AtkEventType for this event message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte AtkEventType { get; init; }
|
public byte AtkEventType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the event id for this event message.
|
/// Gets or sets the event id for this event message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int EventParam { get; init; }
|
public int EventParam { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the pointer to an AtkEvent for this event message.
|
/// Gets or sets the pointer to an AtkEvent for this event message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public nint AtkEvent { get; init; }
|
public nint AtkEvent { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the pointer to a block of data for this event message.
|
/// Gets or sets the pointer to a block of data for this event message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public nint Data { get; init; }
|
public nint Data { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||||
|
public AddonReceiveEventArgs Clone() => (AddonReceiveEventArgs)this.MemberwiseClone();
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Clone"/>
|
||||||
|
object ICloneable.Clone() => this.Clone();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,23 +5,37 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Addon argument data for Refresh events.
|
/// Addon argument data for Refresh events.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AddonRefreshArgs : AddonArgs
|
public class AddonRefreshArgs : AddonArgs, ICloneable
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AddonRefreshArgs"/> class.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Not intended for public construction.", false)]
|
||||||
|
public AddonRefreshArgs()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override AddonArgsType Type => AddonArgsType.Refresh;
|
public override AddonArgsType Type => AddonArgsType.Refresh;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the number of AtkValues.
|
/// Gets or sets the number of AtkValues.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public uint AtkValueCount { get; init; }
|
public uint AtkValueCount { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the address of the AtkValue array.
|
/// Gets or sets the address of the AtkValue array.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public nint AtkValues { get; init; }
|
public nint AtkValues { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the AtkValues in the form of a span.
|
/// Gets the AtkValues in the form of a span.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
|
public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||||
|
public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone();
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Clone"/>
|
||||||
|
object ICloneable.Clone() => this.Clone();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,32 @@
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Addon argument data for OnRequestedUpdate events.
|
/// Addon argument data for OnRequestedUpdate events.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AddonRequestedUpdateArgs : AddonArgs
|
public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AddonRequestedUpdateArgs"/> class.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Not intended for public construction.", false)]
|
||||||
|
public AddonRequestedUpdateArgs()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override AddonArgsType Type => AddonArgsType.RequestedUpdate;
|
public override AddonArgsType Type => AddonArgsType.RequestedUpdate;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the NumberArrayData** for this event.
|
/// Gets or sets the NumberArrayData** for this event.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public nint NumberArrayData { get; init; }
|
public nint NumberArrayData { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the StringArrayData** for this event.
|
/// Gets or sets the StringArrayData** for this event.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public nint StringArrayData { get; init; }
|
public nint StringArrayData { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||||
|
public AddonRequestedUpdateArgs Clone() => (AddonRequestedUpdateArgs)this.MemberwiseClone();
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Clone"/>
|
||||||
|
object ICloneable.Clone() => this.Clone();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,23 +5,37 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Addon argument data for Setup events.
|
/// Addon argument data for Setup events.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AddonSetupArgs : AddonArgs
|
public class AddonSetupArgs : AddonArgs, ICloneable
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AddonSetupArgs"/> class.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Not intended for public construction.", false)]
|
||||||
|
public AddonSetupArgs()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override AddonArgsType Type => AddonArgsType.Setup;
|
public override AddonArgsType Type => AddonArgsType.Setup;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the number of AtkValues.
|
/// Gets or sets the number of AtkValues.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public uint AtkValueCount { get; init; }
|
public uint AtkValueCount { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the address of the AtkValue array.
|
/// Gets or sets the address of the AtkValue array.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public nint AtkValues { get; init; }
|
public nint AtkValues { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the AtkValues in the form of a span.
|
/// Gets the AtkValues in the form of a span.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
|
public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||||
|
public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone();
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Clone"/>
|
||||||
|
object ICloneable.Clone() => this.Clone();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,36 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Addon argument data for Update events.
|
/// Addon argument data for Update events.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AddonUpdateArgs : AddonArgs
|
public class AddonUpdateArgs : AddonArgs, ICloneable
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AddonUpdateArgs"/> class.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Not intended for public construction.", false)]
|
||||||
|
public AddonUpdateArgs()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override AddonArgsType Type => AddonArgsType.Update;
|
public override AddonArgsType Type => AddonArgsType.Update;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the time since the last update.
|
/// Gets the time since the last update.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float TimeDelta { get; init; }
|
public float TimeDelta
|
||||||
|
{
|
||||||
|
get => this.TimeDeltaInternal;
|
||||||
|
init => this.TimeDeltaInternal = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the time since the last update.
|
||||||
|
/// </summary>
|
||||||
|
internal float TimeDeltaInternal { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||||
|
public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone();
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Clone"/>
|
||||||
|
object ICloneable.Clone() => this.Clone();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||||
using Dalamud.Hooking;
|
using Dalamud.Hooking;
|
||||||
|
|
@ -37,8 +38,17 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
||||||
private readonly Hook<AddonOnRefreshDelegate> onAddonRefreshHook;
|
private readonly Hook<AddonOnRefreshDelegate> onAddonRefreshHook;
|
||||||
private readonly CallHook<AddonOnRequestedUpdateDelegate> onAddonRequestedUpdateHook;
|
private readonly CallHook<AddonOnRequestedUpdateDelegate> onAddonRequestedUpdateHook;
|
||||||
|
|
||||||
private readonly ConcurrentBag<AddonLifecycleEventListener> newEventListeners = new();
|
// Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet
|
||||||
private readonly ConcurrentBag<AddonLifecycleEventListener> removeEventListeners = new();
|
// package, and these events are always called from the main thread, this is fine.
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
|
// TODO: turn constructors of these internal
|
||||||
|
private readonly AddonSetupArgs recyclingSetupArgs = new();
|
||||||
|
private readonly AddonFinalizeArgs recyclingFinalizeArgs = new();
|
||||||
|
private readonly AddonDrawArgs recyclingDrawArgs = new();
|
||||||
|
private readonly AddonUpdateArgs recyclingUpdateArgs = new();
|
||||||
|
private readonly AddonRefreshArgs recyclingRefreshArgs = new();
|
||||||
|
private readonly AddonRequestedUpdateArgs recyclingRequestedUpdateArgs = new();
|
||||||
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
|
|
||||||
[ServiceManager.ServiceConstructor]
|
[ServiceManager.ServiceConstructor]
|
||||||
private AddonLifecycle(TargetSigScanner sigScanner)
|
private AddonLifecycle(TargetSigScanner sigScanner)
|
||||||
|
|
@ -48,8 +58,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
||||||
|
|
||||||
// We want value of the function pointer at vFunc[2]
|
// We want value of the function pointer at vFunc[2]
|
||||||
this.disallowedReceiveEventAddress = ((nint*)this.address.AtkEventListener)![2];
|
this.disallowedReceiveEventAddress = ((nint*)this.address.AtkEventListener)![2];
|
||||||
|
|
||||||
this.framework.Update += this.OnFrameworkUpdate;
|
|
||||||
|
|
||||||
this.onAddonSetupHook = new CallHook<AddonSetupDelegate>(this.address.AddonSetup, this.OnAddonSetup);
|
this.onAddonSetupHook = new CallHook<AddonSetupDelegate>(this.address.AddonSetup, this.OnAddonSetup);
|
||||||
this.onAddonSetup2Hook = new CallHook<AddonSetupDelegate>(this.address.AddonSetup2, this.OnAddonSetup);
|
this.onAddonSetup2Hook = new CallHook<AddonSetupDelegate>(this.address.AddonSetup2, this.OnAddonSetup);
|
||||||
|
|
@ -58,6 +66,14 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
||||||
this.onAddonUpdateHook = new CallHook<AddonUpdateDelegate>(this.address.AddonUpdate, this.OnAddonUpdate);
|
this.onAddonUpdateHook = new CallHook<AddonUpdateDelegate>(this.address.AddonUpdate, this.OnAddonUpdate);
|
||||||
this.onAddonRefreshHook = Hook<AddonOnRefreshDelegate>.FromAddress(this.address.AddonOnRefresh, this.OnAddonRefresh);
|
this.onAddonRefreshHook = Hook<AddonOnRefreshDelegate>.FromAddress(this.address.AddonOnRefresh, this.OnAddonRefresh);
|
||||||
this.onAddonRequestedUpdateHook = new CallHook<AddonOnRequestedUpdateDelegate>(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate);
|
this.onAddonRequestedUpdateHook = new CallHook<AddonOnRequestedUpdateDelegate>(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate);
|
||||||
|
|
||||||
|
this.onAddonSetupHook.Enable();
|
||||||
|
this.onAddonSetup2Hook.Enable();
|
||||||
|
this.onAddonFinalizeHook.Enable();
|
||||||
|
this.onAddonDrawHook.Enable();
|
||||||
|
this.onAddonUpdateHook.Enable();
|
||||||
|
this.onAddonRefreshHook.Enable();
|
||||||
|
this.onAddonRequestedUpdateHook.Enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
private delegate void AddonSetupDelegate(AtkUnitBase* addon, uint valueCount, AtkValue* values);
|
private delegate void AddonSetupDelegate(AtkUnitBase* addon, uint valueCount, AtkValue* values);
|
||||||
|
|
@ -85,8 +101,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
this.framework.Update -= this.OnFrameworkUpdate;
|
|
||||||
|
|
||||||
this.onAddonSetupHook.Dispose();
|
this.onAddonSetupHook.Dispose();
|
||||||
this.onAddonSetup2Hook.Dispose();
|
this.onAddonSetup2Hook.Dispose();
|
||||||
this.onAddonFinalizeHook.Dispose();
|
this.onAddonFinalizeHook.Dispose();
|
||||||
|
|
@ -107,7 +121,20 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
||||||
/// <param name="listener">The listener to register.</param>
|
/// <param name="listener">The listener to register.</param>
|
||||||
internal void RegisterListener(AddonLifecycleEventListener listener)
|
internal void RegisterListener(AddonLifecycleEventListener listener)
|
||||||
{
|
{
|
||||||
this.newEventListeners.Add(listener);
|
this.framework.RunOnTick(() =>
|
||||||
|
{
|
||||||
|
this.EventListeners.Add(listener);
|
||||||
|
|
||||||
|
// If we want receive event messages have an already active addon, enable the receive event hook.
|
||||||
|
// If the addon isn't active yet, we'll grab the hook when it sets up.
|
||||||
|
if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
|
||||||
|
{
|
||||||
|
if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener)
|
||||||
|
{
|
||||||
|
receiveEventListener.Hook?.Enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -116,7 +143,24 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
||||||
/// <param name="listener">The listener to unregister.</param>
|
/// <param name="listener">The listener to unregister.</param>
|
||||||
internal void UnregisterListener(AddonLifecycleEventListener listener)
|
internal void UnregisterListener(AddonLifecycleEventListener listener)
|
||||||
{
|
{
|
||||||
this.removeEventListeners.Add(listener);
|
this.framework.RunOnTick(() =>
|
||||||
|
{
|
||||||
|
this.EventListeners.Remove(listener);
|
||||||
|
|
||||||
|
// If we are disabling an ReceiveEvent listener, check if we should disable the hook.
|
||||||
|
if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
|
||||||
|
{
|
||||||
|
// Get the ReceiveEvent Listener for this addon
|
||||||
|
if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener)
|
||||||
|
{
|
||||||
|
// If there are no other listeners listening for this event, disable the hook.
|
||||||
|
if (!this.EventListeners.Any(listeners => listeners.AddonName.Contains(listener.AddonName) && listener.EventType is AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent))
|
||||||
|
{
|
||||||
|
receiveEventListener.Hook?.Disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -124,75 +168,30 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="eventType">Event Type.</param>
|
/// <param name="eventType">Event Type.</param>
|
||||||
/// <param name="args">AddonArgs.</param>
|
/// <param name="args">AddonArgs.</param>
|
||||||
internal void InvokeListeners(AddonEvent eventType, AddonArgs args)
|
/// <param name="blame">What to blame on errors.</param>
|
||||||
|
internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "")
|
||||||
{
|
{
|
||||||
// Match on string.empty for listeners that want events for all addons.
|
// Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better.
|
||||||
foreach (var listener in this.EventListeners.Where(listener => listener.EventType == eventType && (listener.AddonName == args.AddonName || listener.AddonName == string.Empty)))
|
foreach (var listener in this.EventListeners)
|
||||||
{
|
{
|
||||||
listener.FunctionDelegate.Invoke(eventType, args);
|
if (listener.EventType != eventType)
|
||||||
}
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
// Used to prevent concurrency issues if plugins try to register during iteration of listeners.
|
// Match on string.empty for listeners that want events for all addons.
|
||||||
private void OnFrameworkUpdate(IFramework unused)
|
if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName))
|
||||||
{
|
continue;
|
||||||
if (this.newEventListeners.Any())
|
|
||||||
{
|
try
|
||||||
foreach (var toAddListener in this.newEventListeners)
|
|
||||||
{
|
{
|
||||||
this.EventListeners.Add(toAddListener);
|
listener.FunctionDelegate.Invoke(eventType, args);
|
||||||
|
|
||||||
// If we want receive event messages have an already active addon, enable the receive event hook.
|
|
||||||
// If the addon isn't active yet, we'll grab the hook when it sets up.
|
|
||||||
if (toAddListener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
|
|
||||||
{
|
|
||||||
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(toAddListener.AddonName)) is { } receiveEventListener)
|
|
||||||
{
|
|
||||||
receiveEventListener.Hook?.Enable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
catch (Exception e)
|
||||||
this.newEventListeners.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.removeEventListeners.Any())
|
|
||||||
{
|
|
||||||
foreach (var toRemoveListener in this.removeEventListeners)
|
|
||||||
{
|
{
|
||||||
this.EventListeners.Remove(toRemoveListener);
|
Log.Error(e, $"Exception in {blame} during {eventType} invoke.");
|
||||||
|
|
||||||
// If we are disabling an ReceiveEvent listener, check if we should disable the hook.
|
|
||||||
if (toRemoveListener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
|
|
||||||
{
|
|
||||||
// Get the ReceiveEvent Listener for this addon
|
|
||||||
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(toRemoveListener.AddonName)) is { } receiveEventListener)
|
|
||||||
{
|
|
||||||
// If there are no other listeners listening for this event, disable the hook.
|
|
||||||
if (!this.EventListeners.Any(listener => listener.AddonName.Contains(toRemoveListener.AddonName) && listener.EventType is AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent))
|
|
||||||
{
|
|
||||||
receiveEventListener.Hook?.Disable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.removeEventListeners.Clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[ServiceManager.CallWhenServicesReady]
|
|
||||||
private void ContinueConstruction()
|
|
||||||
{
|
|
||||||
this.onAddonSetupHook.Enable();
|
|
||||||
this.onAddonSetup2Hook.Enable();
|
|
||||||
this.onAddonFinalizeHook.Enable();
|
|
||||||
this.onAddonDrawHook.Enable();
|
|
||||||
this.onAddonUpdateHook.Enable();
|
|
||||||
this.onAddonRefreshHook.Enable();
|
|
||||||
this.onAddonRequestedUpdateHook.Enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RegisterReceiveEventHook(AtkUnitBase* addon)
|
private void RegisterReceiveEventHook(AtkUnitBase* addon)
|
||||||
{
|
{
|
||||||
// Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener.
|
// Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener.
|
||||||
|
|
@ -253,20 +252,13 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
||||||
{
|
{
|
||||||
Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration.");
|
Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration.");
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
this.recyclingSetupArgs.AddonInternal = (nint)addon;
|
||||||
{
|
this.recyclingSetupArgs.AtkValueCount = valueCount;
|
||||||
this.InvokeListeners(AddonEvent.PreSetup, new AddonSetupArgs
|
this.recyclingSetupArgs.AtkValues = (nint)values;
|
||||||
{
|
this.InvokeListenersSafely(AddonEvent.PreSetup, this.recyclingSetupArgs);
|
||||||
Addon = (nint)addon,
|
valueCount = this.recyclingSetupArgs.AtkValueCount;
|
||||||
AtkValueCount = valueCount,
|
values = (AtkValue*)this.recyclingSetupArgs.AtkValues;
|
||||||
AtkValues = (nint)values,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Log.Error(e, "Exception in OnAddonSetup pre-setup invoke.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -277,19 +269,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
||||||
Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method.");
|
Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method.");
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
this.InvokeListenersSafely(AddonEvent.PostSetup, this.recyclingSetupArgs);
|
||||||
{
|
|
||||||
this.InvokeListeners(AddonEvent.PostSetup, new AddonSetupArgs
|
|
||||||
{
|
|
||||||
Addon = (nint)addon,
|
|
||||||
AtkValueCount = valueCount,
|
|
||||||
AtkValues = (nint)values,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Log.Error(e, "Exception in OnAddonSetup post-setup invoke.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
|
private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
|
||||||
|
|
@ -303,15 +283,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
||||||
{
|
{
|
||||||
Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal.");
|
Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal.");
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
this.recyclingFinalizeArgs.AddonInternal = (nint)atkUnitBase[0];
|
||||||
{
|
this.InvokeListenersSafely(AddonEvent.PreFinalize, this.recyclingFinalizeArgs);
|
||||||
this.InvokeListeners(AddonEvent.PreFinalize, new AddonFinalizeArgs { Addon = (nint)atkUnitBase[0] });
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Log.Error(e, "Exception in OnAddonFinalize pre-finalize invoke.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -325,14 +299,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
||||||
|
|
||||||
private void OnAddonDraw(AtkUnitBase* addon)
|
private void OnAddonDraw(AtkUnitBase* addon)
|
||||||
{
|
{
|
||||||
try
|
this.recyclingDrawArgs.AddonInternal = (nint)addon;
|
||||||
{
|
this.InvokeListenersSafely(AddonEvent.PreDraw, this.recyclingDrawArgs);
|
||||||
this.InvokeListeners(AddonEvent.PreDraw, new AddonDrawArgs { Addon = (nint)addon });
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Log.Error(e, "Exception in OnAddonDraw pre-draw invoke.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -343,26 +311,14 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
||||||
Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method.");
|
Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method.");
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
this.InvokeListenersSafely(AddonEvent.PostDraw, this.recyclingDrawArgs);
|
||||||
{
|
|
||||||
this.InvokeListeners(AddonEvent.PostDraw, new AddonDrawArgs { Addon = (nint)addon });
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Log.Error(e, "Exception in OnAddonDraw post-draw invoke.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
|
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
|
||||||
{
|
{
|
||||||
try
|
this.recyclingUpdateArgs.AddonInternal = (nint)addon;
|
||||||
{
|
this.recyclingUpdateArgs.TimeDeltaInternal = delta;
|
||||||
this.InvokeListeners(AddonEvent.PreUpdate, new AddonUpdateArgs { Addon = (nint)addon, TimeDelta = delta });
|
this.InvokeListenersSafely(AddonEvent.PreUpdate, this.recyclingUpdateArgs);
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Log.Error(e, "Exception in OnAddonUpdate pre-update invoke.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -373,33 +329,19 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
||||||
Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method.");
|
Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method.");
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
this.InvokeListenersSafely(AddonEvent.PostUpdate, this.recyclingUpdateArgs);
|
||||||
{
|
|
||||||
this.InvokeListeners(AddonEvent.PostUpdate, new AddonUpdateArgs { Addon = (nint)addon, TimeDelta = delta });
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Log.Error(e, "Exception in OnAddonUpdate post-update invoke.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values)
|
private byte OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values)
|
||||||
{
|
{
|
||||||
byte result = 0;
|
byte result = 0;
|
||||||
|
|
||||||
try
|
this.recyclingRefreshArgs.AddonInternal = (nint)addon;
|
||||||
{
|
this.recyclingRefreshArgs.AtkValueCount = valueCount;
|
||||||
this.InvokeListeners(AddonEvent.PreRefresh, new AddonRefreshArgs
|
this.recyclingRefreshArgs.AtkValues = (nint)values;
|
||||||
{
|
this.InvokeListenersSafely(AddonEvent.PreRefresh, this.recyclingRefreshArgs);
|
||||||
Addon = (nint)addon,
|
valueCount = this.recyclingRefreshArgs.AtkValueCount;
|
||||||
AtkValueCount = valueCount,
|
values = (AtkValue*)this.recyclingRefreshArgs.AtkValues;
|
||||||
AtkValues = (nint)values,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Log.Error(e, "Exception in OnAddonRefresh pre-refresh invoke.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -410,38 +352,18 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
||||||
Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method.");
|
Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method.");
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
this.InvokeListenersSafely(AddonEvent.PostRefresh, this.recyclingRefreshArgs);
|
||||||
{
|
|
||||||
this.InvokeListeners(AddonEvent.PostRefresh, new AddonRefreshArgs
|
|
||||||
{
|
|
||||||
Addon = (nint)addon,
|
|
||||||
AtkValueCount = valueCount,
|
|
||||||
AtkValues = (nint)values,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Log.Error(e, "Exception in OnAddonRefresh post-refresh invoke.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
|
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
|
||||||
{
|
{
|
||||||
try
|
this.recyclingRequestedUpdateArgs.AddonInternal = (nint)addon;
|
||||||
{
|
this.recyclingRequestedUpdateArgs.NumberArrayData = (nint)numberArrayData;
|
||||||
this.InvokeListeners(AddonEvent.PreRequestedUpdate, new AddonRequestedUpdateArgs
|
this.recyclingRequestedUpdateArgs.StringArrayData = (nint)stringArrayData;
|
||||||
{
|
this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.recyclingRequestedUpdateArgs);
|
||||||
Addon = (nint)addon,
|
numberArrayData = (NumberArrayData**)this.recyclingRequestedUpdateArgs.NumberArrayData;
|
||||||
NumberArrayData = (nint)numberArrayData,
|
stringArrayData = (StringArrayData**)this.recyclingRequestedUpdateArgs.StringArrayData;
|
||||||
StringArrayData = (nint)stringArrayData,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Log.Error(e, "Exception in OnRequestedUpdate pre-requestedUpdate invoke.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -452,19 +374,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
||||||
Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
|
Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.recyclingRequestedUpdateArgs);
|
||||||
{
|
|
||||||
this.InvokeListeners(AddonEvent.PostRequestedUpdate, new AddonRequestedUpdateArgs
|
|
||||||
{
|
|
||||||
Addon = (nint)addon,
|
|
||||||
NumberArrayData = (nint)numberArrayData,
|
|
||||||
StringArrayData = (nint)stringArrayData,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Log.Error(e, "Exception in OnRequestedUpdate post-requestedUpdate invoke.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,13 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
|
||||||
{
|
{
|
||||||
private static readonly ModuleLog Log = new("AddonLifecycle");
|
private static readonly ModuleLog Log = new("AddonLifecycle");
|
||||||
|
|
||||||
|
// Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet
|
||||||
|
// package, and these events are always called from the main thread, this is fine.
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
|
// TODO: turn constructors of these internal
|
||||||
|
private readonly AddonReceiveEventArgs recyclingReceiveEventArgs = new();
|
||||||
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="AddonLifecycleReceiveEventListener"/> class.
|
/// Initializes a new instance of the <see cref="AddonLifecycleReceiveEventListener"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -74,22 +81,17 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
|
||||||
this.Hook!.Original(addon, eventType, eventParam, atkEvent, data);
|
this.Hook!.Original(addon, eventType, eventParam, atkEvent, data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
this.recyclingReceiveEventArgs.AddonInternal = (nint)addon;
|
||||||
{
|
this.recyclingReceiveEventArgs.AtkEventType = (byte)eventType;
|
||||||
this.AddonLifecycle.InvokeListeners(AddonEvent.PreReceiveEvent, new AddonReceiveEventArgs
|
this.recyclingReceiveEventArgs.EventParam = eventParam;
|
||||||
{
|
this.recyclingReceiveEventArgs.AtkEvent = (IntPtr)atkEvent;
|
||||||
Addon = (nint)addon,
|
this.recyclingReceiveEventArgs.Data = data;
|
||||||
AtkEventType = (byte)eventType,
|
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.recyclingReceiveEventArgs);
|
||||||
EventParam = eventParam,
|
eventType = (AtkEventType)this.recyclingReceiveEventArgs.AtkEventType;
|
||||||
AtkEvent = (nint)atkEvent,
|
eventParam = this.recyclingReceiveEventArgs.EventParam;
|
||||||
Data = data,
|
atkEvent = (AtkEvent*)this.recyclingReceiveEventArgs.AtkEvent;
|
||||||
});
|
data = this.recyclingReceiveEventArgs.Data;
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Log.Error(e, "Exception in OnReceiveEvent pre-receiveEvent invoke.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -100,20 +102,6 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
|
||||||
Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
|
Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.recyclingReceiveEventArgs);
|
||||||
{
|
|
||||||
this.AddonLifecycle.InvokeListeners(AddonEvent.PostReceiveEvent, new AddonReceiveEventArgs
|
|
||||||
{
|
|
||||||
Addon = (nint)addon,
|
|
||||||
AtkEventType = (byte)eventType,
|
|
||||||
EventParam = eventParam,
|
|
||||||
AtkEvent = (nint)atkEvent,
|
|
||||||
Data = data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Log.Error(e, "Exception in OnAddonRefresh post-receiveEvent invoke.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,8 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState
|
||||||
this.framework.Update += this.FrameworkOnOnUpdateEvent;
|
this.framework.Update += this.FrameworkOnOnUpdateEvent;
|
||||||
|
|
||||||
this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop;
|
this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop;
|
||||||
|
|
||||||
|
this.setupTerritoryTypeHook.Enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||||
|
|
@ -120,12 +122,6 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState
|
||||||
this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop;
|
this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop;
|
||||||
}
|
}
|
||||||
|
|
||||||
[ServiceManager.CallWhenServicesReady]
|
|
||||||
private void ContinueConstruction()
|
|
||||||
{
|
|
||||||
this.setupTerritoryTypeHook.Enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private IntPtr SetupTerritoryTypeDetour(IntPtr manager, ushort terriType)
|
private IntPtr SetupTerritoryTypeDetour(IntPtr manager, ushort terriType)
|
||||||
{
|
{
|
||||||
this.TerritoryType = terriType;
|
this.TerritoryType = terriType;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ internal sealed partial class Condition : IServiceType, ICondition
|
||||||
/// Gets the current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has.
|
/// Gets the current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal const int MaxConditionEntries = 104;
|
internal const int MaxConditionEntries = 104;
|
||||||
|
|
||||||
|
[ServiceManager.ServiceDependency]
|
||||||
|
private readonly Framework framework = Service<Framework>.Get();
|
||||||
|
|
||||||
private readonly bool[] cache = new bool[MaxConditionEntries];
|
private readonly bool[] cache = new bool[MaxConditionEntries];
|
||||||
|
|
||||||
|
|
@ -24,6 +27,12 @@ internal sealed partial class Condition : IServiceType, ICondition
|
||||||
{
|
{
|
||||||
var resolver = clientState.AddressResolver;
|
var resolver = clientState.AddressResolver;
|
||||||
this.Address = resolver.ConditionFlags;
|
this.Address = resolver.ConditionFlags;
|
||||||
|
|
||||||
|
// Initialization
|
||||||
|
for (var i = 0; i < MaxConditionEntries; i++)
|
||||||
|
this.cache[i] = this[i];
|
||||||
|
|
||||||
|
this.framework.Update += this.FrameworkUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
|
@ -80,17 +89,7 @@ internal sealed partial class Condition : IServiceType, ICondition
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
[ServiceManager.CallWhenServicesReady]
|
private void FrameworkUpdate(IFramework unused)
|
||||||
private void ContinueConstruction(Framework framework)
|
|
||||||
{
|
|
||||||
// Initialization
|
|
||||||
for (var i = 0; i < MaxConditionEntries; i++)
|
|
||||||
this.cache[i] = this[i];
|
|
||||||
|
|
||||||
framework.Update += this.FrameworkUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FrameworkUpdate(IFramework framework)
|
|
||||||
{
|
{
|
||||||
for (var i = 0; i < MaxConditionEntries; i++)
|
for (var i = 0; i < MaxConditionEntries; i++)
|
||||||
{
|
{
|
||||||
|
|
@ -144,7 +143,7 @@ internal sealed partial class Condition : IDisposable
|
||||||
|
|
||||||
if (disposing)
|
if (disposing)
|
||||||
{
|
{
|
||||||
Service<Framework>.Get().Update -= this.FrameworkUpdate;
|
this.framework.Update -= this.FrameworkUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isDisposed = true;
|
this.isDisposed = true;
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState
|
||||||
var resolver = clientState.AddressResolver;
|
var resolver = clientState.AddressResolver;
|
||||||
Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}");
|
Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}");
|
||||||
this.gamepadPoll = Hook<ControllerPoll>.FromAddress(resolver.GamepadPoll, this.GamepadPollDetour);
|
this.gamepadPoll = Hook<ControllerPoll>.FromAddress(resolver.GamepadPoll, this.GamepadPollDetour);
|
||||||
|
this.gamepadPoll?.Enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
private delegate int ControllerPoll(IntPtr controllerInput);
|
private delegate int ControllerPoll(IntPtr controllerInput);
|
||||||
|
|
@ -114,12 +115,6 @@ internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
[ServiceManager.CallWhenServicesReady]
|
|
||||||
private void ContinueConstruction()
|
|
||||||
{
|
|
||||||
this.gamepadPoll?.Enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private int GamepadPollDetour(IntPtr gamepadInput)
|
private int GamepadPollDetour(IntPtr gamepadInput)
|
||||||
{
|
{
|
||||||
var original = this.gamepadPoll!.Original(gamepadInput);
|
var original = this.gamepadPoll!.Original(gamepadInput);
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState
|
||||||
|
|
||||||
this.framework.Update += this.FrameworkOnUpdateEvent;
|
this.framework.Update += this.FrameworkOnUpdateEvent;
|
||||||
this.clientState.TerritoryChanged += this.TerritoryOnChangedEvent;
|
this.clientState.TerritoryChanged += this.TerritoryOnChangedEvent;
|
||||||
|
|
||||||
|
this.contentDirectorNetworkMessageHook.Enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||||
|
|
@ -67,12 +69,6 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState
|
||||||
this.clientState.TerritoryChanged -= this.TerritoryOnChangedEvent;
|
this.clientState.TerritoryChanged -= this.TerritoryOnChangedEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
[ServiceManager.CallWhenServicesReady]
|
|
||||||
private void ContinueConstruction()
|
|
||||||
{
|
|
||||||
this.contentDirectorNetworkMessageHook.Enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte ContentDirectorNetworkMessageDetour(IntPtr a1, IntPtr a2, ushort* a3)
|
private byte ContentDirectorNetworkMessageDetour(IntPtr a1, IntPtr a2, ushort* a3)
|
||||||
{
|
{
|
||||||
var category = *a3;
|
var category = *a3;
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,9 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
||||||
|
|
||||||
this.updateHook = Hook<OnUpdateDetour>.FromAddress(this.addressResolver.TickAddress, this.HandleFrameworkUpdate);
|
this.updateHook = Hook<OnUpdateDetour>.FromAddress(this.addressResolver.TickAddress, this.HandleFrameworkUpdate);
|
||||||
this.destroyHook = Hook<OnRealDestroyDelegate>.FromAddress(this.addressResolver.DestroyAddress, this.HandleFrameworkDestroy);
|
this.destroyHook = Hook<OnRealDestroyDelegate>.FromAddress(this.addressResolver.DestroyAddress, this.HandleFrameworkDestroy);
|
||||||
|
|
||||||
|
this.updateHook.Enable();
|
||||||
|
this.destroyHook.Enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -330,13 +333,6 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[ServiceManager.CallWhenServicesReady]
|
|
||||||
private void ContinueConstruction()
|
|
||||||
{
|
|
||||||
this.updateHook.Enable();
|
|
||||||
this.destroyHook.Enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RunPendingTickTasks()
|
private void RunPendingTickTasks()
|
||||||
{
|
{
|
||||||
if (this.runOnNextTickTaskList.Count == 0 && this.runOnNextTickTaskList2.Count == 0)
|
if (this.runOnNextTickTaskList.Count == 0 && this.runOnNextTickTaskList2.Count == 0)
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,38 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
using Dalamud.Configuration.Internal;
|
using Dalamud.Configuration.Internal;
|
||||||
using Dalamud.Game.Libc;
|
|
||||||
using Dalamud.Game.Text;
|
using Dalamud.Game.Text;
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
using Dalamud.Hooking;
|
using Dalamud.Hooking;
|
||||||
using Dalamud.IoC;
|
using Dalamud.IoC;
|
||||||
using Dalamud.IoC.Internal;
|
using Dalamud.IoC.Internal;
|
||||||
|
using Dalamud.Logging.Internal;
|
||||||
|
using Dalamud.Memory;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
using Serilog;
|
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||||
|
|
||||||
namespace Dalamud.Game.Gui;
|
namespace Dalamud.Game.Gui;
|
||||||
|
|
||||||
|
// TODO(api10): Update IChatGui, ChatGui and XivChatEntry to use correct types and names:
|
||||||
|
// "uint SenderId" should be "int Timestamp".
|
||||||
|
// "IntPtr Parameters" should be something like "bool Silent". It suppresses new message sounds in certain channels.
|
||||||
|
// This has to be a 1 byte boolean, so only change it to bool if marshalling is disabled.
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This class handles interacting with the native chat UI.
|
/// This class handles interacting with the native chat UI.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[InterfaceVersion("1.0")]
|
[InterfaceVersion("1.0")]
|
||||||
[ServiceManager.BlockingEarlyLoadedService]
|
[ServiceManager.BlockingEarlyLoadedService]
|
||||||
internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
|
internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui
|
||||||
{
|
{
|
||||||
|
private static readonly ModuleLog Log = new("ChatGui");
|
||||||
|
|
||||||
private readonly ChatGuiAddressResolver address;
|
private readonly ChatGuiAddressResolver address;
|
||||||
|
|
||||||
private readonly Queue<XivChatEntry> chatQueue = new();
|
private readonly Queue<XivChatEntry> chatQueue = new();
|
||||||
|
|
@ -36,10 +45,7 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
|
||||||
[ServiceManager.ServiceDependency]
|
[ServiceManager.ServiceDependency]
|
||||||
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
|
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
|
||||||
|
|
||||||
[ServiceManager.ServiceDependency]
|
private ImmutableDictionary<(string PluginName, uint CommandId), Action<uint, SeString>>? dalamudLinkHandlersCopy;
|
||||||
private readonly LibcFunction libcFunction = Service<LibcFunction>.Get();
|
|
||||||
|
|
||||||
private IntPtr baseAddress = IntPtr.Zero;
|
|
||||||
|
|
||||||
[ServiceManager.ServiceConstructor]
|
[ServiceManager.ServiceConstructor]
|
||||||
private ChatGui(TargetSigScanner sigScanner)
|
private ChatGui(TargetSigScanner sigScanner)
|
||||||
|
|
@ -47,13 +53,17 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
|
||||||
this.address = new ChatGuiAddressResolver();
|
this.address = new ChatGuiAddressResolver();
|
||||||
this.address.Setup(sigScanner);
|
this.address.Setup(sigScanner);
|
||||||
|
|
||||||
this.printMessageHook = Hook<PrintMessageDelegate>.FromAddress(this.address.PrintMessage, this.HandlePrintMessageDetour);
|
this.printMessageHook = Hook<PrintMessageDelegate>.FromAddress((nint)RaptureLogModule.Addresses.PrintMessage.Value, this.HandlePrintMessageDetour);
|
||||||
this.populateItemLinkHook = Hook<PopulateItemLinkDelegate>.FromAddress(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour);
|
this.populateItemLinkHook = Hook<PopulateItemLinkDelegate>.FromAddress(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour);
|
||||||
this.interactableLinkClickedHook = Hook<InteractableLinkClickedDelegate>.FromAddress(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour);
|
this.interactableLinkClickedHook = Hook<InteractableLinkClickedDelegate>.FromAddress(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour);
|
||||||
|
|
||||||
|
this.printMessageHook.Enable();
|
||||||
|
this.populateItemLinkHook.Enable();
|
||||||
|
this.interactableLinkClickedHook.Enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||||
private delegate IntPtr PrintMessageDelegate(IntPtr manager, XivChatType chatType, IntPtr senderName, IntPtr message, uint senderId, IntPtr parameter);
|
private delegate uint PrintMessageDelegate(RaptureLogModule* manager, XivChatType chatType, Utf8String* sender, Utf8String* message, int timestamp, byte silent);
|
||||||
|
|
||||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||||
private delegate void PopulateItemLinkDelegate(IntPtr linkObjectPtr, IntPtr itemInfoPtr);
|
private delegate void PopulateItemLinkDelegate(IntPtr linkObjectPtr, IntPtr itemInfoPtr);
|
||||||
|
|
@ -80,7 +90,21 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
|
||||||
public byte LastLinkedItemFlags { get; private set; }
|
public byte LastLinkedItemFlags { get; private set; }
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IReadOnlyDictionary<(string PluginName, uint CommandId), Action<uint, SeString>> RegisteredLinkHandlers => this.dalamudLinkHandlers;
|
public IReadOnlyDictionary<(string PluginName, uint CommandId), Action<uint, SeString>> RegisteredLinkHandlers
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var copy = this.dalamudLinkHandlersCopy;
|
||||||
|
if (copy is not null)
|
||||||
|
return copy;
|
||||||
|
|
||||||
|
lock (this.dalamudLinkHandlers)
|
||||||
|
{
|
||||||
|
return this.dalamudLinkHandlersCopy ??=
|
||||||
|
this.dalamudLinkHandlers.ToImmutableDictionary(x => x.Key, x => x.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispose of managed and unmanaged resources.
|
/// Dispose of managed and unmanaged resources.
|
||||||
|
|
@ -131,18 +155,13 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
|
||||||
{
|
{
|
||||||
var chat = this.chatQueue.Dequeue();
|
var chat = this.chatQueue.Dequeue();
|
||||||
|
|
||||||
if (this.baseAddress == IntPtr.Zero)
|
var sender = Utf8String.FromSequence(chat.Name.Encode());
|
||||||
{
|
var message = Utf8String.FromSequence(chat.Message.Encode());
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var senderRaw = (chat.Name ?? string.Empty).Encode();
|
this.HandlePrintMessageDetour(RaptureLogModule.Instance(), chat.Type, sender, message, (int)chat.SenderId, (byte)(chat.Parameters != 0 ? 1 : 0));
|
||||||
using var senderOwned = this.libcFunction.NewString(senderRaw);
|
|
||||||
|
|
||||||
var messageRaw = (chat.Message ?? string.Empty).Encode();
|
sender->Dtor(true);
|
||||||
using var messageOwned = this.libcFunction.NewString(messageRaw);
|
message->Dtor(true);
|
||||||
|
|
||||||
this.HandlePrintMessageDetour(this.baseAddress, chat.Type, senderOwned.Address, messageOwned.Address, chat.SenderId, chat.Parameters);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -156,7 +175,12 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
|
||||||
internal DalamudLinkPayload AddChatLinkHandler(string pluginName, uint commandId, Action<uint, SeString> commandAction)
|
internal DalamudLinkPayload AddChatLinkHandler(string pluginName, uint commandId, Action<uint, SeString> commandAction)
|
||||||
{
|
{
|
||||||
var payload = new DalamudLinkPayload { Plugin = pluginName, CommandId = commandId };
|
var payload = new DalamudLinkPayload { Plugin = pluginName, CommandId = commandId };
|
||||||
this.dalamudLinkHandlers.Add((pluginName, commandId), commandAction);
|
lock (this.dalamudLinkHandlers)
|
||||||
|
{
|
||||||
|
this.dalamudLinkHandlers.Add((pluginName, commandId), commandAction);
|
||||||
|
this.dalamudLinkHandlersCopy = null;
|
||||||
|
}
|
||||||
|
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -166,9 +190,14 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
|
||||||
/// <param name="pluginName">The name of the plugin handling the links.</param>
|
/// <param name="pluginName">The name of the plugin handling the links.</param>
|
||||||
internal void RemoveChatLinkHandler(string pluginName)
|
internal void RemoveChatLinkHandler(string pluginName)
|
||||||
{
|
{
|
||||||
foreach (var handler in this.dalamudLinkHandlers.Keys.ToList().Where(k => k.PluginName == pluginName))
|
lock (this.dalamudLinkHandlers)
|
||||||
{
|
{
|
||||||
this.dalamudLinkHandlers.Remove(handler);
|
var changed = false;
|
||||||
|
|
||||||
|
foreach (var handler in this.RegisteredLinkHandlers.Keys.Where(k => k.PluginName == pluginName))
|
||||||
|
changed |= this.dalamudLinkHandlers.Remove(handler);
|
||||||
|
if (changed)
|
||||||
|
this.dalamudLinkHandlersCopy = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,15 +208,11 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
|
||||||
/// <param name="commandId">The ID of the command to be removed.</param>
|
/// <param name="commandId">The ID of the command to be removed.</param>
|
||||||
internal void RemoveChatLinkHandler(string pluginName, uint commandId)
|
internal void RemoveChatLinkHandler(string pluginName, uint commandId)
|
||||||
{
|
{
|
||||||
this.dalamudLinkHandlers.Remove((pluginName, commandId));
|
lock (this.dalamudLinkHandlers)
|
||||||
}
|
{
|
||||||
|
if (this.dalamudLinkHandlers.Remove((pluginName, commandId)))
|
||||||
[ServiceManager.CallWhenServicesReady]
|
this.dalamudLinkHandlersCopy = null;
|
||||||
private void ContinueConstruction()
|
}
|
||||||
{
|
|
||||||
this.printMessageHook.Enable();
|
|
||||||
this.populateItemLinkHook.Enable();
|
|
||||||
this.interactableLinkClickedHook.Enable();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PrintTagged(string message, XivChatType channel, string? tag, ushort? color)
|
private void PrintTagged(string message, XivChatType channel, string? tag, ushort? color)
|
||||||
|
|
@ -254,29 +279,17 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IntPtr HandlePrintMessageDetour(IntPtr manager, XivChatType chatType, IntPtr pSenderName, IntPtr pMessage, uint senderId, IntPtr parameter)
|
private uint HandlePrintMessageDetour(RaptureLogModule* manager, XivChatType chatType, Utf8String* sender, Utf8String* message, int timestamp, byte silent)
|
||||||
{
|
{
|
||||||
var retVal = IntPtr.Zero;
|
var messageId = 0u;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sender = StdString.ReadFromPointer(pSenderName);
|
var originalSenderData = sender->AsSpan().ToArray();
|
||||||
var parsedSender = SeString.Parse(sender.RawData);
|
var originalMessageData = message->AsSpan().ToArray();
|
||||||
var originalSenderData = (byte[])sender.RawData.Clone();
|
|
||||||
var oldEditedSender = parsedSender.Encode();
|
|
||||||
var senderPtr = pSenderName;
|
|
||||||
OwnedStdString allocatedString = null;
|
|
||||||
|
|
||||||
var message = StdString.ReadFromPointer(pMessage);
|
var parsedSender = SeString.Parse(originalSenderData);
|
||||||
var parsedMessage = SeString.Parse(message.RawData);
|
var parsedMessage = SeString.Parse(originalMessageData);
|
||||||
var originalMessageData = (byte[])message.RawData.Clone();
|
|
||||||
var oldEdited = parsedMessage.Encode();
|
|
||||||
var messagePtr = pMessage;
|
|
||||||
OwnedStdString allocatedStringSender = null;
|
|
||||||
|
|
||||||
// Log.Verbose("[CHATGUI][{0}][{1}]", parsedSender.TextValue, parsedMessage.TextValue);
|
|
||||||
|
|
||||||
// Log.Debug($"HandlePrintMessageDetour {manager} - [{chattype}] [{BitConverter.ToString(message.RawData).Replace("-", " ")}] {message.Value} from {senderName.Value}");
|
|
||||||
|
|
||||||
// Call events
|
// Call events
|
||||||
var isHandled = false;
|
var isHandled = false;
|
||||||
|
|
@ -287,7 +300,7 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var messageHandledDelegate = @delegate as IChatGui.OnCheckMessageHandledDelegate;
|
var messageHandledDelegate = @delegate as IChatGui.OnCheckMessageHandledDelegate;
|
||||||
messageHandledDelegate!.Invoke(chatType, senderId, ref parsedSender, ref parsedMessage, ref isHandled);
|
messageHandledDelegate!.Invoke(chatType, (uint)timestamp, ref parsedSender, ref parsedMessage, ref isHandled);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|
@ -303,7 +316,7 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var messageHandledDelegate = @delegate as IChatGui.OnMessageDelegate;
|
var messageHandledDelegate = @delegate as IChatGui.OnMessageDelegate;
|
||||||
messageHandledDelegate!.Invoke(chatType, senderId, ref parsedSender, ref parsedMessage, ref isHandled);
|
messageHandledDelegate!.Invoke(chatType, (uint)timestamp, ref parsedSender, ref parsedMessage, ref isHandled);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|
@ -312,61 +325,39 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var newEdited = parsedMessage.Encode();
|
var possiblyModifiedSenderData = parsedSender.Encode();
|
||||||
if (!Util.FastByteArrayCompare(oldEdited, newEdited))
|
var possiblyModifiedMessageData = parsedMessage.Encode();
|
||||||
|
|
||||||
|
if (!Util.FastByteArrayCompare(originalSenderData, possiblyModifiedSenderData))
|
||||||
{
|
{
|
||||||
Log.Verbose("SeString was edited, taking precedence over StdString edit.");
|
Log.Verbose($"HandlePrintMessageDetour Sender modified: {SeString.Parse(originalSenderData)} -> {parsedSender}");
|
||||||
message.RawData = newEdited;
|
sender->SetString(possiblyModifiedSenderData);
|
||||||
// Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Util.FastByteArrayCompare(originalMessageData, message.RawData))
|
if (!Util.FastByteArrayCompare(originalMessageData, possiblyModifiedMessageData))
|
||||||
{
|
{
|
||||||
allocatedString = this.libcFunction.NewString(message.RawData);
|
Log.Verbose($"HandlePrintMessageDetour Message modified: {SeString.Parse(originalMessageData)} -> {parsedMessage}");
|
||||||
Log.Debug($"HandlePrintMessageDetour String modified: {originalMessageData}({messagePtr}) -> {message}({allocatedString.Address})");
|
message->SetString(possiblyModifiedMessageData);
|
||||||
messagePtr = allocatedString.Address;
|
|
||||||
}
|
|
||||||
|
|
||||||
var newEditedSender = parsedSender.Encode();
|
|
||||||
if (!Util.FastByteArrayCompare(oldEditedSender, newEditedSender))
|
|
||||||
{
|
|
||||||
Log.Verbose("SeString was edited, taking precedence over StdString edit.");
|
|
||||||
sender.RawData = newEditedSender;
|
|
||||||
// Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Util.FastByteArrayCompare(originalSenderData, sender.RawData))
|
|
||||||
{
|
|
||||||
allocatedStringSender = this.libcFunction.NewString(sender.RawData);
|
|
||||||
Log.Debug(
|
|
||||||
$"HandlePrintMessageDetour Sender modified: {originalSenderData}({senderPtr}) -> {sender}({allocatedStringSender.Address})");
|
|
||||||
senderPtr = allocatedStringSender.Address;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print the original chat if it's handled.
|
// Print the original chat if it's handled.
|
||||||
if (isHandled)
|
if (isHandled)
|
||||||
{
|
{
|
||||||
this.ChatMessageHandled?.Invoke(chatType, senderId, parsedSender, parsedMessage);
|
this.ChatMessageHandled?.Invoke(chatType, (uint)timestamp, parsedSender, parsedMessage);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
retVal = this.printMessageHook.Original(manager, chatType, senderPtr, messagePtr, senderId, parameter);
|
messageId = this.printMessageHook.Original(manager, chatType, sender, message, timestamp, silent);
|
||||||
this.ChatMessageUnhandled?.Invoke(chatType, senderId, parsedSender, parsedMessage);
|
this.ChatMessageUnhandled?.Invoke(chatType, (uint)timestamp, parsedSender, parsedMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.baseAddress == IntPtr.Zero)
|
|
||||||
this.baseAddress = manager;
|
|
||||||
|
|
||||||
allocatedString?.Dispose();
|
|
||||||
allocatedStringSender?.Dispose();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Error(ex, "Exception on OnChatMessage hook.");
|
Log.Error(ex, "Exception on OnChatMessage hook.");
|
||||||
retVal = this.printMessageHook.Original(manager, chatType, pSenderName, pMessage, senderId, parameter);
|
messageId = this.printMessageHook.Original(manager, chatType, sender, message, timestamp, silent);
|
||||||
}
|
}
|
||||||
|
|
||||||
return retVal;
|
return messageId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InteractableLinkClickedDetour(IntPtr managerPtr, IntPtr messagePtr)
|
private void InteractableLinkClickedDetour(IntPtr managerPtr, IntPtr messagePtr)
|
||||||
|
|
@ -384,18 +375,14 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
|
||||||
Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}");
|
Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}");
|
||||||
|
|
||||||
var payloadPtr = Marshal.ReadIntPtr(messagePtr, 0x10);
|
var payloadPtr = Marshal.ReadIntPtr(messagePtr, 0x10);
|
||||||
var messageSize = 0;
|
var seStr = MemoryHelper.ReadSeStringNullTerminated(payloadPtr);
|
||||||
while (Marshal.ReadByte(payloadPtr, messageSize) != 0) messageSize++;
|
|
||||||
var payloadBytes = new byte[messageSize];
|
|
||||||
Marshal.Copy(payloadPtr, payloadBytes, 0, messageSize);
|
|
||||||
var seStr = SeString.Parse(payloadBytes);
|
|
||||||
var terminatorIndex = seStr.Payloads.IndexOf(RawPayload.LinkTerminator);
|
var terminatorIndex = seStr.Payloads.IndexOf(RawPayload.LinkTerminator);
|
||||||
var payloads = terminatorIndex >= 0 ? seStr.Payloads.Take(terminatorIndex + 1).ToList() : seStr.Payloads;
|
var payloads = terminatorIndex >= 0 ? seStr.Payloads.Take(terminatorIndex + 1).ToList() : seStr.Payloads;
|
||||||
if (payloads.Count == 0) return;
|
if (payloads.Count == 0) return;
|
||||||
var linkPayload = payloads[0];
|
var linkPayload = payloads[0];
|
||||||
if (linkPayload is DalamudLinkPayload link)
|
if (linkPayload is DalamudLinkPayload link)
|
||||||
{
|
{
|
||||||
if (this.dalamudLinkHandlers.TryGetValue((link.Plugin, link.CommandId), out var value))
|
if (this.RegisteredLinkHandlers.TryGetValue((link.Plugin, link.CommandId), out var value))
|
||||||
{
|
{
|
||||||
Log.Verbose($"Sending DalamudLink to {link.Plugin}: {link.CommandId}");
|
Log.Verbose($"Sending DalamudLink to {link.Plugin}: {link.CommandId}");
|
||||||
value.Invoke(link.CommandId, new SeString(payloads));
|
value.Invoke(link.CommandId, new SeString(payloads));
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,6 @@ namespace Dalamud.Game.Gui;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class ChatGuiAddressResolver : BaseAddressResolver
|
internal sealed class ChatGuiAddressResolver : BaseAddressResolver
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Gets the address of the native PrintMessage method.
|
|
||||||
/// </summary>
|
|
||||||
public IntPtr PrintMessage { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the address of the native PopulateItemLinkObject method.
|
/// Gets the address of the native PopulateItemLinkObject method.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -20,77 +15,9 @@ internal sealed class ChatGuiAddressResolver : BaseAddressResolver
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IntPtr InteractableLinkClicked { get; private set; }
|
public IntPtr InteractableLinkClicked { get; private set; }
|
||||||
|
|
||||||
/*
|
|
||||||
--- for reference: 4.57 ---
|
|
||||||
.text:00000001405CD210 ; __int64 __fastcall Xiv::Gui::ChatGui::PrintMessage(__int64 handler, unsigned __int16 chatType, __int64 senderName, __int64 message, int senderActorId, char isLocal)
|
|
||||||
.text:00000001405CD210 Xiv__Gui__ChatGui__PrintMessage proc near
|
|
||||||
.text:00000001405CD210 ; CODE XREF: sub_1401419F0+201↑p
|
|
||||||
.text:00000001405CD210 ; sub_140141D10+220↑p ...
|
|
||||||
.text:00000001405CD210
|
|
||||||
.text:00000001405CD210 var_220 = qword ptr -220h
|
|
||||||
.text:00000001405CD210 var_218 = byte ptr -218h
|
|
||||||
.text:00000001405CD210 var_210 = word ptr -210h
|
|
||||||
.text:00000001405CD210 var_208 = byte ptr -208h
|
|
||||||
.text:00000001405CD210 var_200 = word ptr -200h
|
|
||||||
.text:00000001405CD210 var_1FC = dword ptr -1FCh
|
|
||||||
.text:00000001405CD210 var_1F8 = qword ptr -1F8h
|
|
||||||
.text:00000001405CD210 var_1F0 = qword ptr -1F0h
|
|
||||||
.text:00000001405CD210 var_1E8 = qword ptr -1E8h
|
|
||||||
.text:00000001405CD210 var_1E0 = dword ptr -1E0h
|
|
||||||
.text:00000001405CD210 var_1DC = word ptr -1DCh
|
|
||||||
.text:00000001405CD210 var_1DA = word ptr -1DAh
|
|
||||||
.text:00000001405CD210 var_1D8 = qword ptr -1D8h
|
|
||||||
.text:00000001405CD210 var_1D0 = byte ptr -1D0h
|
|
||||||
.text:00000001405CD210 var_1C8 = qword ptr -1C8h
|
|
||||||
.text:00000001405CD210 var_1B0 = dword ptr -1B0h
|
|
||||||
.text:00000001405CD210 var_1AC = dword ptr -1ACh
|
|
||||||
.text:00000001405CD210 var_1A8 = dword ptr -1A8h
|
|
||||||
.text:00000001405CD210 var_1A4 = dword ptr -1A4h
|
|
||||||
.text:00000001405CD210 var_1A0 = dword ptr -1A0h
|
|
||||||
.text:00000001405CD210 var_160 = dword ptr -160h
|
|
||||||
.text:00000001405CD210 var_15C = dword ptr -15Ch
|
|
||||||
.text:00000001405CD210 var_140 = dword ptr -140h
|
|
||||||
.text:00000001405CD210 var_138 = dword ptr -138h
|
|
||||||
.text:00000001405CD210 var_130 = byte ptr -130h
|
|
||||||
.text:00000001405CD210 var_C0 = byte ptr -0C0h
|
|
||||||
.text:00000001405CD210 var_50 = qword ptr -50h
|
|
||||||
.text:00000001405CD210 var_38 = qword ptr -38h
|
|
||||||
.text:00000001405CD210 var_30 = qword ptr -30h
|
|
||||||
.text:00000001405CD210 var_28 = qword ptr -28h
|
|
||||||
.text:00000001405CD210 var_20 = qword ptr -20h
|
|
||||||
.text:00000001405CD210 senderActorId = dword ptr 30h
|
|
||||||
.text:00000001405CD210 isLocal = byte ptr 38h
|
|
||||||
.text:00000001405CD210
|
|
||||||
.text:00000001405CD210 ; __unwind { // __GSHandlerCheck
|
|
||||||
.text:00000001405CD210 push rbp
|
|
||||||
.text:00000001405CD212 push rdi
|
|
||||||
.text:00000001405CD213 push r14
|
|
||||||
.text:00000001405CD215 push r15
|
|
||||||
.text:00000001405CD217 lea rbp, [rsp-128h]
|
|
||||||
.text:00000001405CD21F sub rsp, 228h
|
|
||||||
.text:00000001405CD226 mov rax, cs:__security_cookie
|
|
||||||
.text:00000001405CD22D xor rax, rsp
|
|
||||||
.text:00000001405CD230 mov [rbp+140h+var_50], rax
|
|
||||||
.text:00000001405CD237 xor r10b, r10b
|
|
||||||
.text:00000001405CD23A mov [rsp+240h+var_1F8], rcx
|
|
||||||
.text:00000001405CD23F xor eax, eax
|
|
||||||
.text:00000001405CD241 mov r11, r9
|
|
||||||
.text:00000001405CD244 mov r14, r8
|
|
||||||
.text:00000001405CD247 mov r9d, eax
|
|
||||||
.text:00000001405CD24A movzx r15d, dx
|
|
||||||
.text:00000001405CD24E lea r8, [rcx+0C10h]
|
|
||||||
.text:00000001405CD255 mov rdi, rcx
|
|
||||||
*/
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
protected override void Setup64Bit(ISigScanner sig)
|
protected override void Setup64Bit(ISigScanner sig)
|
||||||
{
|
{
|
||||||
// PrintMessage = sig.ScanText("4055 57 41 ?? 41 ?? 488DAC24D8FEFFFF 4881EC28020000 488B05???????? 4833C4 488985F0000000 4532D2 48894C2448"); LAST PART FOR 5.1???
|
|
||||||
this.PrintMessage = sig.ScanText("40 55 53 56 41 54 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC 20 02 00 00 48 8B 05");
|
|
||||||
// PrintMessage = sig.ScanText("4055 57 41 ?? 41 ?? 488DAC24E8FEFFFF 4881EC18020000 488B05???????? 4833C4 488985E0000000 4532D2 48894C2438"); old
|
|
||||||
|
|
||||||
// PrintMessage = sig.ScanText("40 55 57 41 56 41 57 48 8D AC 24 D8 FE FF FF 48 81 EC 28 02 00 00 48 8B 05 63 47 4A 01 48 33 C4 48 89 85 F0 00 00 00 45 32 D2 48 89 4C 24 48 33");
|
|
||||||
|
|
||||||
// PopulateItemLinkObject = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 FA F2 B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A");
|
// PopulateItemLinkObject = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 FA F2 B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A");
|
||||||
|
|
||||||
// PopulateItemLinkObject = sig.ScanText( "48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); 5.0
|
// PopulateItemLinkObject = sig.ScanText( "48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); 5.0
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui
|
||||||
|
|
||||||
this.addFlyTextNative = Marshal.GetDelegateForFunctionPointer<AddFlyTextDelegate>(this.Address.AddFlyText);
|
this.addFlyTextNative = Marshal.GetDelegateForFunctionPointer<AddFlyTextDelegate>(this.Address.AddFlyText);
|
||||||
this.createFlyTextHook = Hook<CreateFlyTextDelegate>.FromAddress(this.Address.CreateFlyText, this.CreateFlyTextDetour);
|
this.createFlyTextHook = Hook<CreateFlyTextDelegate>.FromAddress(this.Address.CreateFlyText, this.CreateFlyTextDetour);
|
||||||
|
|
||||||
|
this.createFlyTextHook.Enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -143,12 +145,6 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui
|
||||||
return terminated;
|
return terminated;
|
||||||
}
|
}
|
||||||
|
|
||||||
[ServiceManager.CallWhenServicesReady]
|
|
||||||
private void ContinueConstruction(GameGui gameGui)
|
|
||||||
{
|
|
||||||
this.createFlyTextHook.Enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private IntPtr CreateFlyTextDetour(
|
private IntPtr CreateFlyTextDetour(
|
||||||
IntPtr addonFlyText,
|
IntPtr addonFlyText,
|
||||||
FlyTextKind kind,
|
FlyTextKind kind,
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,15 @@ internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui
|
||||||
this.toggleUiHideHook = Hook<ToggleUiHideDelegate>.FromAddress(this.address.ToggleUiHide, this.ToggleUiHideDetour);
|
this.toggleUiHideHook = Hook<ToggleUiHideDelegate>.FromAddress(this.address.ToggleUiHide, this.ToggleUiHideDetour);
|
||||||
|
|
||||||
this.utf8StringFromSequenceHook = Hook<Utf8StringFromSequenceDelegate>.FromAddress(this.address.Utf8StringFromSequence, this.Utf8StringFromSequenceDetour);
|
this.utf8StringFromSequenceHook = Hook<Utf8StringFromSequenceDelegate>.FromAddress(this.address.Utf8StringFromSequence, this.Utf8StringFromSequenceDetour);
|
||||||
|
|
||||||
|
this.setGlobalBgmHook.Enable();
|
||||||
|
this.handleItemHoverHook.Enable();
|
||||||
|
this.handleItemOutHook.Enable();
|
||||||
|
this.handleImmHook.Enable();
|
||||||
|
this.toggleUiHideHook.Enable();
|
||||||
|
this.handleActionHoverHook.Enable();
|
||||||
|
this.handleActionOutHook.Enable();
|
||||||
|
this.utf8StringFromSequenceHook.Enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marshaled delegates
|
// Marshaled delegates
|
||||||
|
|
@ -376,19 +385,6 @@ internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui
|
||||||
this.GameUiHidden = false;
|
this.GameUiHidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
[ServiceManager.CallWhenServicesReady]
|
|
||||||
private void ContinueConstruction()
|
|
||||||
{
|
|
||||||
this.setGlobalBgmHook.Enable();
|
|
||||||
this.handleItemHoverHook.Enable();
|
|
||||||
this.handleItemOutHook.Enable();
|
|
||||||
this.handleImmHook.Enable();
|
|
||||||
this.toggleUiHideHook.Enable();
|
|
||||||
this.handleActionHoverHook.Enable();
|
|
||||||
this.handleActionOutHook.Enable();
|
|
||||||
this.utf8StringFromSequenceHook.Enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private IntPtr HandleSetGlobalBgmDetour(ushort bgmKey, byte a2, uint a3, uint a4, uint a5, byte a6)
|
private IntPtr HandleSetGlobalBgmDetour(ushort bgmKey, byte a2, uint a3, uint a4, uint a5, byte a6)
|
||||||
{
|
{
|
||||||
var retVal = this.setGlobalBgmHook.Original(bgmKey, a2, a3, a4, a5, a6);
|
var retVal = this.setGlobalBgmHook.Original(bgmKey, a2, a3, a4, a5, a6);
|
||||||
|
|
|
||||||
|
|
@ -253,7 +253,7 @@ internal unsafe class DalamudIME : IDisposable, IServiceType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[ServiceManager.CallWhenServicesReady]
|
[ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui.dll to become available.")]
|
||||||
private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene)
|
private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu
|
||||||
this.memory = Marshal.AllocHGlobal(PartyFinderPacket.PacketSize);
|
this.memory = Marshal.AllocHGlobal(PartyFinderPacket.PacketSize);
|
||||||
|
|
||||||
this.receiveListingHook = Hook<ReceiveListingDelegate>.FromAddress(this.address.ReceiveListing, this.HandleReceiveListingDetour);
|
this.receiveListingHook = Hook<ReceiveListingDelegate>.FromAddress(this.address.ReceiveListing, this.HandleReceiveListingDetour);
|
||||||
|
this.receiveListingHook.Enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||||
|
|
@ -60,12 +61,6 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[ServiceManager.CallWhenServicesReady]
|
|
||||||
private void ContinueConstruction(GameGui gameGui)
|
|
||||||
{
|
|
||||||
this.receiveListingHook.Enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleReceiveListingDetour(IntPtr managerPtr, IntPtr data)
|
private void HandleReceiveListingDetour(IntPtr managerPtr, IntPtr data)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,10 @@ internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui
|
||||||
this.showNormalToastHook = Hook<ShowNormalToastDelegate>.FromAddress(this.address.ShowNormalToast, this.HandleNormalToastDetour);
|
this.showNormalToastHook = Hook<ShowNormalToastDelegate>.FromAddress(this.address.ShowNormalToast, this.HandleNormalToastDetour);
|
||||||
this.showQuestToastHook = Hook<ShowQuestToastDelegate>.FromAddress(this.address.ShowQuestToast, this.HandleQuestToastDetour);
|
this.showQuestToastHook = Hook<ShowQuestToastDelegate>.FromAddress(this.address.ShowQuestToast, this.HandleQuestToastDetour);
|
||||||
this.showErrorToastHook = Hook<ShowErrorToastDelegate>.FromAddress(this.address.ShowErrorToast, this.HandleErrorToastDetour);
|
this.showErrorToastHook = Hook<ShowErrorToastDelegate>.FromAddress(this.address.ShowErrorToast, this.HandleErrorToastDetour);
|
||||||
|
|
||||||
|
this.showNormalToastHook.Enable();
|
||||||
|
this.showQuestToastHook.Enable();
|
||||||
|
this.showErrorToastHook.Enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Marshal delegates
|
#region Marshal delegates
|
||||||
|
|
@ -109,14 +113,6 @@ internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui
|
||||||
return terminated;
|
return terminated;
|
||||||
}
|
}
|
||||||
|
|
||||||
[ServiceManager.CallWhenServicesReady]
|
|
||||||
private void ContinueConstruction(GameGui gameGui)
|
|
||||||
{
|
|
||||||
this.showNormalToastHook.Enable();
|
|
||||||
this.showQuestToastHook.Enable();
|
|
||||||
this.showErrorToastHook.Enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private SeString ParseString(IntPtr text)
|
private SeString ParseString(IntPtr text)
|
||||||
{
|
{
|
||||||
var bytes = new List<byte>();
|
var bytes = new List<byte>();
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,10 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType
|
||||||
this.locDalamudSettings = Loc.Localize("SystemMenuSettings", "Dalamud Settings");
|
this.locDalamudSettings = Loc.Localize("SystemMenuSettings", "Dalamud Settings");
|
||||||
|
|
||||||
// this.contextMenu.ContextMenuOpened += this.ContextMenuOnContextMenuOpened;
|
// this.contextMenu.ContextMenuOpened += this.ContextMenuOnContextMenuOpened;
|
||||||
|
|
||||||
|
this.hookAgentHudOpenSystemMenu.Enable();
|
||||||
|
this.hookUiModuleRequestMainCommand.Enable();
|
||||||
|
this.hookAtkUnitBaseReceiveGlobalEvent.Enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
private delegate void AgentHudOpenSystemMenuPrototype(void* thisPtr, AtkValue* atkValueArgs, uint menuSize);
|
private delegate void AgentHudOpenSystemMenuPrototype(void* thisPtr, AtkValue* atkValueArgs, uint menuSize);
|
||||||
|
|
@ -75,14 +79,6 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType
|
||||||
|
|
||||||
private delegate IntPtr AtkUnitBaseReceiveGlobalEvent(AtkUnitBase* thisPtr, ushort cmd, uint a3, IntPtr a4, uint* a5);
|
private delegate IntPtr AtkUnitBaseReceiveGlobalEvent(AtkUnitBase* thisPtr, ushort cmd, uint a3, IntPtr a4, uint* a5);
|
||||||
|
|
||||||
[ServiceManager.CallWhenServicesReady]
|
|
||||||
private void ContinueConstruction(DalamudInterface dalamudInterface)
|
|
||||||
{
|
|
||||||
this.hookAgentHudOpenSystemMenu.Enable();
|
|
||||||
this.hookUiModuleRequestMainCommand.Enable();
|
|
||||||
this.hookAtkUnitBaseReceiveGlobalEvent.Enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
private void ContextMenuOnContextMenuOpened(ContextMenuOpenedArgs args)
|
private void ContextMenuOnContextMenuOpened(ContextMenuOpenedArgs args)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
547
Dalamud/Game/Inventory/GameInventory.cs
Normal file
547
Dalamud/Game/Inventory/GameInventory.cs
Normal file
|
|
@ -0,0 +1,547 @@
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
using Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||||
|
using Dalamud.Hooking;
|
||||||
|
using Dalamud.IoC;
|
||||||
|
using Dalamud.IoC.Internal;
|
||||||
|
using Dalamud.Logging.Internal;
|
||||||
|
using Dalamud.Plugin.Internal;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
|
|
||||||
|
namespace Dalamud.Game.Inventory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This class provides events for the players in-game inventory.
|
||||||
|
/// </summary>
|
||||||
|
[InterfaceVersion("1.0")]
|
||||||
|
[ServiceManager.BlockingEarlyLoadedService]
|
||||||
|
internal class GameInventory : IDisposable, IServiceType
|
||||||
|
{
|
||||||
|
private readonly List<GameInventoryPluginScoped> subscribersPendingChange = new();
|
||||||
|
private readonly List<GameInventoryPluginScoped> subscribers = new();
|
||||||
|
|
||||||
|
private readonly List<InventoryItemAddedArgs> addedEvents = new();
|
||||||
|
private readonly List<InventoryItemRemovedArgs> removedEvents = new();
|
||||||
|
private readonly List<InventoryItemChangedArgs> changedEvents = new();
|
||||||
|
private readonly List<InventoryItemMovedArgs> movedEvents = new();
|
||||||
|
private readonly List<InventoryItemSplitArgs> splitEvents = new();
|
||||||
|
private readonly List<InventoryItemMergedArgs> mergedEvents = new();
|
||||||
|
|
||||||
|
[ServiceManager.ServiceDependency]
|
||||||
|
private readonly Framework framework = Service<Framework>.Get();
|
||||||
|
|
||||||
|
private readonly Hook<RaptureAtkModuleUpdateDelegate> raptureAtkModuleUpdateHook;
|
||||||
|
|
||||||
|
private readonly GameInventoryType[] inventoryTypes;
|
||||||
|
private readonly GameInventoryItem[]?[] inventoryItems;
|
||||||
|
|
||||||
|
private bool subscribersChanged;
|
||||||
|
private bool inventoriesMightBeChanged;
|
||||||
|
|
||||||
|
[ServiceManager.ServiceConstructor]
|
||||||
|
private GameInventory()
|
||||||
|
{
|
||||||
|
this.inventoryTypes = Enum.GetValues<GameInventoryType>();
|
||||||
|
this.inventoryItems = new GameInventoryItem[this.inventoryTypes.Length][];
|
||||||
|
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
this.raptureAtkModuleUpdateHook = Hook<RaptureAtkModuleUpdateDelegate>.FromFunctionPointerVariable(
|
||||||
|
new(&((RaptureAtkModule.RaptureAtkModuleVTable*)RaptureAtkModule.StaticAddressPointers.VTable)->Update),
|
||||||
|
this.RaptureAtkModuleUpdateDetour);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.raptureAtkModuleUpdateHook.Enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe delegate void RaptureAtkModuleUpdateDelegate(RaptureAtkModule* ram, float f1);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (this.subscribersPendingChange)
|
||||||
|
{
|
||||||
|
this.subscribers.Clear();
|
||||||
|
this.subscribersPendingChange.Clear();
|
||||||
|
this.subscribersChanged = false;
|
||||||
|
this.framework.Update -= this.OnFrameworkUpdate;
|
||||||
|
this.raptureAtkModuleUpdateHook.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subscribe to events.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="s">The event target.</param>
|
||||||
|
public void Subscribe(GameInventoryPluginScoped s)
|
||||||
|
{
|
||||||
|
lock (this.subscribersPendingChange)
|
||||||
|
{
|
||||||
|
this.subscribersPendingChange.Add(s);
|
||||||
|
this.subscribersChanged = true;
|
||||||
|
if (this.subscribersPendingChange.Count == 1)
|
||||||
|
{
|
||||||
|
this.inventoriesMightBeChanged = true;
|
||||||
|
this.framework.Update += this.OnFrameworkUpdate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unsubscribe from events.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="s">The event target.</param>
|
||||||
|
public void Unsubscribe(GameInventoryPluginScoped s)
|
||||||
|
{
|
||||||
|
lock (this.subscribersPendingChange)
|
||||||
|
{
|
||||||
|
if (!this.subscribersPendingChange.Remove(s))
|
||||||
|
return;
|
||||||
|
this.subscribersChanged = true;
|
||||||
|
if (this.subscribersPendingChange.Count == 0)
|
||||||
|
this.framework.Update -= this.OnFrameworkUpdate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFrameworkUpdate(IFramework framework1)
|
||||||
|
{
|
||||||
|
if (!this.inventoriesMightBeChanged)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.inventoriesMightBeChanged = false;
|
||||||
|
|
||||||
|
for (var i = 0; i < this.inventoryTypes.Length; i++)
|
||||||
|
{
|
||||||
|
var newItems = GameInventoryItem.GetReadOnlySpanOfInventory(this.inventoryTypes[i]);
|
||||||
|
if (newItems.IsEmpty)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Assumption: newItems is sorted by slots, and the last item has the highest slot number.
|
||||||
|
var oldItems = this.inventoryItems[i] ??= new GameInventoryItem[newItems[^1].InternalItem.Slot + 1];
|
||||||
|
|
||||||
|
foreach (ref readonly var newItem in newItems)
|
||||||
|
{
|
||||||
|
ref var oldItem = ref oldItems[newItem.InternalItem.Slot];
|
||||||
|
|
||||||
|
if (oldItem.IsEmpty)
|
||||||
|
{
|
||||||
|
if (!newItem.IsEmpty)
|
||||||
|
{
|
||||||
|
this.addedEvents.Add(new(newItem));
|
||||||
|
oldItem = newItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (newItem.IsEmpty)
|
||||||
|
{
|
||||||
|
this.removedEvents.Add(new(oldItem));
|
||||||
|
oldItem = newItem;
|
||||||
|
}
|
||||||
|
else if (!oldItem.Equals(newItem))
|
||||||
|
{
|
||||||
|
this.changedEvents.Add(new(oldItem, newItem));
|
||||||
|
oldItem = newItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Was there any change? If not, stop further processing.
|
||||||
|
// Note that only these three are checked; the rest will be populated after this check.
|
||||||
|
if (this.addedEvents.Count == 0 && this.removedEvents.Count == 0 && this.changedEvents.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Make a copy of subscribers, to accommodate self removal during the loop.
|
||||||
|
if (this.subscribersChanged)
|
||||||
|
{
|
||||||
|
bool isNew;
|
||||||
|
lock (this.subscribersPendingChange)
|
||||||
|
{
|
||||||
|
isNew = this.subscribersPendingChange.Any() && !this.subscribers.Any();
|
||||||
|
this.subscribers.Clear();
|
||||||
|
this.subscribers.AddRange(this.subscribersPendingChange);
|
||||||
|
this.subscribersChanged = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this the first time (resuming) scanning for changes? Then discard the "changes".
|
||||||
|
if (isNew)
|
||||||
|
{
|
||||||
|
this.addedEvents.Clear();
|
||||||
|
this.removedEvents.Clear();
|
||||||
|
this.changedEvents.Clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast InventoryChangedRaw.
|
||||||
|
// Same reason with the above on why are there 3 lists of events involved.
|
||||||
|
var allRawEventsCollection = new DeferredReadOnlyCollection<InventoryEventArgs>(
|
||||||
|
this.addedEvents.Count +
|
||||||
|
this.removedEvents.Count +
|
||||||
|
this.changedEvents.Count,
|
||||||
|
() => Array.Empty<InventoryEventArgs>()
|
||||||
|
.Concat(this.addedEvents)
|
||||||
|
.Concat(this.removedEvents)
|
||||||
|
.Concat(this.changedEvents));
|
||||||
|
foreach (var s in this.subscribers)
|
||||||
|
s.InvokeChangedRaw(allRawEventsCollection);
|
||||||
|
|
||||||
|
// Resolve moved items, from 1 added + 1 removed event.
|
||||||
|
for (var iAdded = this.addedEvents.Count - 1; iAdded >= 0; --iAdded)
|
||||||
|
{
|
||||||
|
var added = this.addedEvents[iAdded];
|
||||||
|
for (var iRemoved = this.removedEvents.Count - 1; iRemoved >= 0; --iRemoved)
|
||||||
|
{
|
||||||
|
var removed = this.removedEvents[iRemoved];
|
||||||
|
if (added.Item.ItemId != removed.Item.ItemId)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
this.movedEvents.Add(new(removed, added));
|
||||||
|
|
||||||
|
// Remove the reinterpreted entries.
|
||||||
|
this.addedEvents.RemoveAt(iAdded);
|
||||||
|
this.removedEvents.RemoveAt(iRemoved);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve moved items, from 2 changed events.
|
||||||
|
for (var i = this.changedEvents.Count - 1; i >= 0; --i)
|
||||||
|
{
|
||||||
|
var e1 = this.changedEvents[i];
|
||||||
|
for (var j = i - 1; j >= 0; --j)
|
||||||
|
{
|
||||||
|
var e2 = this.changedEvents[j];
|
||||||
|
if (e1.Item.ItemId != e2.OldItemState.ItemId || e1.OldItemState.ItemId != e2.Item.ItemId)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Move happened, and e2 has an item.
|
||||||
|
if (!e2.Item.IsEmpty)
|
||||||
|
this.movedEvents.Add(new(e1, e2));
|
||||||
|
|
||||||
|
// Move happened, and e1 has an item.
|
||||||
|
if (!e1.Item.IsEmpty)
|
||||||
|
this.movedEvents.Add(new(e2, e1));
|
||||||
|
|
||||||
|
// Remove the reinterpreted entries. Note that i > j.
|
||||||
|
this.changedEvents.RemoveAt(i);
|
||||||
|
this.changedEvents.RemoveAt(j);
|
||||||
|
|
||||||
|
// We've removed two. Adjust the outer counter.
|
||||||
|
--i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve split items, from 1 added + 1 changed event.
|
||||||
|
for (var iAdded = this.addedEvents.Count - 1; iAdded >= 0; --iAdded)
|
||||||
|
{
|
||||||
|
var added = this.addedEvents[iAdded];
|
||||||
|
for (var iChanged = this.changedEvents.Count - 1; iChanged >= 0; --iChanged)
|
||||||
|
{
|
||||||
|
var changed = this.changedEvents[iChanged];
|
||||||
|
if (added.Item.ItemId != changed.Item.ItemId || added.Item.ItemId != changed.OldItemState.ItemId)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
this.splitEvents.Add(new(changed, added));
|
||||||
|
|
||||||
|
// Remove the reinterpreted entries.
|
||||||
|
this.addedEvents.RemoveAt(iAdded);
|
||||||
|
this.changedEvents.RemoveAt(iChanged);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve merged items, from 1 removed + 1 changed event.
|
||||||
|
for (var iRemoved = this.removedEvents.Count - 1; iRemoved >= 0; --iRemoved)
|
||||||
|
{
|
||||||
|
var removed = this.removedEvents[iRemoved];
|
||||||
|
for (var iChanged = this.changedEvents.Count - 1; iChanged >= 0; --iChanged)
|
||||||
|
{
|
||||||
|
var changed = this.changedEvents[iChanged];
|
||||||
|
if (removed.Item.ItemId != changed.Item.ItemId || removed.Item.ItemId != changed.OldItemState.ItemId)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
this.mergedEvents.Add(new(removed, changed));
|
||||||
|
|
||||||
|
// Remove the reinterpreted entries.
|
||||||
|
this.removedEvents.RemoveAt(iRemoved);
|
||||||
|
this.changedEvents.RemoveAt(iChanged);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a collection view of all events.
|
||||||
|
var allEventsCollection = new DeferredReadOnlyCollection<InventoryEventArgs>(
|
||||||
|
this.addedEvents.Count +
|
||||||
|
this.removedEvents.Count +
|
||||||
|
this.changedEvents.Count +
|
||||||
|
this.movedEvents.Count +
|
||||||
|
this.splitEvents.Count +
|
||||||
|
this.mergedEvents.Count,
|
||||||
|
() => Array.Empty<InventoryEventArgs>()
|
||||||
|
.Concat(this.addedEvents)
|
||||||
|
.Concat(this.removedEvents)
|
||||||
|
.Concat(this.changedEvents)
|
||||||
|
.Concat(this.movedEvents)
|
||||||
|
.Concat(this.splitEvents)
|
||||||
|
.Concat(this.mergedEvents));
|
||||||
|
|
||||||
|
// Broadcast the rest.
|
||||||
|
foreach (var s in this.subscribers)
|
||||||
|
{
|
||||||
|
s.InvokeChanged(allEventsCollection);
|
||||||
|
s.Invoke(this.addedEvents);
|
||||||
|
s.Invoke(this.removedEvents);
|
||||||
|
s.Invoke(this.changedEvents);
|
||||||
|
s.Invoke(this.movedEvents);
|
||||||
|
s.Invoke(this.splitEvents);
|
||||||
|
s.Invoke(this.mergedEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're done using the lists. Clean them up.
|
||||||
|
this.addedEvents.Clear();
|
||||||
|
this.removedEvents.Clear();
|
||||||
|
this.changedEvents.Clear();
|
||||||
|
this.movedEvents.Clear();
|
||||||
|
this.splitEvents.Clear();
|
||||||
|
this.mergedEvents.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void RaptureAtkModuleUpdateDetour(RaptureAtkModule* ram, float f1)
|
||||||
|
{
|
||||||
|
this.inventoriesMightBeChanged |= ram->AgentUpdateFlag != 0;
|
||||||
|
this.raptureAtkModuleUpdateHook.Original(ram, f1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="IReadOnlyCollection{T}"/> view of <see cref="IEnumerable{T}"/>, so that the number of items
|
||||||
|
/// contained within can be known in advance, and it can be enumerated multiple times.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of elements being enumerated.</typeparam>
|
||||||
|
private class DeferredReadOnlyCollection<T> : IReadOnlyCollection<T>
|
||||||
|
{
|
||||||
|
private readonly Func<IEnumerable<T>> enumerableGenerator;
|
||||||
|
|
||||||
|
public DeferredReadOnlyCollection(int count, Func<IEnumerable<T>> enumerableGenerator)
|
||||||
|
{
|
||||||
|
this.enumerableGenerator = enumerableGenerator;
|
||||||
|
this.Count = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count { get; }
|
||||||
|
|
||||||
|
public IEnumerator<T> GetEnumerator() => this.enumerableGenerator().GetEnumerator();
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => this.enumerableGenerator().GetEnumerator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Plugin-scoped version of a GameInventory service.
|
||||||
|
/// </summary>
|
||||||
|
[PluginInterface]
|
||||||
|
[InterfaceVersion("1.0")]
|
||||||
|
[ServiceManager.ScopedService]
|
||||||
|
#pragma warning disable SA1015
|
||||||
|
[ResolveVia<IGameInventory>]
|
||||||
|
#pragma warning restore SA1015
|
||||||
|
internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInventory
|
||||||
|
{
|
||||||
|
private static readonly ModuleLog Log = new(nameof(GameInventoryPluginScoped));
|
||||||
|
|
||||||
|
[ServiceManager.ServiceDependency]
|
||||||
|
private readonly GameInventory gameInventoryService = Service<GameInventory>.Get();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="GameInventoryPluginScoped"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public GameInventoryPluginScoped() => this.gameInventoryService.Subscribe(this);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event IGameInventory.InventoryChangelogDelegate? InventoryChanged;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event IGameInventory.InventoryChangelogDelegate? InventoryChangedRaw;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event IGameInventory.InventoryChangedDelegate? ItemAdded;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event IGameInventory.InventoryChangedDelegate? ItemRemoved;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event IGameInventory.InventoryChangedDelegate? ItemChanged;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event IGameInventory.InventoryChangedDelegate? ItemMoved;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event IGameInventory.InventoryChangedDelegate? ItemSplit;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event IGameInventory.InventoryChangedDelegate? ItemMerged;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event IGameInventory.InventoryChangedDelegate<InventoryItemAddedArgs>? ItemAddedExplicit;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event IGameInventory.InventoryChangedDelegate<InventoryItemRemovedArgs>? ItemRemovedExplicit;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event IGameInventory.InventoryChangedDelegate<InventoryItemChangedArgs>? ItemChangedExplicit;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event IGameInventory.InventoryChangedDelegate<InventoryItemMovedArgs>? ItemMovedExplicit;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event IGameInventory.InventoryChangedDelegate<InventoryItemSplitArgs>? ItemSplitExplicit;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event IGameInventory.InventoryChangedDelegate<InventoryItemMergedArgs>? ItemMergedExplicit;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
this.gameInventoryService.Unsubscribe(this);
|
||||||
|
|
||||||
|
this.InventoryChanged = null;
|
||||||
|
this.InventoryChangedRaw = null;
|
||||||
|
this.ItemAdded = null;
|
||||||
|
this.ItemRemoved = null;
|
||||||
|
this.ItemChanged = null;
|
||||||
|
this.ItemMoved = null;
|
||||||
|
this.ItemSplit = null;
|
||||||
|
this.ItemMerged = null;
|
||||||
|
this.ItemAddedExplicit = null;
|
||||||
|
this.ItemRemovedExplicit = null;
|
||||||
|
this.ItemChangedExplicit = null;
|
||||||
|
this.ItemMovedExplicit = null;
|
||||||
|
this.ItemSplitExplicit = null;
|
||||||
|
this.ItemMergedExplicit = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoke <see cref="InventoryChanged"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The data.</param>
|
||||||
|
internal void InvokeChanged(IReadOnlyCollection<InventoryEventArgs> data)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.InventoryChanged?.Invoke(data);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error(
|
||||||
|
e,
|
||||||
|
"[{plugin}] Exception during {argType} callback",
|
||||||
|
Service<PluginManager>.GetNullable()?.FindCallingPlugin(new(e))?.Name ?? "(unknown plugin)",
|
||||||
|
nameof(this.InventoryChanged));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoke <see cref="InventoryChangedRaw"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The data.</param>
|
||||||
|
internal void InvokeChangedRaw(IReadOnlyCollection<InventoryEventArgs> data)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.InventoryChangedRaw?.Invoke(data);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error(
|
||||||
|
e,
|
||||||
|
"[{plugin}] Exception during {argType} callback",
|
||||||
|
Service<PluginManager>.GetNullable()?.FindCallingPlugin(new(e))?.Name ?? "(unknown plugin)",
|
||||||
|
nameof(this.InventoryChangedRaw));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note below: using List<T> instead of IEnumerable<T>, since List<T> has a specialized lightweight enumerator.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoke the appropriate event handler.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="events">The data.</param>
|
||||||
|
internal void Invoke(List<InventoryItemAddedArgs> events) =>
|
||||||
|
Invoke(this.ItemAdded, this.ItemAddedExplicit, events);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoke the appropriate event handler.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="events">The data.</param>
|
||||||
|
internal void Invoke(List<InventoryItemRemovedArgs> events) =>
|
||||||
|
Invoke(this.ItemRemoved, this.ItemRemovedExplicit, events);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoke the appropriate event handler.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="events">The data.</param>
|
||||||
|
internal void Invoke(List<InventoryItemChangedArgs> events) =>
|
||||||
|
Invoke(this.ItemChanged, this.ItemChangedExplicit, events);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoke the appropriate event handler.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="events">The data.</param>
|
||||||
|
internal void Invoke(List<InventoryItemMovedArgs> events) =>
|
||||||
|
Invoke(this.ItemMoved, this.ItemMovedExplicit, events);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoke the appropriate event handler.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="events">The data.</param>
|
||||||
|
internal void Invoke(List<InventoryItemSplitArgs> events) =>
|
||||||
|
Invoke(this.ItemSplit, this.ItemSplitExplicit, events);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoke the appropriate event handler.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="events">The data.</param>
|
||||||
|
internal void Invoke(List<InventoryItemMergedArgs> events) =>
|
||||||
|
Invoke(this.ItemMerged, this.ItemMergedExplicit, events);
|
||||||
|
|
||||||
|
private static void Invoke<T>(
|
||||||
|
IGameInventory.InventoryChangedDelegate? cb,
|
||||||
|
IGameInventory.InventoryChangedDelegate<T>? cbt,
|
||||||
|
List<T> events) where T : InventoryEventArgs
|
||||||
|
{
|
||||||
|
foreach (var evt in events)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cb?.Invoke(evt.Type, evt);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error(
|
||||||
|
e,
|
||||||
|
"[{plugin}] Exception during untyped callback for {evt}",
|
||||||
|
Service<PluginManager>.GetNullable()?.FindCallingPlugin(new(e))?.Name ?? "(unknown plugin)",
|
||||||
|
evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cbt?.Invoke(evt);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error(
|
||||||
|
e,
|
||||||
|
"[{plugin}] Exception during typed callback for {evt}",
|
||||||
|
Service<PluginManager>.GetNullable()?.FindCallingPlugin(new(e))?.Name ?? "(unknown plugin)",
|
||||||
|
evt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
Dalamud/Game/Inventory/GameInventoryEvent.cs
Normal file
43
Dalamud/Game/Inventory/GameInventoryEvent.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
namespace Dalamud.Game.Inventory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Class representing a item's changelog state.
|
||||||
|
/// </summary>
|
||||||
|
public enum GameInventoryEvent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A value indicating that there was no event.<br />
|
||||||
|
/// You should not see this value, unless you explicitly used it yourself, or APIs using this enum say otherwise.
|
||||||
|
/// </summary>
|
||||||
|
Empty = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Item was added to an inventory.
|
||||||
|
/// </summary>
|
||||||
|
Added = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Item was removed from an inventory.
|
||||||
|
/// </summary>
|
||||||
|
Removed = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Properties are changed for an item in an inventory.
|
||||||
|
/// </summary>
|
||||||
|
Changed = 3,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Item has been moved, possibly across different inventories.
|
||||||
|
/// </summary>
|
||||||
|
Moved = 4,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Item has been split into two stacks from one, possibly across different inventories.
|
||||||
|
/// </summary>
|
||||||
|
Split = 5,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Item has been merged into one stack from two, possibly across different inventories.
|
||||||
|
/// </summary>
|
||||||
|
Merged = 6,
|
||||||
|
}
|
||||||
203
Dalamud/Game/Inventory/GameInventoryItem.cs
Normal file
203
Dalamud/Game/Inventory/GameInventoryItem.cs
Normal file
|
|
@ -0,0 +1,203 @@
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
|
|
||||||
|
namespace Dalamud.Game.Inventory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dalamud wrapper around a ClientStructs InventoryItem.
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Explicit, Size = StructSizeInBytes)]
|
||||||
|
public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The actual data.
|
||||||
|
/// </summary>
|
||||||
|
[FieldOffset(0)]
|
||||||
|
internal readonly InventoryItem InternalItem;
|
||||||
|
|
||||||
|
private const int StructSizeInBytes = 0x38;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The view of the backing data, in <see cref="ulong"/>.
|
||||||
|
/// </summary>
|
||||||
|
[FieldOffset(0)]
|
||||||
|
private fixed ulong dataUInt64[StructSizeInBytes / 0x8];
|
||||||
|
|
||||||
|
static GameInventoryItem()
|
||||||
|
{
|
||||||
|
Debug.Assert(
|
||||||
|
sizeof(InventoryItem) == StructSizeInBytes,
|
||||||
|
$"Definition of {nameof(InventoryItem)} has been changed. " +
|
||||||
|
$"Update {nameof(StructSizeInBytes)} to {sizeof(InventoryItem)} to accommodate for the size change.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="GameInventoryItem"/> struct.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">Inventory item to wrap.</param>
|
||||||
|
internal GameInventoryItem(InventoryItem item) => this.InternalItem = item;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the this <see cref="GameInventoryItem"/> is empty.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsEmpty => this.InternalItem.ItemID == 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the container inventory type.
|
||||||
|
/// </summary>
|
||||||
|
public GameInventoryType ContainerType => (GameInventoryType)this.InternalItem.Container;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the inventory slot index this item is in.
|
||||||
|
/// </summary>
|
||||||
|
public uint InventorySlot => (uint)this.InternalItem.Slot;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the item id.
|
||||||
|
/// </summary>
|
||||||
|
public uint ItemId => this.InternalItem.ItemID;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the quantity of items in this item stack.
|
||||||
|
/// </summary>
|
||||||
|
public uint Quantity => this.InternalItem.Quantity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the spiritbond of this item.
|
||||||
|
/// </summary>
|
||||||
|
public uint Spiritbond => this.InternalItem.Spiritbond;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the repair condition of this item.
|
||||||
|
/// </summary>
|
||||||
|
public uint Condition => this.InternalItem.Condition;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the item is High Quality.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsHq => (this.InternalItem.Flags & InventoryItem.ItemFlags.HQ) != 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the item has a company crest applied.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsCompanyCrestApplied => (this.InternalItem.Flags & InventoryItem.ItemFlags.CompanyCrestApplied) != 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the item is a relic.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsRelic => (this.InternalItem.Flags & InventoryItem.ItemFlags.Relic) != 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the is a collectable.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsCollectable => (this.InternalItem.Flags & InventoryItem.ItemFlags.Collectable) != 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the array of materia types.
|
||||||
|
/// </summary>
|
||||||
|
public ReadOnlySpan<ushort> Materia => new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.Materia[0])), 5);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the array of materia grades.
|
||||||
|
/// </summary>
|
||||||
|
public ReadOnlySpan<ushort> MateriaGrade =>
|
||||||
|
new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.MateriaGrade[0])), 5);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the address of native inventory item in the game.<br />
|
||||||
|
/// Can be 0 if this instance of <see cref="GameInventoryItem"/> does not point to a valid set of container type and slot.<br />
|
||||||
|
/// Note that this instance of <see cref="GameInventoryItem"/> can be a snapshot; it may not necessarily match the
|
||||||
|
/// data you can query from the game using this address value.
|
||||||
|
/// </summary>
|
||||||
|
public nint Address
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var s = GetReadOnlySpanOfInventory(this.ContainerType);
|
||||||
|
if (s.IsEmpty)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
foreach (ref readonly var i in s)
|
||||||
|
{
|
||||||
|
if (i.InventorySlot == this.InventorySlot)
|
||||||
|
return (nint)Unsafe.AsPointer(ref Unsafe.AsRef(in i));
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the color used for this item.
|
||||||
|
/// </summary>
|
||||||
|
public byte Stain => this.InternalItem.Stain;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the glamour id for this item.
|
||||||
|
/// </summary>
|
||||||
|
public uint GlamourId => this.InternalItem.GlamourID;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the items crafter's content id.
|
||||||
|
/// NOTE: I'm not sure if this is a good idea to include or not in the dalamud api. Marked internal for now.
|
||||||
|
/// </summary>
|
||||||
|
internal ulong CrafterContentId => this.InternalItem.CrafterContentID;
|
||||||
|
|
||||||
|
public static bool operator ==(in GameInventoryItem l, in GameInventoryItem r) => l.Equals(r);
|
||||||
|
|
||||||
|
public static bool operator !=(in GameInventoryItem l, in GameInventoryItem r) => !l.Equals(r);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
readonly bool IEquatable<GameInventoryItem>.Equals(GameInventoryItem other) => this.Equals(other);
|
||||||
|
|
||||||
|
/// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
|
||||||
|
/// <param name="other">An object to compare with this object.</param>
|
||||||
|
/// <returns><c>true</c> if the current object is equal to the <paramref name="other" /> parameter; otherwise, <c>false</c>.</returns>
|
||||||
|
public readonly bool Equals(in GameInventoryItem other)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < StructSizeInBytes / 8; i++)
|
||||||
|
{
|
||||||
|
if (this.dataUInt64[i] != other.dataUInt64[i])
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="object.Equals(object?)" />
|
||||||
|
public override bool Equals(object obj) => obj is GameInventoryItem gii && this.Equals(gii);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="object.GetHashCode" />
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
var k = 0x5a8447b91aff51b4UL;
|
||||||
|
for (var i = 0; i < StructSizeInBytes / 8; i++)
|
||||||
|
k ^= this.dataUInt64[i];
|
||||||
|
return unchecked((int)(k ^ (k >> 32)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="object.ToString"/>
|
||||||
|
public override string ToString() =>
|
||||||
|
this.IsEmpty
|
||||||
|
? "empty"
|
||||||
|
: $"item({this.ItemId}@{this.ContainerType}#{this.InventorySlot})";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a <see cref="Span{T}"/> view of <see cref="InventoryItem"/>s, wrapped as <see cref="GameInventoryItem"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The inventory type.</param>
|
||||||
|
/// <returns>The span.</returns>
|
||||||
|
internal static ReadOnlySpan<GameInventoryItem> GetReadOnlySpanOfInventory(GameInventoryType type)
|
||||||
|
{
|
||||||
|
var inventoryManager = InventoryManager.Instance();
|
||||||
|
if (inventoryManager is null) return default;
|
||||||
|
|
||||||
|
var inventory = inventoryManager->GetInventoryContainer((InventoryType)type);
|
||||||
|
if (inventory is null) return default;
|
||||||
|
|
||||||
|
return new ReadOnlySpan<GameInventoryItem>(inventory->Items, (int)inventory->Size);
|
||||||
|
}
|
||||||
|
}
|
||||||
356
Dalamud/Game/Inventory/GameInventoryType.cs
Normal file
356
Dalamud/Game/Inventory/GameInventoryType.cs
Normal file
|
|
@ -0,0 +1,356 @@
|
||||||
|
namespace Dalamud.Game.Inventory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enum representing various player inventories.
|
||||||
|
/// </summary>
|
||||||
|
public enum GameInventoryType : ushort
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// First panel of main player inventory.
|
||||||
|
/// </summary>
|
||||||
|
Inventory1 = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Second panel of main player inventory.
|
||||||
|
/// </summary>
|
||||||
|
Inventory2 = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Third panel of main player inventory.
|
||||||
|
/// </summary>
|
||||||
|
Inventory3 = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fourth panel of main player inventory.
|
||||||
|
/// </summary>
|
||||||
|
Inventory4 = 3,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Items that are currently equipped by the player.
|
||||||
|
/// </summary>
|
||||||
|
EquippedItems = 1000,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Player currency container.
|
||||||
|
/// ie, gil, serpent seals, sacks of nuts.
|
||||||
|
/// </summary>
|
||||||
|
Currency = 2000,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Crystal container.
|
||||||
|
/// </summary>
|
||||||
|
Crystals = 2001,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mail container.
|
||||||
|
/// </summary>
|
||||||
|
Mail = 2003,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Key item container.
|
||||||
|
/// </summary>
|
||||||
|
KeyItems = 2004,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Quest item hand-in inventory.
|
||||||
|
/// </summary>
|
||||||
|
HandIn = 2005,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DamagedGear container.
|
||||||
|
/// </summary>
|
||||||
|
DamagedGear = 2007,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Examine window container.
|
||||||
|
/// </summary>
|
||||||
|
Examine = 2009,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Doman Enclave Reconstruction Reclamation Box.
|
||||||
|
/// </summary>
|
||||||
|
ReconstructionBuyback = 2013,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Armory off-hand weapon container.
|
||||||
|
/// </summary>
|
||||||
|
ArmoryOffHand = 3200,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Armory head container.
|
||||||
|
/// </summary>
|
||||||
|
ArmoryHead = 3201,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Armory body container.
|
||||||
|
/// </summary>
|
||||||
|
ArmoryBody = 3202,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Armory hand/gloves container.
|
||||||
|
/// </summary>
|
||||||
|
ArmoryHands = 3203,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Armory waist container.
|
||||||
|
/// <remarks>
|
||||||
|
/// This container should be unused as belt items were removed from the game in Shadowbringers.
|
||||||
|
/// </remarks>
|
||||||
|
/// </summary>
|
||||||
|
ArmoryWaist = 3204,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Armory legs/pants/skirt container.
|
||||||
|
/// </summary>
|
||||||
|
ArmoryLegs = 3205,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Armory feet/boots/shoes container.
|
||||||
|
/// </summary>
|
||||||
|
ArmoryFeets = 3206,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Armory earring container.
|
||||||
|
/// </summary>
|
||||||
|
ArmoryEar = 3207,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Armory necklace container.
|
||||||
|
/// </summary>
|
||||||
|
ArmoryNeck = 3208,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Armory bracelet container.
|
||||||
|
/// </summary>
|
||||||
|
ArmoryWrist = 3209,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Armory ring container.
|
||||||
|
/// </summary>
|
||||||
|
ArmoryRings = 3300,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Armory soul crystal container.
|
||||||
|
/// </summary>
|
||||||
|
ArmorySoulCrystal = 3400,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Armory main-hand weapon container.
|
||||||
|
/// </summary>
|
||||||
|
ArmoryMainHand = 3500,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// First panel of saddelbag inventory.
|
||||||
|
/// </summary>
|
||||||
|
SaddleBag1 = 4000,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Second panel of Saddlebag inventory.
|
||||||
|
/// </summary>
|
||||||
|
SaddleBag2 = 4001,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// First panel of premium saddlebag inventory.
|
||||||
|
/// </summary>
|
||||||
|
PremiumSaddleBag1 = 4100,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Second panel of premium saddlebag inventory.
|
||||||
|
/// </summary>
|
||||||
|
PremiumSaddleBag2 = 4101,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// First panel of retainer inventory.
|
||||||
|
/// </summary>
|
||||||
|
RetainerPage1 = 10000,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Second panel of retainer inventory.
|
||||||
|
/// </summary>
|
||||||
|
RetainerPage2 = 10001,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Third panel of retainer inventory.
|
||||||
|
/// </summary>
|
||||||
|
RetainerPage3 = 10002,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fourth panel of retainer inventory.
|
||||||
|
/// </summary>
|
||||||
|
RetainerPage4 = 10003,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fifth panel of retainer inventory.
|
||||||
|
/// </summary>
|
||||||
|
RetainerPage5 = 10004,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sixth panel of retainer inventory.
|
||||||
|
/// </summary>
|
||||||
|
RetainerPage6 = 10005,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seventh panel of retainer inventory.
|
||||||
|
/// </summary>
|
||||||
|
RetainerPage7 = 10006,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retainer equipment container.
|
||||||
|
/// </summary>
|
||||||
|
RetainerEquippedItems = 11000,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retainer currency container.
|
||||||
|
/// </summary>
|
||||||
|
RetainerGil = 12000,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retainer crystal container.
|
||||||
|
/// </summary>
|
||||||
|
RetainerCrystals = 12001,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retainer market item container.
|
||||||
|
/// </summary>
|
||||||
|
RetainerMarket = 12002,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// First panel of Free Company inventory.
|
||||||
|
/// </summary>
|
||||||
|
FreeCompanyPage1 = 20000,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Second panel of Free Company inventory.
|
||||||
|
/// </summary>
|
||||||
|
FreeCompanyPage2 = 20001,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Third panel of Free Company inventory.
|
||||||
|
/// </summary>
|
||||||
|
FreeCompanyPage3 = 20002,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fourth panel of Free Company inventory.
|
||||||
|
/// </summary>
|
||||||
|
FreeCompanyPage4 = 20003,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fifth panel of Free Company inventory.
|
||||||
|
/// </summary>
|
||||||
|
FreeCompanyPage5 = 20004,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Free Company currency container.
|
||||||
|
/// </summary>
|
||||||
|
FreeCompanyGil = 22000,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Free Company crystal container.
|
||||||
|
/// </summary>
|
||||||
|
FreeCompanyCrystals = 22001,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Housing exterior appearance container.
|
||||||
|
/// </summary>
|
||||||
|
HousingExteriorAppearance = 25000,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Housing exterior placed items container.
|
||||||
|
/// </summary>
|
||||||
|
HousingExteriorPlacedItems = 25001,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Housing interior appearance container.
|
||||||
|
/// </summary>
|
||||||
|
HousingInteriorAppearance = 25002,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// First panel of housing interior inventory.
|
||||||
|
/// </summary>
|
||||||
|
HousingInteriorPlacedItems1 = 25003,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Second panel of housing interior inventory.
|
||||||
|
/// </summary>
|
||||||
|
HousingInteriorPlacedItems2 = 25004,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Third panel of housing interior inventory.
|
||||||
|
/// </summary>
|
||||||
|
HousingInteriorPlacedItems3 = 25005,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fourth panel of housing interior inventory.
|
||||||
|
/// </summary>
|
||||||
|
HousingInteriorPlacedItems4 = 25006,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fifth panel of housing interior inventory.
|
||||||
|
/// </summary>
|
||||||
|
HousingInteriorPlacedItems5 = 25007,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sixth panel of housing interior inventory.
|
||||||
|
/// </summary>
|
||||||
|
HousingInteriorPlacedItems6 = 25008,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seventh panel of housing interior inventory.
|
||||||
|
/// </summary>
|
||||||
|
HousingInteriorPlacedItems7 = 25009,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Eighth panel of housing interior inventory.
|
||||||
|
/// </summary>
|
||||||
|
HousingInteriorPlacedItems8 = 25010,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Housing exterior storeroom inventory.
|
||||||
|
/// </summary>
|
||||||
|
HousingExteriorStoreroom = 27000,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// First panel of housing interior storeroom inventory.
|
||||||
|
/// </summary>
|
||||||
|
HousingInteriorStoreroom1 = 27001,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Second panel of housing interior storeroom inventory.
|
||||||
|
/// </summary>
|
||||||
|
HousingInteriorStoreroom2 = 27002,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Third panel of housing interior storeroom inventory.
|
||||||
|
/// </summary>
|
||||||
|
HousingInteriorStoreroom3 = 27003,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fourth panel of housing interior storeroom inventory.
|
||||||
|
/// </summary>
|
||||||
|
HousingInteriorStoreroom4 = 27004,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fifth panel of housing interior storeroom inventory.
|
||||||
|
/// </summary>
|
||||||
|
HousingInteriorStoreroom5 = 27005,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sixth panel of housing interior storeroom inventory.
|
||||||
|
/// </summary>
|
||||||
|
HousingInteriorStoreroom6 = 27006,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seventh panel of housing interior storeroom inventory.
|
||||||
|
/// </summary>
|
||||||
|
HousingInteriorStoreroom7 = 27007,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Eighth panel of housing interior storeroom inventory.
|
||||||
|
/// </summary>
|
||||||
|
HousingInteriorStoreroom8 = 27008,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An invalid value.
|
||||||
|
/// </summary>
|
||||||
|
Invalid = ushort.MaxValue,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
namespace Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the data associated with an item being affected across different slots, possibly in different containers.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class InventoryComplexEventArgs : InventoryEventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="InventoryComplexEventArgs"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">Type of the event.</param>
|
||||||
|
/// <param name="sourceEvent">The item at before slot.</param>
|
||||||
|
/// <param name="targetEvent">The item at after slot.</param>
|
||||||
|
internal InventoryComplexEventArgs(
|
||||||
|
GameInventoryEvent type, InventoryEventArgs sourceEvent, InventoryEventArgs targetEvent)
|
||||||
|
: base(type, targetEvent.Item)
|
||||||
|
{
|
||||||
|
this.SourceEvent = sourceEvent;
|
||||||
|
this.TargetEvent = targetEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the inventory this item was at.
|
||||||
|
/// </summary>
|
||||||
|
public GameInventoryType SourceInventory => this.SourceEvent.Item.ContainerType;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the inventory this item now is.
|
||||||
|
/// </summary>
|
||||||
|
public GameInventoryType TargetInventory => this.Item.ContainerType;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the slot this item was at.
|
||||||
|
/// </summary>
|
||||||
|
public uint SourceSlot => this.SourceEvent.Item.InventorySlot;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the slot this item now is.
|
||||||
|
/// </summary>
|
||||||
|
public uint TargetSlot => this.Item.InventorySlot;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the associated source event.
|
||||||
|
/// </summary>
|
||||||
|
public InventoryEventArgs SourceEvent { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the associated target event.
|
||||||
|
/// </summary>
|
||||||
|
public InventoryEventArgs TargetEvent { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override string ToString() => $"{this.Type}({this.SourceEvent}, {this.TargetEvent})";
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
namespace Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abstract base class representing inventory changed events.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class InventoryEventArgs
|
||||||
|
{
|
||||||
|
private readonly GameInventoryItem item;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="InventoryEventArgs"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">Type of the event.</param>
|
||||||
|
/// <param name="item">Item about the event.</param>
|
||||||
|
protected InventoryEventArgs(GameInventoryEvent type, in GameInventoryItem item)
|
||||||
|
{
|
||||||
|
this.Type = type;
|
||||||
|
this.item = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the type of event for these args.
|
||||||
|
/// </summary>
|
||||||
|
public GameInventoryEvent Type { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the item associated with this event.
|
||||||
|
/// <remarks><em>This is a copy of the item data.</em></remarks>
|
||||||
|
/// </summary>
|
||||||
|
// impl note: we return a ref readonly view, to avoid making copies every time this property is accessed.
|
||||||
|
// see: https://devblogs.microsoft.com/premier-developer/avoiding-struct-and-readonly-reference-performance-pitfalls-with-errorprone-net/
|
||||||
|
// "Consider using ref readonly locals and ref return for library code"
|
||||||
|
public ref readonly GameInventoryItem Item => ref this.item;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override string ToString() => $"{this.Type}({this.Item})";
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
namespace Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the data associated with an item being added to an inventory.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InventoryItemAddedArgs : InventoryEventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="InventoryItemAddedArgs"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The item.</param>
|
||||||
|
internal InventoryItemAddedArgs(in GameInventoryItem item)
|
||||||
|
: base(GameInventoryEvent.Added, item)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the inventory this item was added to.
|
||||||
|
/// </summary>
|
||||||
|
public GameInventoryType Inventory => this.Item.ContainerType;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the slot this item was added to.
|
||||||
|
/// </summary>
|
||||||
|
public uint Slot => this.Item.InventorySlot;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
namespace Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the data associated with an items properties being changed.
|
||||||
|
/// This also includes an items stack count changing.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InventoryItemChangedArgs : InventoryEventArgs
|
||||||
|
{
|
||||||
|
private readonly GameInventoryItem oldItemState;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="InventoryItemChangedArgs"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="oldItem">The item before change.</param>
|
||||||
|
/// <param name="newItem">The item after change.</param>
|
||||||
|
internal InventoryItemChangedArgs(in GameInventoryItem oldItem, in GameInventoryItem newItem)
|
||||||
|
: base(GameInventoryEvent.Changed, newItem)
|
||||||
|
{
|
||||||
|
this.oldItemState = oldItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the inventory this item is in.
|
||||||
|
/// </summary>
|
||||||
|
public GameInventoryType Inventory => this.Item.ContainerType;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the inventory slot this item is in.
|
||||||
|
/// </summary>
|
||||||
|
public uint Slot => this.Item.InventorySlot;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the state of the item from before it was changed.
|
||||||
|
/// <remarks><em>This is a copy of the item data.</em></remarks>
|
||||||
|
/// </summary>
|
||||||
|
// impl note: see InventoryEventArgs.Item.
|
||||||
|
public ref readonly GameInventoryItem OldItemState => ref this.oldItemState;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
namespace Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the data associated with an item being merged from two stacks into one.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InventoryItemMergedArgs : InventoryComplexEventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="InventoryItemMergedArgs"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sourceEvent">The item at before slot.</param>
|
||||||
|
/// <param name="targetEvent">The item at after slot.</param>
|
||||||
|
internal InventoryItemMergedArgs(InventoryEventArgs sourceEvent, InventoryEventArgs targetEvent)
|
||||||
|
: base(GameInventoryEvent.Merged, sourceEvent, targetEvent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override string ToString() =>
|
||||||
|
this.TargetEvent is InventoryItemChangedArgs iica
|
||||||
|
? $"{this.Type}(" +
|
||||||
|
$"item({this.Item.ItemId}), " +
|
||||||
|
$"{this.SourceInventory}#{this.SourceSlot}({this.SourceEvent.Item.Quantity} to 0), " +
|
||||||
|
$"{this.TargetInventory}#{this.TargetSlot}({iica.OldItemState.Quantity} to {iica.Item.Quantity}))"
|
||||||
|
: base.ToString();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
namespace Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the data associated with an item being moved from one inventory and added to another.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InventoryItemMovedArgs : InventoryComplexEventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="InventoryItemMovedArgs"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sourceEvent">The item at before slot.</param>
|
||||||
|
/// <param name="targetEvent">The item at after slot.</param>
|
||||||
|
internal InventoryItemMovedArgs(InventoryEventArgs sourceEvent, InventoryEventArgs targetEvent)
|
||||||
|
: base(GameInventoryEvent.Moved, sourceEvent, targetEvent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override string ToString() =>
|
||||||
|
$"{this.Type}(item({this.Item.ItemId}) from {this.SourceInventory}#{this.SourceSlot} to {this.TargetInventory}#{this.TargetSlot})";
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
namespace Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the data associated with an item being removed from an inventory.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InventoryItemRemovedArgs : InventoryEventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="InventoryItemRemovedArgs"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The item.</param>
|
||||||
|
internal InventoryItemRemovedArgs(in GameInventoryItem item)
|
||||||
|
: base(GameInventoryEvent.Removed, item)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the inventory this item was removed from.
|
||||||
|
/// </summary>
|
||||||
|
public GameInventoryType Inventory => this.Item.ContainerType;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the slot this item was removed from.
|
||||||
|
/// </summary>
|
||||||
|
public uint Slot => this.Item.InventorySlot;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
namespace Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the data associated with an item being split from one stack into two.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InventoryItemSplitArgs : InventoryComplexEventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="InventoryItemSplitArgs"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sourceEvent">The item at before slot.</param>
|
||||||
|
/// <param name="targetEvent">The item at after slot.</param>
|
||||||
|
internal InventoryItemSplitArgs(InventoryEventArgs sourceEvent, InventoryEventArgs targetEvent)
|
||||||
|
: base(GameInventoryEvent.Split, sourceEvent, targetEvent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override string ToString() =>
|
||||||
|
this.SourceEvent is InventoryItemChangedArgs iica
|
||||||
|
? $"{this.Type}(" +
|
||||||
|
$"item({this.Item.ItemId}), " +
|
||||||
|
$"{this.SourceInventory}#{this.SourceSlot}({iica.OldItemState.Quantity} to {iica.Item.Quantity}), " +
|
||||||
|
$"{this.TargetInventory}#{this.TargetSlot}(0 to {this.Item.Quantity}))"
|
||||||
|
: base.ToString();
|
||||||
|
}
|
||||||
|
|
@ -44,6 +44,9 @@ internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork
|
||||||
|
|
||||||
this.processZonePacketDownHook = Hook<ProcessZonePacketDownDelegate>.FromAddress(this.address.ProcessZonePacketDown, this.ProcessZonePacketDownDetour);
|
this.processZonePacketDownHook = Hook<ProcessZonePacketDownDelegate>.FromAddress(this.address.ProcessZonePacketDown, this.ProcessZonePacketDownDetour);
|
||||||
this.processZonePacketUpHook = Hook<ProcessZonePacketUpDelegate>.FromAddress(this.address.ProcessZonePacketUp, this.ProcessZonePacketUpDetour);
|
this.processZonePacketUpHook = Hook<ProcessZonePacketUpDelegate>.FromAddress(this.address.ProcessZonePacketUp, this.ProcessZonePacketUpDetour);
|
||||||
|
|
||||||
|
this.processZonePacketDownHook.Enable();
|
||||||
|
this.processZonePacketUpHook.Enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||||
|
|
@ -62,13 +65,6 @@ internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork
|
||||||
this.processZonePacketUpHook.Dispose();
|
this.processZonePacketUpHook.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
[ServiceManager.CallWhenServicesReady]
|
|
||||||
private void ContinueConstruction()
|
|
||||||
{
|
|
||||||
this.processZonePacketDownHook.Enable();
|
|
||||||
this.processZonePacketUpHook.Enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ProcessZonePacketDownDetour(IntPtr a, uint targetId, IntPtr dataPtr)
|
private void ProcessZonePacketDownDetour(IntPtr a, uint targetId, IntPtr dataPtr)
|
||||||
{
|
{
|
||||||
this.baseAddress = a;
|
this.baseAddress = a;
|
||||||
|
|
|
||||||
199
Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs
Normal file
199
Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using CheapLoc;
|
||||||
|
|
||||||
|
using Dalamud.Game.Gui.Toast;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
|
using Dalamud.Logging.Internal;
|
||||||
|
|
||||||
|
using ImGuiNET;
|
||||||
|
|
||||||
|
using TerraFX.Interop.Windows;
|
||||||
|
|
||||||
|
using static TerraFX.Interop.Windows.Windows;
|
||||||
|
|
||||||
|
namespace Dalamud.Interface.Internal;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures the ImGui clipboard behaviour to work nicely with XIV.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// XIV uses '\r' for line endings and will truncate all text after a '\n' character.
|
||||||
|
/// This means that copy/pasting multi-line text from ImGui to XIV will only copy the first line.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// ImGui uses '\n' for line endings and will ignore '\r' entirely.
|
||||||
|
/// This means that copy/pasting multi-line text from XIV to ImGui will copy all the text
|
||||||
|
/// without line breaks.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// To fix this we normalize all clipboard line endings entering/exiting ImGui to '\r\n' which
|
||||||
|
/// works for both ImGui and XIV.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
[ServiceManager.EarlyLoadedService]
|
||||||
|
internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDisposable
|
||||||
|
{
|
||||||
|
private static readonly ModuleLog Log = new(nameof(ImGuiClipboardFunctionProvider));
|
||||||
|
private readonly nint clipboardUserDataOriginal;
|
||||||
|
private readonly nint setTextOriginal;
|
||||||
|
private readonly nint getTextOriginal;
|
||||||
|
|
||||||
|
[ServiceManager.ServiceDependency]
|
||||||
|
private readonly ToastGui toastGui = Service<ToastGui>.Get();
|
||||||
|
|
||||||
|
private ImVectorWrapper<byte> clipboardData;
|
||||||
|
private GCHandle clipboardUserData;
|
||||||
|
|
||||||
|
[ServiceManager.ServiceConstructor]
|
||||||
|
private ImGuiClipboardFunctionProvider(InterfaceManager.InterfaceManagerWithScene imws)
|
||||||
|
{
|
||||||
|
// Effectively waiting for ImGui to become available.
|
||||||
|
_ = imws;
|
||||||
|
Debug.Assert(ImGuiHelpers.IsImGuiInitialized, "IMWS initialized but IsImGuiInitialized is false?");
|
||||||
|
|
||||||
|
var io = ImGui.GetIO();
|
||||||
|
this.clipboardUserDataOriginal = io.ClipboardUserData;
|
||||||
|
this.setTextOriginal = io.SetClipboardTextFn;
|
||||||
|
this.getTextOriginal = io.GetClipboardTextFn;
|
||||||
|
io.ClipboardUserData = GCHandle.ToIntPtr(this.clipboardUserData = GCHandle.Alloc(this));
|
||||||
|
io.SetClipboardTextFn = (nint)(delegate* unmanaged<nint, byte*, void>)&StaticSetClipboardTextImpl;
|
||||||
|
io.GetClipboardTextFn = (nint)(delegate* unmanaged<nint, byte*>)&StaticGetClipboardTextImpl;
|
||||||
|
|
||||||
|
this.clipboardData = new(0);
|
||||||
|
return;
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly]
|
||||||
|
static void StaticSetClipboardTextImpl(nint userData, byte* text) =>
|
||||||
|
((ImGuiClipboardFunctionProvider)GCHandle.FromIntPtr(userData).Target)!.SetClipboardTextImpl(text);
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly]
|
||||||
|
static byte* StaticGetClipboardTextImpl(nint userData) =>
|
||||||
|
((ImGuiClipboardFunctionProvider)GCHandle.FromIntPtr(userData).Target)!.GetClipboardTextImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!this.clipboardUserData.IsAllocated)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var io = ImGui.GetIO();
|
||||||
|
io.SetClipboardTextFn = this.setTextOriginal;
|
||||||
|
io.GetClipboardTextFn = this.getTextOriginal;
|
||||||
|
io.ClipboardUserData = this.clipboardUserDataOriginal;
|
||||||
|
|
||||||
|
this.clipboardUserData.Free();
|
||||||
|
this.clipboardData.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool OpenClipboardOrShowError()
|
||||||
|
{
|
||||||
|
if (!OpenClipboard(default))
|
||||||
|
{
|
||||||
|
this.toastGui.ShowError(
|
||||||
|
Loc.Localize(
|
||||||
|
"ImGuiClipboardFunctionProviderClipboardInUse",
|
||||||
|
"Some other application is using the clipboard. Try again later."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetClipboardTextImpl(byte* text)
|
||||||
|
{
|
||||||
|
if (!this.OpenClipboardOrShowError())
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var len = 0;
|
||||||
|
while (text[len] != 0)
|
||||||
|
len++;
|
||||||
|
var str = Encoding.UTF8.GetString(text, len);
|
||||||
|
str = str.ReplaceLineEndings("\r\n");
|
||||||
|
var hMem = GlobalAlloc(GMEM.GMEM_MOVEABLE, (nuint)((str.Length + 1) * 2));
|
||||||
|
if (hMem == 0)
|
||||||
|
throw new OutOfMemoryException();
|
||||||
|
|
||||||
|
var ptr = (char*)GlobalLock(hMem);
|
||||||
|
if (ptr == null)
|
||||||
|
{
|
||||||
|
throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error())
|
||||||
|
?? throw new InvalidOperationException($"{nameof(GlobalLock)} failed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
str.AsSpan().CopyTo(new(ptr, str.Length));
|
||||||
|
ptr[str.Length] = default;
|
||||||
|
GlobalUnlock(hMem);
|
||||||
|
|
||||||
|
SetClipboardData(CF.CF_UNICODETEXT, hMem);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error(e, $"Error in {nameof(this.SetClipboardTextImpl)}");
|
||||||
|
this.toastGui.ShowError(
|
||||||
|
Loc.Localize(
|
||||||
|
"ImGuiClipboardFunctionProviderErrorCopy",
|
||||||
|
"Failed to copy. See logs for details."));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseClipboard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte* GetClipboardTextImpl()
|
||||||
|
{
|
||||||
|
this.clipboardData.Clear();
|
||||||
|
|
||||||
|
var formats = stackalloc uint[] { CF.CF_UNICODETEXT, CF.CF_TEXT };
|
||||||
|
if (GetPriorityClipboardFormat(formats, 2) < 1 || !this.OpenClipboardOrShowError())
|
||||||
|
{
|
||||||
|
this.clipboardData.Add(0);
|
||||||
|
return this.clipboardData.Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var hMem = (HGLOBAL)GetClipboardData(CF.CF_UNICODETEXT);
|
||||||
|
if (hMem != default)
|
||||||
|
{
|
||||||
|
var ptr = (char*)GlobalLock(hMem);
|
||||||
|
if (ptr == null)
|
||||||
|
{
|
||||||
|
throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error())
|
||||||
|
?? throw new InvalidOperationException($"{nameof(GlobalLock)} failed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var str = new string(ptr);
|
||||||
|
str = str.ReplaceLineEndings("\r\n");
|
||||||
|
this.clipboardData.Resize(Encoding.UTF8.GetByteCount(str) + 1);
|
||||||
|
Encoding.UTF8.GetBytes(str, this.clipboardData.DataSpan);
|
||||||
|
this.clipboardData[^1] = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.clipboardData.Add(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error(e, $"Error in {nameof(this.GetClipboardTextImpl)}");
|
||||||
|
this.toastGui.ShowError(
|
||||||
|
Loc.Localize(
|
||||||
|
"ImGuiClipboardFunctionProviderErrorPaste",
|
||||||
|
"Failed to paste. See logs for details."));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseClipboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.clipboardData.Data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1063,14 +1063,10 @@ internal class InterfaceManager : IDisposable, IServiceType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[ServiceManager.CallWhenServicesReady]
|
[ServiceManager.CallWhenServicesReady(
|
||||||
private void ContinueConstruction(
|
"InterfaceManager accepts event registration and stuff even when the game window is not ready.")]
|
||||||
TargetSigScanner sigScanner,
|
private void ContinueConstruction(TargetSigScanner sigScanner, DalamudConfiguration configuration)
|
||||||
DalamudAssetManager dalamudAssetManager,
|
|
||||||
DalamudConfiguration configuration)
|
|
||||||
{
|
{
|
||||||
dalamudAssetManager.WaitForAllRequiredAssets().Wait();
|
|
||||||
|
|
||||||
this.address.Setup(sigScanner);
|
this.address.Setup(sigScanner);
|
||||||
this.framework.RunOnFrameworkThread(() =>
|
this.framework.RunOnFrameworkThread(() =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -679,6 +679,9 @@ internal class ConsoleWindow : Window, IDisposable
|
||||||
|
|
||||||
private bool IsFilterApplicable(LogEntry entry)
|
private bool IsFilterApplicable(LogEntry entry)
|
||||||
{
|
{
|
||||||
|
if (this.regexError)
|
||||||
|
return false;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// If this entry is below a newly set minimum level, fail it
|
// If this entry is below a newly set minimum level, fail it
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ internal class DataWindow : Window
|
||||||
new FateTableWidget(),
|
new FateTableWidget(),
|
||||||
new FlyTextWidget(),
|
new FlyTextWidget(),
|
||||||
new FontAwesomeTestWidget(),
|
new FontAwesomeTestWidget(),
|
||||||
|
new GameInventoryTestWidget(),
|
||||||
new GamepadWidget(),
|
new GamepadWidget(),
|
||||||
new GaugeWidget(),
|
new GaugeWidget(),
|
||||||
new HookWidget(),
|
new HookWidget(),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
using Dalamud.Configuration.Internal;
|
||||||
|
using Dalamud.Game.Inventory;
|
||||||
|
using Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||||
|
using Dalamud.Interface.Colors;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using Dalamud.Logging.Internal;
|
||||||
|
|
||||||
|
using ImGuiNET;
|
||||||
|
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
namespace Dalamud.Interface.Internal.Windows.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tester for <see cref="GameInventory"/>.
|
||||||
|
/// </summary>
|
||||||
|
internal class GameInventoryTestWidget : IDataWindowWidget
|
||||||
|
{
|
||||||
|
private static readonly ModuleLog Log = new(nameof(GameInventoryTestWidget));
|
||||||
|
|
||||||
|
private GameInventoryPluginScoped? scoped;
|
||||||
|
private bool standardEnabled;
|
||||||
|
private bool rawEnabled;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string[]? CommandShortcuts { get; init; } = { "gameinventorytest" };
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string DisplayName { get; init; } = "GameInventory Test";
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool Ready { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Load() => this.Ready = true;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Draw()
|
||||||
|
{
|
||||||
|
if (Service<DalamudConfiguration>.Get().LogLevel > LogEventLevel.Information)
|
||||||
|
{
|
||||||
|
ImGuiHelpers.SafeTextColoredWrapped(
|
||||||
|
ImGuiColors.DalamudRed,
|
||||||
|
"Enable LogLevel=Information display to see the logs.");
|
||||||
|
}
|
||||||
|
|
||||||
|
using var table = ImRaii.Table(this.DisplayName, 3, ImGuiTableFlags.SizingFixedFit);
|
||||||
|
if (!table.Success)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted("Standard Logging");
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
using (ImRaii.Disabled(this.standardEnabled))
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Enable##standard-enable") && !this.standardEnabled)
|
||||||
|
{
|
||||||
|
this.scoped ??= new();
|
||||||
|
this.scoped.InventoryChanged += ScopedOnInventoryChanged;
|
||||||
|
this.standardEnabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
using (ImRaii.Disabled(!this.standardEnabled))
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Disable##standard-disable") && this.scoped is not null && this.standardEnabled)
|
||||||
|
{
|
||||||
|
this.scoped.InventoryChanged -= ScopedOnInventoryChanged;
|
||||||
|
this.standardEnabled = false;
|
||||||
|
if (!this.rawEnabled)
|
||||||
|
{
|
||||||
|
this.scoped.Dispose();
|
||||||
|
this.scoped = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted("Raw Logging");
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
using (ImRaii.Disabled(this.rawEnabled))
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Enable##raw-enable") && !this.rawEnabled)
|
||||||
|
{
|
||||||
|
this.scoped ??= new();
|
||||||
|
this.scoped.InventoryChangedRaw += ScopedOnInventoryChangedRaw;
|
||||||
|
this.rawEnabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
using (ImRaii.Disabled(!this.rawEnabled))
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Disable##raw-disable") && this.scoped is not null && this.rawEnabled)
|
||||||
|
{
|
||||||
|
this.scoped.InventoryChangedRaw -= ScopedOnInventoryChangedRaw;
|
||||||
|
this.rawEnabled = false;
|
||||||
|
if (!this.standardEnabled)
|
||||||
|
{
|
||||||
|
this.scoped.Dispose();
|
||||||
|
this.scoped = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted("All");
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
using (ImRaii.Disabled(this.standardEnabled && this.rawEnabled))
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Enable##all-enable"))
|
||||||
|
{
|
||||||
|
this.scoped ??= new();
|
||||||
|
if (!this.standardEnabled)
|
||||||
|
this.scoped.InventoryChanged += ScopedOnInventoryChanged;
|
||||||
|
if (!this.rawEnabled)
|
||||||
|
this.scoped.InventoryChangedRaw += ScopedOnInventoryChangedRaw;
|
||||||
|
this.standardEnabled = this.rawEnabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
using (ImRaii.Disabled(this.scoped is null))
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Disable##all-disable"))
|
||||||
|
{
|
||||||
|
this.scoped?.Dispose();
|
||||||
|
this.scoped = null;
|
||||||
|
this.standardEnabled = this.rawEnabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ScopedOnInventoryChangedRaw(IReadOnlyCollection<InventoryEventArgs> events)
|
||||||
|
{
|
||||||
|
var i = 0;
|
||||||
|
foreach (var e in events)
|
||||||
|
Log.Information($"[{++i}/{events.Count}] Raw: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ScopedOnInventoryChanged(IReadOnlyCollection<InventoryEventArgs> events)
|
||||||
|
{
|
||||||
|
var i = 0;
|
||||||
|
foreach (var e in events)
|
||||||
|
{
|
||||||
|
if (e is InventoryComplexEventArgs icea)
|
||||||
|
Log.Information($"[{++i}/{events.Count}] {icea}\n\t├ {icea.SourceEvent}\n\t└ {icea.TargetEvent}");
|
||||||
|
else
|
||||||
|
Log.Information($"[{++i}/{events.Count}] {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,44 @@
|
||||||
using Dalamud.Interface.Utility;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using Dalamud.Interface.Internal.Notifications;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using Dalamud.Plugin.Ipc.Internal;
|
using Dalamud.Plugin.Ipc.Internal;
|
||||||
|
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
|
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
using Formatting = Newtonsoft.Json.Formatting;
|
||||||
|
|
||||||
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Widget for displaying plugin data share modules.
|
/// Widget for displaying plugin data share modules.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[SuppressMessage(
|
||||||
|
"StyleCop.CSharp.LayoutRules",
|
||||||
|
"SA1519:Braces should not be omitted from multi-line child statement",
|
||||||
|
Justification = "Multiple fixed blocks")]
|
||||||
internal class DataShareWidget : IDataWindowWidget
|
internal class DataShareWidget : IDataWindowWidget
|
||||||
{
|
{
|
||||||
|
private const ImGuiTabItemFlags NoCloseButton = (ImGuiTabItemFlags)(1 << 20);
|
||||||
|
|
||||||
|
private readonly List<(string Name, byte[]? Data)> dataView = new();
|
||||||
|
private int nextTab = -1;
|
||||||
|
private IReadOnlyDictionary<string, CallGateChannel>? gates;
|
||||||
|
private List<CallGateChannel>? gatesSorted;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string[]? CommandShortcuts { get; init; } = { "datashare" };
|
public string[]? CommandShortcuts { get; init; } = { "datashare" };
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string DisplayName { get; init; } = "Data Share";
|
public string DisplayName { get; init; } = "Data Share & Call Gate";
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public bool Ready { get; set; }
|
public bool Ready { get; set; }
|
||||||
|
|
@ -25,28 +50,290 @@ internal class DataShareWidget : IDataWindowWidget
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void Draw()
|
public unsafe void Draw()
|
||||||
{
|
{
|
||||||
if (!ImGui.BeginTable("###DataShareTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg))
|
using var tabbar = ImRaii.TabBar("##tabbar");
|
||||||
|
if (!tabbar.Success)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var d = true;
|
||||||
|
using (var tabitem = ImRaii.TabItem(
|
||||||
|
"Data Share##tabbar-datashare",
|
||||||
|
ref d,
|
||||||
|
NoCloseButton | (this.nextTab == 0 ? ImGuiTabItemFlags.SetSelected : 0)))
|
||||||
|
{
|
||||||
|
if (tabitem.Success)
|
||||||
|
this.DrawDataShare();
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var tabitem = ImRaii.TabItem(
|
||||||
|
"Call Gate##tabbar-callgate",
|
||||||
|
ref d,
|
||||||
|
NoCloseButton | (this.nextTab == 1 ? ImGuiTabItemFlags.SetSelected : 0)))
|
||||||
|
{
|
||||||
|
if (tabitem.Success)
|
||||||
|
this.DrawCallGate();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < this.dataView.Count; i++)
|
||||||
|
{
|
||||||
|
using var idpush = ImRaii.PushId($"##tabbar-data-{i}");
|
||||||
|
var (name, data) = this.dataView[i];
|
||||||
|
d = true;
|
||||||
|
using var tabitem = ImRaii.TabItem(
|
||||||
|
name,
|
||||||
|
ref d,
|
||||||
|
this.nextTab == 2 + i ? ImGuiTabItemFlags.SetSelected : 0);
|
||||||
|
if (!d)
|
||||||
|
this.dataView.RemoveAt(i--);
|
||||||
|
if (!tabitem.Success)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (ImGui.Button("Refresh"))
|
||||||
|
data = null;
|
||||||
|
|
||||||
|
if (data is null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dataShare = Service<DataShare>.Get();
|
||||||
|
var data2 = dataShare.GetData<object>(name);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
data = Encoding.UTF8.GetBytes(
|
||||||
|
JsonConvert.SerializeObject(
|
||||||
|
data2,
|
||||||
|
Formatting.Indented,
|
||||||
|
new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
dataShare.RelinquishData(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
data = Encoding.UTF8.GetBytes(e.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataView[i] = (name, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button("Copy"))
|
||||||
|
{
|
||||||
|
fixed (byte* pData = data)
|
||||||
|
ImGuiNative.igSetClipboardText(pData);
|
||||||
|
}
|
||||||
|
|
||||||
|
fixed (byte* pLabel = "text"u8)
|
||||||
|
fixed (byte* pData = data)
|
||||||
|
{
|
||||||
|
ImGuiNative.igInputTextMultiline(
|
||||||
|
pLabel,
|
||||||
|
pData,
|
||||||
|
(uint)data.Length,
|
||||||
|
ImGui.GetContentRegionAvail(),
|
||||||
|
ImGuiInputTextFlags.ReadOnly,
|
||||||
|
null,
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nextTab = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReprMethod(MethodInfo? mi, bool withParams)
|
||||||
|
{
|
||||||
|
if (mi is null)
|
||||||
|
return "-";
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.Append(ReprType(mi.DeclaringType))
|
||||||
|
.Append("::")
|
||||||
|
.Append(mi.Name);
|
||||||
|
if (!withParams)
|
||||||
|
return sb.ToString();
|
||||||
|
sb.Append('(');
|
||||||
|
var parfirst = true;
|
||||||
|
foreach (var par in mi.GetParameters())
|
||||||
|
{
|
||||||
|
if (!parfirst)
|
||||||
|
sb.Append(", ");
|
||||||
|
else
|
||||||
|
parfirst = false;
|
||||||
|
sb.AppendLine()
|
||||||
|
.Append('\t')
|
||||||
|
.Append(ReprType(par.ParameterType))
|
||||||
|
.Append(' ')
|
||||||
|
.Append(par.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parfirst)
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.Append(')');
|
||||||
|
if (mi.ReturnType != typeof(void))
|
||||||
|
sb.Append(" -> ").Append(ReprType(mi.ReturnType));
|
||||||
|
return sb.ToString();
|
||||||
|
|
||||||
|
static string WithoutGeneric(string s)
|
||||||
|
{
|
||||||
|
var i = s.IndexOf('`');
|
||||||
|
return i != -1 ? s[..i] : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string ReprType(Type? t) =>
|
||||||
|
t switch
|
||||||
|
{
|
||||||
|
null => "null",
|
||||||
|
_ when t == typeof(string) => "string",
|
||||||
|
_ when t == typeof(object) => "object",
|
||||||
|
_ when t == typeof(void) => "void",
|
||||||
|
_ when t == typeof(decimal) => "decimal",
|
||||||
|
_ when t == typeof(bool) => "bool",
|
||||||
|
_ when t == typeof(double) => "double",
|
||||||
|
_ when t == typeof(float) => "float",
|
||||||
|
_ when t == typeof(char) => "char",
|
||||||
|
_ when t == typeof(ulong) => "ulong",
|
||||||
|
_ when t == typeof(long) => "long",
|
||||||
|
_ when t == typeof(uint) => "uint",
|
||||||
|
_ when t == typeof(int) => "int",
|
||||||
|
_ when t == typeof(ushort) => "ushort",
|
||||||
|
_ when t == typeof(short) => "short",
|
||||||
|
_ when t == typeof(byte) => "byte",
|
||||||
|
_ when t == typeof(sbyte) => "sbyte",
|
||||||
|
_ when t == typeof(nint) => "nint",
|
||||||
|
_ when t == typeof(nuint) => "nuint",
|
||||||
|
_ when t.IsArray && t.HasElementType => ReprType(t.GetElementType()) + "[]",
|
||||||
|
_ when t.IsPointer && t.HasElementType => ReprType(t.GetElementType()) + "*",
|
||||||
|
_ when t.IsGenericTypeDefinition =>
|
||||||
|
t.Assembly == typeof(object).Assembly
|
||||||
|
? t.Name + "<>"
|
||||||
|
: (t.FullName ?? t.Name) + "<>",
|
||||||
|
_ when t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>) =>
|
||||||
|
ReprType(t.GetGenericArguments()[0]) + "?",
|
||||||
|
_ when t.IsGenericType =>
|
||||||
|
WithoutGeneric(ReprType(t.GetGenericTypeDefinition())) +
|
||||||
|
"<" + string.Join(", ", t.GetGenericArguments().Select(ReprType)) + ">",
|
||||||
|
_ => t.Assembly == typeof(object).Assembly ? t.Name : t.FullName ?? t.Name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawTextCell(string s, Func<string>? tooltip = null, bool framepad = false)
|
||||||
|
{
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
var offset = ImGui.GetCursorScreenPos() + new Vector2(0, framepad ? ImGui.GetStyle().FramePadding.Y : 0);
|
||||||
|
if (framepad)
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGui.TextUnformatted(s);
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetNextWindowPos(offset - ImGui.GetStyle().WindowPadding);
|
||||||
|
var vp = ImGui.GetWindowViewport();
|
||||||
|
var wrx = (vp.WorkPos.X + vp.WorkSize.X) - offset.X;
|
||||||
|
ImGui.SetNextWindowSizeConstraints(Vector2.One, new(wrx, float.MaxValue));
|
||||||
|
using (ImRaii.Tooltip())
|
||||||
|
{
|
||||||
|
ImGui.PushTextWrapPos(wrx);
|
||||||
|
ImGui.TextWrapped((tooltip?.Invoke() ?? s).Replace("%", "%%"));
|
||||||
|
ImGui.PopTextWrapPos();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.IsItemClicked())
|
||||||
|
{
|
||||||
|
ImGui.SetClipboardText(tooltip?.Invoke() ?? s);
|
||||||
|
Service<NotificationManager>.Get().AddNotification(
|
||||||
|
$"Copied {ImGui.TableGetColumnName()} to clipboard.",
|
||||||
|
this.DisplayName,
|
||||||
|
NotificationType.Success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawCallGate()
|
||||||
|
{
|
||||||
|
var callGate = Service<CallGate>.Get();
|
||||||
|
if (ImGui.Button("Purge empty call gates"))
|
||||||
|
callGate.PurgeEmptyGates();
|
||||||
|
|
||||||
|
using var table = ImRaii.Table("##callgate-table", 5);
|
||||||
|
ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.DefaultSort);
|
||||||
|
ImGui.TableSetupColumn("Action");
|
||||||
|
ImGui.TableSetupColumn("Func");
|
||||||
|
ImGui.TableSetupColumn("#", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale);
|
||||||
|
ImGui.TableSetupColumn("Subscriber");
|
||||||
|
ImGui.TableHeadersRow();
|
||||||
|
|
||||||
|
var gates2 = callGate.Gates;
|
||||||
|
if (!ReferenceEquals(gates2, this.gates) || this.gatesSorted is null)
|
||||||
|
{
|
||||||
|
this.gatesSorted = (this.gates = gates2).Values.ToList();
|
||||||
|
this.gatesSorted.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var item in this.gatesSorted)
|
||||||
|
{
|
||||||
|
var subs = item.Subscriptions;
|
||||||
|
for (var i = 0; i < subs.Count || i == 0; i++)
|
||||||
|
{
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
this.DrawTextCell(item.Name);
|
||||||
|
this.DrawTextCell(
|
||||||
|
ReprMethod(item.Action?.Method, false),
|
||||||
|
() => ReprMethod(item.Action?.Method, true));
|
||||||
|
this.DrawTextCell(
|
||||||
|
ReprMethod(item.Func?.Method, false),
|
||||||
|
() => ReprMethod(item.Func?.Method, true));
|
||||||
|
if (subs.Count == 0)
|
||||||
|
{
|
||||||
|
this.DrawTextCell("0");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.DrawTextCell($"{i + 1}/{subs.Count}");
|
||||||
|
this.DrawTextCell($"{subs[i].Method.DeclaringType}::{subs[i].Method.Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawDataShare()
|
||||||
|
{
|
||||||
|
if (!ImGui.BeginTable("###DataShareTable", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ImGui.TableSetupColumn("Shared Tag");
|
ImGui.TableSetupColumn("Shared Tag");
|
||||||
|
ImGui.TableSetupColumn("Show");
|
||||||
ImGui.TableSetupColumn("Creator Assembly");
|
ImGui.TableSetupColumn("Creator Assembly");
|
||||||
ImGui.TableSetupColumn("#", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale);
|
ImGui.TableSetupColumn("#", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale);
|
||||||
ImGui.TableSetupColumn("Consumers");
|
ImGui.TableSetupColumn("Consumers");
|
||||||
ImGui.TableHeadersRow();
|
ImGui.TableHeadersRow();
|
||||||
foreach (var share in Service<DataShare>.Get().GetAllShares())
|
foreach (var share in Service<DataShare>.Get().GetAllShares())
|
||||||
{
|
{
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
this.DrawTextCell(share.Tag, null, true);
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImGui.TextUnformatted(share.Tag);
|
if (ImGui.Button($"Show##datasharetable-show-{share.Tag}"))
|
||||||
ImGui.TableNextColumn();
|
{
|
||||||
ImGui.TextUnformatted(share.CreatorAssembly);
|
var index = 0;
|
||||||
ImGui.TableNextColumn();
|
for (; index < this.dataView.Count; index++)
|
||||||
ImGui.TextUnformatted(share.Users.Length.ToString());
|
{
|
||||||
ImGui.TableNextColumn();
|
if (this.dataView[index].Name == share.Tag)
|
||||||
ImGui.TextUnformatted(string.Join(", ", share.Users));
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index == this.dataView.Count)
|
||||||
|
this.dataView.Add((share.Tag, null));
|
||||||
|
else
|
||||||
|
this.dataView[index] = (share.Tag, null);
|
||||||
|
this.nextTab = 2 + index;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.DrawTextCell(share.CreatorAssembly, null, true);
|
||||||
|
this.DrawTextCell(share.Users.Length.ToString(), null, true);
|
||||||
|
this.DrawTextCell(string.Join(", ", share.Users), null, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
using System.Linq;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
|
|
@ -13,6 +15,13 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class ServicesWidget : IDataWindowWidget
|
internal class ServicesWidget : IDataWindowWidget
|
||||||
{
|
{
|
||||||
|
private readonly Dictionary<ServiceDependencyNode, Vector4> nodeRects = new();
|
||||||
|
private readonly HashSet<Type> selectedNodes = new();
|
||||||
|
private readonly HashSet<Type> tempRelatedNodes = new();
|
||||||
|
|
||||||
|
private bool includeUnloadDependencies;
|
||||||
|
private List<List<ServiceDependencyNode>>? dependencyNodes;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string[]? CommandShortcuts { get; init; } = { "services" };
|
public string[]? CommandShortcuts { get; init; } = { "services" };
|
||||||
|
|
||||||
|
|
@ -33,27 +42,294 @@ internal class ServicesWidget : IDataWindowWidget
|
||||||
{
|
{
|
||||||
var container = Service<ServiceContainer>.Get();
|
var container = Service<ServiceContainer>.Get();
|
||||||
|
|
||||||
foreach (var instance in container.Instances)
|
if (ImGui.CollapsingHeader("Dependencies"))
|
||||||
{
|
{
|
||||||
var hasInterface = container.InterfaceToTypeMap.Values.Any(x => x == instance.Key);
|
if (ImGui.Button("Clear selection"))
|
||||||
var isPublic = instance.Key.IsPublic;
|
this.selectedNodes.Clear();
|
||||||
|
|
||||||
ImGui.BulletText($"{instance.Key.FullName} ({instance.Key.GetServiceKind()})");
|
ImGui.SameLine();
|
||||||
|
switch (this.includeUnloadDependencies)
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface))
|
|
||||||
{
|
{
|
||||||
ImGui.Text(hasInterface
|
case true when ImGui.Button("Show load-time dependencies"):
|
||||||
? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}"
|
this.includeUnloadDependencies = false;
|
||||||
: "\t => NO INTERFACE!!!");
|
this.dependencyNodes = null;
|
||||||
|
break;
|
||||||
|
case false when ImGui.Button("Show unload-time dependencies"):
|
||||||
|
this.includeUnloadDependencies = true;
|
||||||
|
this.dependencyNodes = null;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPublic)
|
this.dependencyNodes ??= ServiceDependencyNode.CreateTreeByLevel(this.includeUnloadDependencies);
|
||||||
|
var cellPad = ImGui.CalcTextSize("WW");
|
||||||
|
var margin = ImGui.CalcTextSize("W\nW\nW");
|
||||||
|
var rowHeight = cellPad.Y * 3;
|
||||||
|
var width = ImGui.GetContentRegionAvail().X;
|
||||||
|
if (ImGui.BeginChild(
|
||||||
|
"dependency-graph",
|
||||||
|
new(width, (this.dependencyNodes.Count * (rowHeight + margin.Y)) + cellPad.Y),
|
||||||
|
false,
|
||||||
|
ImGuiWindowFlags.HorizontalScrollbar))
|
||||||
{
|
{
|
||||||
using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
|
const uint rectBaseBorderColor = 0xFFFFFFFF;
|
||||||
ImGui.Text("\t => PUBLIC!!!");
|
const uint rectHoverFillColor = 0xFF404040;
|
||||||
|
const uint rectHoverRelatedFillColor = 0xFF802020;
|
||||||
|
const uint rectSelectedFillColor = 0xFF20A020;
|
||||||
|
const uint rectSelectedRelatedFillColor = 0xFF204020;
|
||||||
|
const uint lineBaseColor = 0xFF808080;
|
||||||
|
const uint lineHoverColor = 0xFFFF8080;
|
||||||
|
const uint lineHoverNotColor = 0xFF404040;
|
||||||
|
const uint lineSelectedColor = 0xFF80FF00;
|
||||||
|
const uint lineInvalidColor = 0xFFFF0000;
|
||||||
|
|
||||||
|
ServiceDependencyNode? hoveredNode = null;
|
||||||
|
|
||||||
|
var pos = ImGui.GetCursorScreenPos();
|
||||||
|
var dl = ImGui.GetWindowDrawList();
|
||||||
|
var mouse = ImGui.GetMousePos();
|
||||||
|
var maxRowWidth = 0f;
|
||||||
|
|
||||||
|
// 1. Layout
|
||||||
|
for (var level = 0; level < this.dependencyNodes.Count; level++)
|
||||||
|
{
|
||||||
|
var levelNodes = this.dependencyNodes[level];
|
||||||
|
|
||||||
|
var rowWidth = 0f;
|
||||||
|
foreach (var node in levelNodes)
|
||||||
|
rowWidth += ImGui.CalcTextSize(node.TypeName).X + cellPad.X + margin.X;
|
||||||
|
|
||||||
|
var off = cellPad / 2;
|
||||||
|
if (rowWidth < width)
|
||||||
|
off.X += ImGui.GetScrollX() + ((width - rowWidth) / 2);
|
||||||
|
else if (rowWidth - ImGui.GetScrollX() < width)
|
||||||
|
off.X += width - (rowWidth - ImGui.GetScrollX());
|
||||||
|
off.Y = (rowHeight + margin.Y) * level;
|
||||||
|
|
||||||
|
foreach (var node in levelNodes)
|
||||||
|
{
|
||||||
|
var textSize = ImGui.CalcTextSize(node.TypeName);
|
||||||
|
var cellSize = textSize + cellPad;
|
||||||
|
|
||||||
|
var rc = new Vector4(pos + off, pos.X + off.X + cellSize.X, pos.Y + off.Y + cellSize.Y);
|
||||||
|
this.nodeRects[node] = rc;
|
||||||
|
if (rc.X <= mouse.X && mouse.X < rc.Z && rc.Y <= mouse.Y && mouse.Y < rc.W)
|
||||||
|
{
|
||||||
|
hoveredNode = node;
|
||||||
|
if (ImGui.IsMouseClicked(ImGuiMouseButton.Left))
|
||||||
|
{
|
||||||
|
if (this.selectedNodes.Contains(node.Type))
|
||||||
|
this.selectedNodes.Remove(node.Type);
|
||||||
|
else
|
||||||
|
this.selectedNodes.Add(node.Type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
off.X += cellSize.X + margin.X;
|
||||||
|
}
|
||||||
|
|
||||||
|
maxRowWidth = Math.Max(maxRowWidth, rowWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Draw non-hovered lines
|
||||||
|
foreach (var levelNodes in this.dependencyNodes)
|
||||||
|
{
|
||||||
|
foreach (var node in levelNodes)
|
||||||
|
{
|
||||||
|
var rect = this.nodeRects[node];
|
||||||
|
var point1 = new Vector2((rect.X + rect.Z) / 2, rect.Y);
|
||||||
|
|
||||||
|
foreach (var parent in node.InvalidParents)
|
||||||
|
{
|
||||||
|
rect = this.nodeRects[parent];
|
||||||
|
var point2 = new Vector2((rect.X + rect.Z) / 2, rect.W);
|
||||||
|
if (node == hoveredNode || parent == hoveredNode)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
dl.AddLine(point1, point2, lineInvalidColor, 2f * ImGuiHelpers.GlobalScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var parent in node.Parents)
|
||||||
|
{
|
||||||
|
rect = this.nodeRects[parent];
|
||||||
|
var point2 = new Vector2((rect.X + rect.Z) / 2, rect.W);
|
||||||
|
if (node == hoveredNode || parent == hoveredNode)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var isSelected = this.selectedNodes.Contains(node.Type) ||
|
||||||
|
this.selectedNodes.Contains(parent.Type);
|
||||||
|
dl.AddLine(
|
||||||
|
point1,
|
||||||
|
point2,
|
||||||
|
isSelected
|
||||||
|
? lineSelectedColor
|
||||||
|
: hoveredNode is not null
|
||||||
|
? lineHoverNotColor
|
||||||
|
: lineBaseColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Draw boxes
|
||||||
|
foreach (var levelNodes in this.dependencyNodes)
|
||||||
|
{
|
||||||
|
foreach (var node in levelNodes)
|
||||||
|
{
|
||||||
|
var textSize = ImGui.CalcTextSize(node.TypeName);
|
||||||
|
var cellSize = textSize + cellPad;
|
||||||
|
|
||||||
|
var rc = this.nodeRects[node];
|
||||||
|
if (hoveredNode == node)
|
||||||
|
dl.AddRectFilled(new(rc.X, rc.Y), new(rc.Z, rc.W), rectHoverFillColor);
|
||||||
|
else if (this.selectedNodes.Contains(node.Type))
|
||||||
|
dl.AddRectFilled(new(rc.X, rc.Y), new(rc.Z, rc.W), rectSelectedFillColor);
|
||||||
|
else if (node.Relatives.Any(x => this.selectedNodes.Contains(x.Type)))
|
||||||
|
dl.AddRectFilled(new(rc.X, rc.Y), new(rc.Z, rc.W), rectSelectedRelatedFillColor);
|
||||||
|
else if (hoveredNode?.Relatives.Select(x => x.Type).Contains(node.Type) is true)
|
||||||
|
dl.AddRectFilled(new(rc.X, rc.Y), new(rc.Z, rc.W), rectHoverRelatedFillColor);
|
||||||
|
|
||||||
|
dl.AddRect(new(rc.X, rc.Y), new(rc.Z, rc.W), rectBaseBorderColor);
|
||||||
|
ImGui.SetCursorPos((new Vector2(rc.X, rc.Y) - pos) + ((cellSize - textSize) / 2));
|
||||||
|
ImGui.TextUnformatted(node.TypeName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Draw hovered lines
|
||||||
|
if (hoveredNode is not null)
|
||||||
|
{
|
||||||
|
foreach (var levelNodes in this.dependencyNodes)
|
||||||
|
{
|
||||||
|
foreach (var node in levelNodes)
|
||||||
|
{
|
||||||
|
var rect = this.nodeRects[node];
|
||||||
|
var point1 = new Vector2((rect.X + rect.Z) / 2, rect.Y);
|
||||||
|
foreach (var parent in node.Parents)
|
||||||
|
{
|
||||||
|
if (node == hoveredNode || parent == hoveredNode)
|
||||||
|
{
|
||||||
|
rect = this.nodeRects[parent];
|
||||||
|
var point2 = new Vector2((rect.X + rect.Z) / 2, rect.W);
|
||||||
|
dl.AddLine(
|
||||||
|
point1,
|
||||||
|
point2,
|
||||||
|
lineHoverColor,
|
||||||
|
2 * ImGuiHelpers.GlobalScale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SetCursorPos(default);
|
||||||
|
ImGui.Dummy(new(maxRowWidth, this.dependencyNodes.Count * rowHeight));
|
||||||
|
ImGui.EndChild();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
ImGuiHelpers.ScaledDummy(2);
|
|
||||||
|
if (ImGui.CollapsingHeader("Plugin-facing Services"))
|
||||||
|
{
|
||||||
|
foreach (var instance in container.Instances)
|
||||||
|
{
|
||||||
|
var hasInterface = container.InterfaceToTypeMap.Values.Any(x => x == instance.Key);
|
||||||
|
var isPublic = instance.Key.IsPublic;
|
||||||
|
|
||||||
|
ImGui.BulletText($"{instance.Key.FullName} ({instance.Key.GetServiceKind()})");
|
||||||
|
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface))
|
||||||
|
{
|
||||||
|
ImGui.Text(
|
||||||
|
hasInterface
|
||||||
|
? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}"
|
||||||
|
: "\t => NO INTERFACE!!!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPublic)
|
||||||
|
{
|
||||||
|
using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
|
||||||
|
ImGui.Text("\t => PUBLIC!!!");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiHelpers.ScaledDummy(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ServiceDependencyNode
|
||||||
|
{
|
||||||
|
private readonly List<ServiceDependencyNode> parents = new();
|
||||||
|
private readonly List<ServiceDependencyNode> children = new();
|
||||||
|
private readonly List<ServiceDependencyNode> invalidParents = new();
|
||||||
|
|
||||||
|
private ServiceDependencyNode(Type t) => this.Type = t;
|
||||||
|
|
||||||
|
public Type Type { get; }
|
||||||
|
|
||||||
|
public string TypeName => this.Type.Name;
|
||||||
|
|
||||||
|
public IReadOnlyList<ServiceDependencyNode> Parents => this.parents;
|
||||||
|
|
||||||
|
public IReadOnlyList<ServiceDependencyNode> Children => this.children;
|
||||||
|
|
||||||
|
public IReadOnlyList<ServiceDependencyNode> InvalidParents => this.invalidParents;
|
||||||
|
|
||||||
|
public IEnumerable<ServiceDependencyNode> Relatives =>
|
||||||
|
this.parents.Concat(this.children).Concat(this.invalidParents);
|
||||||
|
|
||||||
|
public int Level { get; private set; }
|
||||||
|
|
||||||
|
public static List<ServiceDependencyNode> CreateTree(bool includeUnloadDependencies)
|
||||||
|
{
|
||||||
|
var nodes = new Dictionary<Type, ServiceDependencyNode>();
|
||||||
|
foreach (var t in ServiceManager.GetConcreteServiceTypes())
|
||||||
|
nodes.Add(typeof(Service<>).MakeGenericType(t), new(t));
|
||||||
|
foreach (var t in ServiceManager.GetConcreteServiceTypes())
|
||||||
|
{
|
||||||
|
var st = typeof(Service<>).MakeGenericType(t);
|
||||||
|
var node = nodes[st];
|
||||||
|
foreach (var depType in ServiceHelpers.GetDependencies(st, includeUnloadDependencies))
|
||||||
|
{
|
||||||
|
var depServiceType = typeof(Service<>).MakeGenericType(depType);
|
||||||
|
var depNode = nodes[depServiceType];
|
||||||
|
if (node.IsAncestorOf(depType))
|
||||||
|
{
|
||||||
|
node.invalidParents.Add(depNode);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
depNode.UpdateNodeLevel(1);
|
||||||
|
node.UpdateNodeLevel(depNode.Level + 1);
|
||||||
|
node.parents.Add(depNode);
|
||||||
|
depNode.children.Add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes.Values.OrderBy(x => x.Level).ThenBy(x => x.Type.Name).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<List<ServiceDependencyNode>> CreateTreeByLevel(bool includeUnloadDependencies)
|
||||||
|
{
|
||||||
|
var res = new List<List<ServiceDependencyNode>>();
|
||||||
|
foreach (var n in CreateTree(includeUnloadDependencies))
|
||||||
|
{
|
||||||
|
while (res.Count <= n.Level)
|
||||||
|
res.Add(new());
|
||||||
|
res[n.Level].Add(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsAncestorOf(Type type) =>
|
||||||
|
this.children.Any(x => x.Type == type) || this.children.Any(x => x.IsAncestorOf(type));
|
||||||
|
|
||||||
|
private void UpdateNodeLevel(int newLevel)
|
||||||
|
{
|
||||||
|
if (this.Level >= newLevel)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.Level = newLevel;
|
||||||
|
foreach (var c in this.children)
|
||||||
|
c.UpdateNodeLevel(newLevel + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
using Dalamud.Data;
|
using Dalamud.Data;
|
||||||
using Dalamud.Interface.Internal;
|
using Dalamud.Interface.Internal;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
using ImGuiScene;
|
|
||||||
using Lumina.Data.Files;
|
using Lumina.Data.Files;
|
||||||
using Lumina.Data.Parsing.Uld;
|
using Lumina.Data.Parsing.Uld;
|
||||||
|
|
||||||
|
|
@ -155,20 +155,27 @@ public class UldWrapper : IDisposable
|
||||||
|
|
||||||
// Try to load HD textures first.
|
// Try to load HD textures first.
|
||||||
var hrPath = texturePath.Replace(".tex", "_hr1.tex");
|
var hrPath = texturePath.Replace(".tex", "_hr1.tex");
|
||||||
|
var substitution = Service<TextureManager>.Get();
|
||||||
|
hrPath = substitution.GetSubstitutedPath(hrPath);
|
||||||
var hd = true;
|
var hd = true;
|
||||||
var file = this.data.GetFile<TexFile>(hrPath);
|
var tex = Path.IsPathRooted(hrPath)
|
||||||
if (file == null)
|
? this.data.GameData.GetFileFromDisk<TexFile>(hrPath)
|
||||||
|
: this.data.GetFile<TexFile>(hrPath);
|
||||||
|
if (tex == null)
|
||||||
{
|
{
|
||||||
hd = false;
|
hd = false;
|
||||||
file = this.data.GetFile<TexFile>(texturePath);
|
texturePath = substitution.GetSubstitutedPath(texturePath);
|
||||||
|
tex = Path.IsPathRooted(texturePath)
|
||||||
|
? this.data.GameData.GetFileFromDisk<TexFile>(texturePath)
|
||||||
|
: this.data.GetFile<TexFile>(texturePath);
|
||||||
|
|
||||||
// Neither texture could be loaded.
|
// Neither texture could be loaded.
|
||||||
if (file == null)
|
if (tex == null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (id, file.Header.Width, file.Header.Height, hd, file.GetRgbaImageData());
|
return (id, tex.Header.Width, tex.Header.Height, hd, tex.GetRgbaImageData());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,7 @@ public unsafe struct ImVectorWrapper<T> : IList<T>, IList, IReadOnlyList<T>, IDi
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="initialCapacity">The initial capacity.</param>
|
/// <param name="initialCapacity">The initial capacity.</param>
|
||||||
/// <param name="destroyer">The destroyer function to call on item removal.</param>
|
/// <param name="destroyer">The destroyer function to call on item removal.</param>
|
||||||
public ImVectorWrapper(int initialCapacity = 0, ImGuiNativeDestroyDelegate? destroyer = null)
|
public ImVectorWrapper(int initialCapacity, ImGuiNativeDestroyDelegate? destroyer = null)
|
||||||
{
|
{
|
||||||
if (initialCapacity < 0)
|
if (initialCapacity < 0)
|
||||||
{
|
{
|
||||||
|
|
@ -394,7 +394,7 @@ public unsafe struct ImVectorWrapper<T> : IList<T>, IList, IReadOnlyList<T>, IDi
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="List{T}.AddRange"/>
|
/// <inheritdoc cref="List{T}.AddRange"/>
|
||||||
public void AddRange(Span<T> items)
|
public void AddRange(ReadOnlySpan<T> items)
|
||||||
{
|
{
|
||||||
this.EnsureCapacityExponential(this.LengthUnsafe + items.Length);
|
this.EnsureCapacityExponential(this.LengthUnsafe + items.Length);
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
|
|
@ -466,7 +466,7 @@ public unsafe struct ImVectorWrapper<T> : IList<T>, IList, IReadOnlyList<T>, IDi
|
||||||
/// <param name="capacity">The minimum capacity to ensure.</param>
|
/// <param name="capacity">The minimum capacity to ensure.</param>
|
||||||
/// <returns>Whether the capacity has been changed.</returns>
|
/// <returns>Whether the capacity has been changed.</returns>
|
||||||
public bool EnsureCapacityExponential(int capacity)
|
public bool EnsureCapacityExponential(int capacity)
|
||||||
=> this.EnsureCapacity(1 << ((sizeof(int) * 8) - BitOperations.LeadingZeroCount((uint)this.LengthUnsafe)));
|
=> this.EnsureCapacity(1 << ((sizeof(int) * 8) - BitOperations.LeadingZeroCount((uint)capacity)));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resizes the underlying array and fills with zeroes if grown.
|
/// Resizes the underlying array and fills with zeroes if grown.
|
||||||
|
|
@ -519,10 +519,11 @@ public unsafe struct ImVectorWrapper<T> : IList<T>, IList, IReadOnlyList<T>, IDi
|
||||||
if (index < 0 || index > this.LengthUnsafe)
|
if (index < 0 || index > this.LengthUnsafe)
|
||||||
throw new IndexOutOfRangeException();
|
throw new IndexOutOfRangeException();
|
||||||
|
|
||||||
this.EnsureCapacityExponential(this.CapacityUnsafe + 1);
|
this.EnsureCapacityExponential(this.LengthUnsafe + 1);
|
||||||
var num = this.LengthUnsafe - index;
|
var num = this.LengthUnsafe - index;
|
||||||
Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + 1, num * sizeof(T), num * sizeof(T));
|
Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + 1, num * sizeof(T), num * sizeof(T));
|
||||||
this.DataUnsafe[index] = item;
|
this.DataUnsafe[index] = item;
|
||||||
|
this.LengthUnsafe += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="List{T}.InsertRange"/>
|
/// <inheritdoc cref="List{T}.InsertRange"/>
|
||||||
|
|
@ -535,6 +536,7 @@ public unsafe struct ImVectorWrapper<T> : IList<T>, IList, IReadOnlyList<T>, IDi
|
||||||
Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + count, num * sizeof(T), num * sizeof(T));
|
Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + count, num * sizeof(T), num * sizeof(T));
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
this.DataUnsafe[index++] = item;
|
this.DataUnsafe[index++] = item;
|
||||||
|
this.LengthUnsafe += count;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -543,14 +545,15 @@ public unsafe struct ImVectorWrapper<T> : IList<T>, IList, IReadOnlyList<T>, IDi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="List{T}.AddRange"/>
|
/// <inheritdoc cref="List{T}.InsertRange"/>
|
||||||
public void InsertRange(int index, Span<T> items)
|
public void InsertRange(int index, ReadOnlySpan<T> items)
|
||||||
{
|
{
|
||||||
this.EnsureCapacityExponential(this.LengthUnsafe + items.Length);
|
this.EnsureCapacityExponential(this.LengthUnsafe + items.Length);
|
||||||
var num = this.LengthUnsafe - index;
|
var num = this.LengthUnsafe - index;
|
||||||
Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + items.Length, num * sizeof(T), num * sizeof(T));
|
Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + items.Length, num * sizeof(T), num * sizeof(T));
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
this.DataUnsafe[index++] = item;
|
this.DataUnsafe[index++] = item;
|
||||||
|
this.LengthUnsafe += items.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -558,15 +561,7 @@ public unsafe struct ImVectorWrapper<T> : IList<T>, IList, IReadOnlyList<T>, IDi
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="index">The index.</param>
|
/// <param name="index">The index.</param>
|
||||||
/// <param name="skipDestroyer">Whether to skip calling the destroyer function.</param>
|
/// <param name="skipDestroyer">Whether to skip calling the destroyer function.</param>
|
||||||
public void RemoveAt(int index, bool skipDestroyer = false)
|
public void RemoveAt(int index, bool skipDestroyer = false) => this.RemoveRange(index, 1, skipDestroyer);
|
||||||
{
|
|
||||||
this.EnsureIndex(index);
|
|
||||||
var num = this.LengthUnsafe - index - 1;
|
|
||||||
if (!skipDestroyer)
|
|
||||||
this.destroyer?.Invoke(&this.DataUnsafe[index]);
|
|
||||||
|
|
||||||
Buffer.MemoryCopy(this.DataUnsafe + index + 1, this.DataUnsafe + index, num * sizeof(T), num * sizeof(T));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
void IList<T>.RemoveAt(int index) => this.RemoveAt(index);
|
void IList<T>.RemoveAt(int index) => this.RemoveAt(index);
|
||||||
|
|
@ -574,6 +569,73 @@ public unsafe struct ImVectorWrapper<T> : IList<T>, IList, IReadOnlyList<T>, IDi
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
void IList.RemoveAt(int index) => this.RemoveAt(index);
|
void IList.RemoveAt(int index) => this.RemoveAt(index);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes <paramref name="count"/> elements at the given index.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index">The index of the first item to remove.</param>
|
||||||
|
/// <param name="count">Number of items to remove.</param>
|
||||||
|
/// <param name="skipDestroyer">Whether to skip calling the destroyer function.</param>
|
||||||
|
public void RemoveRange(int index, int count, bool skipDestroyer = false)
|
||||||
|
{
|
||||||
|
this.EnsureIndex(index);
|
||||||
|
if (count < 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(count), count, "Must be positive.");
|
||||||
|
if (count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!skipDestroyer && this.destroyer is { } d)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
d(this.DataUnsafe + index + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
var numItemsToMove = this.LengthUnsafe - index - count;
|
||||||
|
var numBytesToMove = numItemsToMove * sizeof(T);
|
||||||
|
Buffer.MemoryCopy(this.DataUnsafe + index + count, this.DataUnsafe + index, numBytesToMove, numBytesToMove);
|
||||||
|
this.LengthUnsafe -= count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replaces a sequence at given offset <paramref name="index"/> of <paramref name="count"/> items with
|
||||||
|
/// <paramref name="replacement"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index">The index of the first item to be replaced.</param>
|
||||||
|
/// <param name="count">The number of items to be replaced.</param>
|
||||||
|
/// <param name="replacement">The replacement.</param>
|
||||||
|
/// <param name="skipDestroyer">Whether to skip calling the destroyer function.</param>
|
||||||
|
public void ReplaceRange(int index, int count, ReadOnlySpan<T> replacement, bool skipDestroyer = false)
|
||||||
|
{
|
||||||
|
this.EnsureIndex(index);
|
||||||
|
if (count < 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(count), count, "Must be positive.");
|
||||||
|
if (count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Ensure the capacity first, so that we can safely destroy the items first.
|
||||||
|
this.EnsureCapacityExponential((this.LengthUnsafe + replacement.Length) - count);
|
||||||
|
|
||||||
|
if (!skipDestroyer && this.destroyer is { } d)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
d(this.DataUnsafe + index + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == replacement.Length)
|
||||||
|
{
|
||||||
|
replacement.CopyTo(this.DataSpan[index..]);
|
||||||
|
}
|
||||||
|
else if (count > replacement.Length)
|
||||||
|
{
|
||||||
|
replacement.CopyTo(this.DataSpan[index..]);
|
||||||
|
this.RemoveRange(index + replacement.Length, count - replacement.Length);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
replacement[..count].CopyTo(this.DataSpan[index..]);
|
||||||
|
this.InsertRange(index + count, replacement[count..]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the capacity exactly as requested.
|
/// Sets the capacity exactly as requested.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -611,9 +673,6 @@ public unsafe struct ImVectorWrapper<T> : IList<T>, IList, IReadOnlyList<T>, IDi
|
||||||
|
|
||||||
if (!oldSpan.IsEmpty && !newSpan.IsEmpty)
|
if (!oldSpan.IsEmpty && !newSpan.IsEmpty)
|
||||||
oldSpan[..this.LengthUnsafe].CopyTo(newSpan);
|
oldSpan[..this.LengthUnsafe].CopyTo(newSpan);
|
||||||
// #if DEBUG
|
|
||||||
// new Span<byte>(newAlloc + this.LengthUnsafe, sizeof(T) * (capacity - this.LengthUnsafe)).Fill(0xCC);
|
|
||||||
// #endif
|
|
||||||
|
|
||||||
if (oldAlloc != null)
|
if (oldAlloc != null)
|
||||||
ImGuiNative.igMemFree(oldAlloc);
|
ImGuiNative.igMemFree(oldAlloc);
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,38 @@ public static unsafe class MemoryHelper
|
||||||
|
|
||||||
#region ReadString
|
#region ReadString
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compares if the given char span equals to the null-terminated string at <paramref name="memoryAddress"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="charSpan">The character span.</param>
|
||||||
|
/// <param name="memoryAddress">The address of null-terminated string.</param>
|
||||||
|
/// <param name="encoding">The encoding of the null-terminated string.</param>
|
||||||
|
/// <param name="maxLength">The maximum length of the null-terminated string.</param>
|
||||||
|
/// <returns>Whether they are equal.</returns>
|
||||||
|
public static bool EqualsZeroTerminatedString(
|
||||||
|
ReadOnlySpan<char> charSpan,
|
||||||
|
nint memoryAddress,
|
||||||
|
Encoding? encoding = null,
|
||||||
|
int maxLength = int.MaxValue)
|
||||||
|
{
|
||||||
|
encoding ??= Encoding.UTF8;
|
||||||
|
maxLength = Math.Min(maxLength, charSpan.Length + 4);
|
||||||
|
|
||||||
|
var pmem = ((byte*)memoryAddress)!;
|
||||||
|
var length = 0;
|
||||||
|
while (length < maxLength && pmem[length] != 0)
|
||||||
|
length++;
|
||||||
|
|
||||||
|
var mem = new Span<byte>(pmem, length);
|
||||||
|
var memCharCount = encoding.GetCharCount(mem);
|
||||||
|
if (memCharCount != charSpan.Length)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Span<char> chars = stackalloc char[memCharCount];
|
||||||
|
encoding.GetChars(mem, chars);
|
||||||
|
return charSpan.SequenceEqual(chars);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Read a UTF-8 encoded string from a specified memory address.
|
/// Read a UTF-8 encoded string from a specified memory address.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
using Dalamud.Interface.Internal;
|
using Dalamud.Interface.Internal;
|
||||||
using Dalamud.Interface.Internal.Windows.PluginInstaller;
|
using Dalamud.Interface.Internal.Windows.PluginInstaller;
|
||||||
|
using Dalamud.IoC;
|
||||||
using Dalamud.IoC.Internal;
|
using Dalamud.IoC.Internal;
|
||||||
using Dalamud.Logging.Internal;
|
using Dalamud.Logging.Internal;
|
||||||
using Dalamud.Networking.Http;
|
using Dalamud.Networking.Http;
|
||||||
|
|
@ -29,6 +30,7 @@ using Dalamud.Plugin.Internal.Profiles;
|
||||||
using Dalamud.Plugin.Internal.Types;
|
using Dalamud.Plugin.Internal.Types;
|
||||||
using Dalamud.Plugin.Internal.Types.Manifest;
|
using Dalamud.Plugin.Internal.Types.Manifest;
|
||||||
using Dalamud.Plugin.Ipc.Internal;
|
using Dalamud.Plugin.Ipc.Internal;
|
||||||
|
using Dalamud.Support;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
using Dalamud.Utility.Timing;
|
using Dalamud.Utility.Timing;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
@ -93,7 +95,9 @@ internal partial class PluginManager : IDisposable, IServiceType
|
||||||
}
|
}
|
||||||
|
|
||||||
[ServiceManager.ServiceConstructor]
|
[ServiceManager.ServiceConstructor]
|
||||||
private PluginManager()
|
private PluginManager(
|
||||||
|
ServiceManager.RegisterStartupBlockerDelegate registerStartupBlocker,
|
||||||
|
ServiceManager.RegisterUnloadAfterDelegate registerUnloadAfter)
|
||||||
{
|
{
|
||||||
this.pluginDirectory = new DirectoryInfo(this.dalamud.StartInfo.PluginDirectory!);
|
this.pluginDirectory = new DirectoryInfo(this.dalamud.StartInfo.PluginDirectory!);
|
||||||
|
|
||||||
|
|
@ -1204,6 +1208,49 @@ internal partial class PluginManager : IDisposable, IServiceType
|
||||||
/// <returns>The calling plugin, or null.</returns>
|
/// <returns>The calling plugin, or null.</returns>
|
||||||
public LocalPlugin? FindCallingPlugin() => this.FindCallingPlugin(new StackTrace());
|
public LocalPlugin? FindCallingPlugin() => this.FindCallingPlugin(new StackTrace());
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves the services that a plugin may have a dependency on.<br />
|
||||||
|
/// This is required, as the lifetime of a plugin cannot be longer than PluginManager,
|
||||||
|
/// and we want to ensure that dependency services to be kept alive at least until all the plugins, and thus
|
||||||
|
/// PluginManager to be gone.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The dependency services.</returns>
|
||||||
|
private static IEnumerable<Type> ResolvePossiblePluginDependencyServices()
|
||||||
|
{
|
||||||
|
foreach (var serviceType in ServiceManager.GetConcreteServiceTypes())
|
||||||
|
{
|
||||||
|
if (serviceType == typeof(PluginManager))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Scoped plugin services lifetime is tied to their scopes. They go away when LocalPlugin goes away.
|
||||||
|
// Nonetheless, their direct dependencies must be considered.
|
||||||
|
if (serviceType.GetServiceKind() == ServiceManager.ServiceKind.ScopedService)
|
||||||
|
{
|
||||||
|
var typeAsServiceT = ServiceHelpers.GetAsService(serviceType);
|
||||||
|
var dependencies = ServiceHelpers.GetDependencies(typeAsServiceT, false);
|
||||||
|
ServiceManager.Log.Verbose("Found dependencies of scoped plugin service {Type} ({Cnt})", serviceType.FullName!, dependencies!.Count);
|
||||||
|
|
||||||
|
foreach (var scopedDep in dependencies)
|
||||||
|
{
|
||||||
|
if (scopedDep == typeof(PluginManager))
|
||||||
|
throw new Exception("Scoped plugin services cannot depend on PluginManager.");
|
||||||
|
|
||||||
|
ServiceManager.Log.Verbose("PluginManager MUST depend on {Type} via {BaseType}", scopedDep.FullName!, serviceType.FullName!);
|
||||||
|
yield return scopedDep;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pluginInterfaceAttribute = serviceType.GetCustomAttribute<PluginInterfaceAttribute>(true);
|
||||||
|
if (pluginInterfaceAttribute == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ServiceManager.Log.Verbose("PluginManager MUST depend on {Type}", serviceType.FullName!);
|
||||||
|
yield return serviceType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<Stream> DownloadPluginAsync(RemotePluginManifest repoManifest, bool useTesting)
|
private async Task<Stream> DownloadPluginAsync(RemotePluginManifest repoManifest, bool useTesting)
|
||||||
{
|
{
|
||||||
var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall;
|
var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall;
|
||||||
|
|
@ -1595,6 +1642,38 @@ internal partial class PluginManager : IDisposable, IServiceType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LoadAndStartLoadSyncPlugins()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (Timings.Start("PM Load Plugin Repos"))
|
||||||
|
{
|
||||||
|
_ = this.SetPluginReposFromConfigAsync(false);
|
||||||
|
this.OnInstalledPluginsChanged += () => Task.Run(Troubleshooting.LogTroubleshooting);
|
||||||
|
|
||||||
|
Log.Information("[T3] PM repos OK!");
|
||||||
|
}
|
||||||
|
|
||||||
|
using (Timings.Start("PM Cleanup Plugins"))
|
||||||
|
{
|
||||||
|
this.CleanupPlugins();
|
||||||
|
Log.Information("[T3] PMC OK!");
|
||||||
|
}
|
||||||
|
|
||||||
|
using (Timings.Start("PM Load Sync Plugins"))
|
||||||
|
{
|
||||||
|
this.LoadAllPlugins().Wait();
|
||||||
|
Log.Information("[T3] PML OK!");
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = Task.Run(Troubleshooting.LogTroubleshooting);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "Plugin load failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static class Locs
|
private static class Locs
|
||||||
{
|
{
|
||||||
public static string DalamudPluginUpdateSuccessful(string name, Version version) => Loc.Localize("DalamudPluginUpdateSuccessful", " 》 {0} updated to v{1}.").Format(name, version);
|
public static string DalamudPluginUpdateSuccessful(string name, Version version) => Loc.Localize("DalamudPluginUpdateSuccessful", " 》 {0} updated to v{1}.").Format(name, version);
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Dalamud.Logging.Internal;
|
|
||||||
using Dalamud.Support;
|
|
||||||
using Dalamud.Utility.Timing;
|
|
||||||
|
|
||||||
namespace Dalamud.Plugin.Internal;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Class responsible for loading plugins on startup.
|
|
||||||
/// </summary>
|
|
||||||
[ServiceManager.BlockingEarlyLoadedService]
|
|
||||||
public class StartupPluginLoader : IServiceType
|
|
||||||
{
|
|
||||||
private static readonly ModuleLog Log = new("SPL");
|
|
||||||
|
|
||||||
[ServiceManager.ServiceConstructor]
|
|
||||||
private StartupPluginLoader(PluginManager pluginManager)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (Timings.Start("PM Load Plugin Repos"))
|
|
||||||
{
|
|
||||||
_ = pluginManager.SetPluginReposFromConfigAsync(false);
|
|
||||||
pluginManager.OnInstalledPluginsChanged += () => Task.Run(Troubleshooting.LogTroubleshooting);
|
|
||||||
|
|
||||||
Log.Information("[T3] PM repos OK!");
|
|
||||||
}
|
|
||||||
|
|
||||||
using (Timings.Start("PM Cleanup Plugins"))
|
|
||||||
{
|
|
||||||
pluginManager.CleanupPlugins();
|
|
||||||
Log.Information("[T3] PMC OK!");
|
|
||||||
}
|
|
||||||
|
|
||||||
using (Timings.Start("PM Load Sync Plugins"))
|
|
||||||
{
|
|
||||||
pluginManager.LoadAllPlugins().Wait();
|
|
||||||
Log.Information("[T3] PML OK!");
|
|
||||||
}
|
|
||||||
|
|
||||||
Task.Run(Troubleshooting.LogTroubleshooting);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Error(ex, "Plugin load failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
namespace Dalamud.Plugin.Ipc.Internal;
|
namespace Dalamud.Plugin.Ipc.Internal;
|
||||||
|
|
||||||
|
|
@ -10,11 +11,28 @@ internal class CallGate : IServiceType
|
||||||
{
|
{
|
||||||
private readonly Dictionary<string, CallGateChannel> gates = new();
|
private readonly Dictionary<string, CallGateChannel> gates = new();
|
||||||
|
|
||||||
|
private ImmutableDictionary<string, CallGateChannel>? gatesCopy;
|
||||||
|
|
||||||
[ServiceManager.ServiceConstructor]
|
[ServiceManager.ServiceConstructor]
|
||||||
private CallGate()
|
private CallGate()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the thread-safe view of the registered gates.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyDictionary<string, CallGateChannel> Gates
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var copy = this.gatesCopy;
|
||||||
|
if (copy is not null)
|
||||||
|
return copy;
|
||||||
|
lock (this.gates)
|
||||||
|
return this.gatesCopy ??= this.gates.ToImmutableDictionary(x => x.Key, x => x.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the provider associated with the specified name.
|
/// Gets the provider associated with the specified name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -22,8 +40,34 @@ internal class CallGate : IServiceType
|
||||||
/// <returns>A CallGate registered under the given name.</returns>
|
/// <returns>A CallGate registered under the given name.</returns>
|
||||||
public CallGateChannel GetOrCreateChannel(string name)
|
public CallGateChannel GetOrCreateChannel(string name)
|
||||||
{
|
{
|
||||||
if (!this.gates.TryGetValue(name, out var gate))
|
lock (this.gates)
|
||||||
gate = this.gates[name] = new CallGateChannel(name);
|
{
|
||||||
return gate;
|
if (!this.gates.TryGetValue(name, out var gate))
|
||||||
|
{
|
||||||
|
gate = this.gates[name] = new(name);
|
||||||
|
this.gatesCopy = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return gate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove empty gates from <see cref="Gates"/>.
|
||||||
|
/// </summary>
|
||||||
|
public void PurgeEmptyGates()
|
||||||
|
{
|
||||||
|
lock (this.gates)
|
||||||
|
{
|
||||||
|
var changed = false;
|
||||||
|
foreach (var (k, v) in this.Gates)
|
||||||
|
{
|
||||||
|
if (v.IsEmpty)
|
||||||
|
changed |= this.gates.Remove(k);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
this.gatesCopy = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
|
|
@ -14,6 +14,17 @@ namespace Dalamud.Plugin.Ipc.Internal;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class CallGateChannel
|
internal class CallGateChannel
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The actual storage.
|
||||||
|
/// </summary>
|
||||||
|
private readonly HashSet<Delegate> subscriptions = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A copy of the actual storage, that will be cleared and populated depending on changes made to
|
||||||
|
/// <see cref="subscriptions"/>.
|
||||||
|
/// </summary>
|
||||||
|
private ImmutableList<Delegate>? subscriptionsCopy;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="CallGateChannel"/> class.
|
/// Initializes a new instance of the <see cref="CallGateChannel"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -31,17 +42,52 @@ internal class CallGateChannel
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a list of delegate subscriptions for when SendMessage is called.
|
/// Gets a list of delegate subscriptions for when SendMessage is called.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<Delegate> Subscriptions { get; } = new();
|
public IReadOnlyList<Delegate> Subscriptions
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var copy = this.subscriptionsCopy;
|
||||||
|
if (copy is not null)
|
||||||
|
return copy;
|
||||||
|
lock (this.subscriptions)
|
||||||
|
return this.subscriptionsCopy ??= this.subscriptions.ToImmutableList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets an action for when InvokeAction is called.
|
/// Gets or sets an action for when InvokeAction is called.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Delegate Action { get; set; }
|
public Delegate? Action { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a func for when InvokeFunc is called.
|
/// Gets or sets a func for when InvokeFunc is called.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Delegate Func { get; set; }
|
public Delegate? Func { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether this <see cref="CallGateChannel"/> is not being used.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsEmpty => this.Action is null && this.Func is null && this.Subscriptions.Count == 0;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="CallGatePubSubBase.Subscribe"/>
|
||||||
|
internal void Subscribe(Delegate action)
|
||||||
|
{
|
||||||
|
lock (this.subscriptions)
|
||||||
|
{
|
||||||
|
this.subscriptionsCopy = null;
|
||||||
|
this.subscriptions.Add(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="CallGatePubSubBase.Unsubscribe"/>
|
||||||
|
internal void Unsubscribe(Delegate action)
|
||||||
|
{
|
||||||
|
lock (this.subscriptions)
|
||||||
|
{
|
||||||
|
this.subscriptionsCopy = null;
|
||||||
|
this.subscriptions.Remove(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invoke all actions that have subscribed to this IPC.
|
/// Invoke all actions that have subscribed to this IPC.
|
||||||
|
|
@ -49,9 +95,6 @@ internal class CallGateChannel
|
||||||
/// <param name="args">Message arguments.</param>
|
/// <param name="args">Message arguments.</param>
|
||||||
internal void SendMessage(object?[]? args)
|
internal void SendMessage(object?[]? args)
|
||||||
{
|
{
|
||||||
if (this.Subscriptions.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var subscription in this.Subscriptions)
|
foreach (var subscription in this.Subscriptions)
|
||||||
{
|
{
|
||||||
var methodInfo = subscription.GetMethodInfo();
|
var methodInfo = subscription.GetMethodInfo();
|
||||||
|
|
@ -105,7 +148,14 @@ internal class CallGateChannel
|
||||||
var paramTypes = methodInfo.GetParameters()
|
var paramTypes = methodInfo.GetParameters()
|
||||||
.Select(pi => pi.ParameterType).ToArray();
|
.Select(pi => pi.ParameterType).ToArray();
|
||||||
|
|
||||||
if (args?.Length != paramTypes.Length)
|
if (args is null)
|
||||||
|
{
|
||||||
|
if (paramTypes.Length == 0)
|
||||||
|
return;
|
||||||
|
throw new IpcLengthMismatchError(this.Name, 0, paramTypes.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.Length != paramTypes.Length)
|
||||||
throw new IpcLengthMismatchError(this.Name, args.Length, paramTypes.Length);
|
throw new IpcLengthMismatchError(this.Name, args.Length, paramTypes.Length);
|
||||||
|
|
||||||
for (var i = 0; i < args.Length; i++)
|
for (var i = 0; i < args.Length; i++)
|
||||||
|
|
@ -137,7 +187,7 @@ internal class CallGateChannel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<Type> GenerateTypes(Type type)
|
private IEnumerable<Type> GenerateTypes(Type? type)
|
||||||
{
|
{
|
||||||
while (type != null && type != typeof(object))
|
while (type != null && type != typeof(object))
|
||||||
{
|
{
|
||||||
|
|
@ -148,6 +198,9 @@ internal class CallGateChannel
|
||||||
|
|
||||||
private object? ConvertObject(object? obj, Type type)
|
private object? ConvertObject(object? obj, Type type)
|
||||||
{
|
{
|
||||||
|
if (obj is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
var json = JsonConvert.SerializeObject(obj);
|
var json = JsonConvert.SerializeObject(obj);
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
#pragma warning disable SA1402 // File may only contain a single type
|
#pragma warning disable SA1402 // File may only contain a single type
|
||||||
|
|
||||||
namespace Dalamud.Plugin.Ipc.Internal;
|
namespace Dalamud.Plugin.Ipc.Internal;
|
||||||
|
|
@ -37,7 +35,7 @@ internal class CallGatePubSub<TRet> : CallGatePubSubBase, ICallGateProvider<TRet
|
||||||
public void InvokeAction()
|
public void InvokeAction()
|
||||||
=> base.InvokeAction();
|
=> base.InvokeAction();
|
||||||
|
|
||||||
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc"/>
|
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc{TRet}"/>
|
||||||
public TRet InvokeFunc()
|
public TRet InvokeFunc()
|
||||||
=> this.InvokeFunc<TRet>();
|
=> this.InvokeFunc<TRet>();
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +73,7 @@ internal class CallGatePubSub<T1, TRet> : CallGatePubSubBase, ICallGateProvider<
|
||||||
public void InvokeAction(T1 arg1)
|
public void InvokeAction(T1 arg1)
|
||||||
=> base.InvokeAction(arg1);
|
=> base.InvokeAction(arg1);
|
||||||
|
|
||||||
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc"/>
|
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc{TRet}"/>
|
||||||
public TRet InvokeFunc(T1 arg1)
|
public TRet InvokeFunc(T1 arg1)
|
||||||
=> this.InvokeFunc<TRet>(arg1);
|
=> this.InvokeFunc<TRet>(arg1);
|
||||||
}
|
}
|
||||||
|
|
@ -113,7 +111,7 @@ internal class CallGatePubSub<T1, T2, TRet> : CallGatePubSubBase, ICallGateProvi
|
||||||
public void InvokeAction(T1 arg1, T2 arg2)
|
public void InvokeAction(T1 arg1, T2 arg2)
|
||||||
=> base.InvokeAction(arg1, arg2);
|
=> base.InvokeAction(arg1, arg2);
|
||||||
|
|
||||||
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc"/>
|
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc{TRet}"/>
|
||||||
public TRet InvokeFunc(T1 arg1, T2 arg2)
|
public TRet InvokeFunc(T1 arg1, T2 arg2)
|
||||||
=> this.InvokeFunc<TRet>(arg1, arg2);
|
=> this.InvokeFunc<TRet>(arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
@ -151,7 +149,7 @@ internal class CallGatePubSub<T1, T2, T3, TRet> : CallGatePubSubBase, ICallGateP
|
||||||
public void InvokeAction(T1 arg1, T2 arg2, T3 arg3)
|
public void InvokeAction(T1 arg1, T2 arg2, T3 arg3)
|
||||||
=> base.InvokeAction(arg1, arg2, arg3);
|
=> base.InvokeAction(arg1, arg2, arg3);
|
||||||
|
|
||||||
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc"/>
|
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc{TRet}"/>
|
||||||
public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3)
|
public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3)
|
||||||
=> this.InvokeFunc<TRet>(arg1, arg2, arg3);
|
=> this.InvokeFunc<TRet>(arg1, arg2, arg3);
|
||||||
}
|
}
|
||||||
|
|
@ -189,7 +187,7 @@ internal class CallGatePubSub<T1, T2, T3, T4, TRet> : CallGatePubSubBase, ICallG
|
||||||
public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4)
|
public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4)
|
||||||
=> base.InvokeAction(arg1, arg2, arg3, arg4);
|
=> base.InvokeAction(arg1, arg2, arg3, arg4);
|
||||||
|
|
||||||
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc"/>
|
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc{TRet}"/>
|
||||||
public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4)
|
public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4)
|
||||||
=> this.InvokeFunc<TRet>(arg1, arg2, arg3, arg4);
|
=> this.InvokeFunc<TRet>(arg1, arg2, arg3, arg4);
|
||||||
}
|
}
|
||||||
|
|
@ -227,7 +225,7 @@ internal class CallGatePubSub<T1, T2, T3, T4, T5, TRet> : CallGatePubSubBase, IC
|
||||||
public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5)
|
public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5)
|
||||||
=> base.InvokeAction(arg1, arg2, arg3, arg4, arg5);
|
=> base.InvokeAction(arg1, arg2, arg3, arg4, arg5);
|
||||||
|
|
||||||
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc"/>
|
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc{TRet}"/>
|
||||||
public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5)
|
public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5)
|
||||||
=> this.InvokeFunc<TRet>(arg1, arg2, arg3, arg4, arg5);
|
=> this.InvokeFunc<TRet>(arg1, arg2, arg3, arg4, arg5);
|
||||||
}
|
}
|
||||||
|
|
@ -265,7 +263,7 @@ internal class CallGatePubSub<T1, T2, T3, T4, T5, T6, TRet> : CallGatePubSubBase
|
||||||
public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6)
|
public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6)
|
||||||
=> base.InvokeAction(arg1, arg2, arg3, arg4, arg5, arg6);
|
=> base.InvokeAction(arg1, arg2, arg3, arg4, arg5, arg6);
|
||||||
|
|
||||||
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc"/>
|
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc{TRet}"/>
|
||||||
public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6)
|
public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6)
|
||||||
=> this.InvokeFunc<TRet>(arg1, arg2, arg3, arg4, arg5, arg6);
|
=> this.InvokeFunc<TRet>(arg1, arg2, arg3, arg4, arg5, arg6);
|
||||||
}
|
}
|
||||||
|
|
@ -303,7 +301,7 @@ internal class CallGatePubSub<T1, T2, T3, T4, T5, T6, T7, TRet> : CallGatePubSub
|
||||||
public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7)
|
public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7)
|
||||||
=> base.InvokeAction(arg1, arg2, arg3, arg4, arg5, arg6, arg7);
|
=> base.InvokeAction(arg1, arg2, arg3, arg4, arg5, arg6, arg7);
|
||||||
|
|
||||||
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc"/>
|
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc{TRet}"/>
|
||||||
public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7)
|
public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7)
|
||||||
=> this.InvokeFunc<TRet>(arg1, arg2, arg3, arg4, arg5, arg6, arg7);
|
=> this.InvokeFunc<TRet>(arg1, arg2, arg3, arg4, arg5, arg6, arg7);
|
||||||
}
|
}
|
||||||
|
|
@ -341,7 +339,7 @@ internal class CallGatePubSub<T1, T2, T3, T4, T5, T6, T7, T8, TRet> : CallGatePu
|
||||||
public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8)
|
public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8)
|
||||||
=> base.InvokeAction(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8);
|
=> base.InvokeAction(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8);
|
||||||
|
|
||||||
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc"/>
|
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc{TRet}"/>
|
||||||
public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8)
|
public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8)
|
||||||
=> this.InvokeFunc<TRet>(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8);
|
=> this.InvokeFunc<TRet>(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
using Dalamud.Plugin.Ipc.Exceptions;
|
using Dalamud.Plugin.Ipc.Exceptions;
|
||||||
|
|
||||||
namespace Dalamud.Plugin.Ipc.Internal;
|
namespace Dalamud.Plugin.Ipc.Internal;
|
||||||
|
|
@ -13,7 +11,7 @@ internal abstract class CallGatePubSubBase
|
||||||
/// Initializes a new instance of the <see cref="CallGatePubSubBase"/> class.
|
/// Initializes a new instance of the <see cref="CallGatePubSubBase"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name">The name of the IPC registration.</param>
|
/// <param name="name">The name of the IPC registration.</param>
|
||||||
public CallGatePubSubBase(string name)
|
protected CallGatePubSubBase(string name)
|
||||||
{
|
{
|
||||||
this.Channel = Service<CallGate>.Get().GetOrCreateChannel(name);
|
this.Channel = Service<CallGate>.Get().GetOrCreateChannel(name);
|
||||||
}
|
}
|
||||||
|
|
@ -54,14 +52,14 @@ internal abstract class CallGatePubSubBase
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="action">Action to subscribe.</param>
|
/// <param name="action">Action to subscribe.</param>
|
||||||
private protected void Subscribe(Delegate action)
|
private protected void Subscribe(Delegate action)
|
||||||
=> this.Channel.Subscriptions.Add(action);
|
=> this.Channel.Subscribe(action);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unsubscribe an expression from this registration.
|
/// Unsubscribe an expression from this registration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="action">Action to unsubscribe.</param>
|
/// <param name="action">Action to unsubscribe.</param>
|
||||||
private protected void Unsubscribe(Delegate action)
|
private protected void Unsubscribe(Delegate action)
|
||||||
=> this.Channel.Subscriptions.Remove(action);
|
=> this.Channel.Unsubscribe(action);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invoke an action registered for inter-plugin communication.
|
/// Invoke an action registered for inter-plugin communication.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Runtime.ExceptionServices;
|
||||||
|
|
||||||
|
using Dalamud.Plugin.Ipc.Exceptions;
|
||||||
|
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
namespace Dalamud.Plugin.Ipc.Internal;
|
namespace Dalamud.Plugin.Ipc.Internal;
|
||||||
|
|
||||||
|
|
@ -8,10 +13,14 @@ namespace Dalamud.Plugin.Ipc.Internal;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal readonly struct DataCache
|
internal readonly struct DataCache
|
||||||
{
|
{
|
||||||
|
/// <summary> Name of the data. </summary>
|
||||||
|
internal readonly string Tag;
|
||||||
|
|
||||||
/// <summary> The assembly name of the initial creator. </summary>
|
/// <summary> The assembly name of the initial creator. </summary>
|
||||||
internal readonly string CreatorAssemblyName;
|
internal readonly string CreatorAssemblyName;
|
||||||
|
|
||||||
/// <summary> A not-necessarily distinct list of current users. </summary>
|
/// <summary> A not-necessarily distinct list of current users. </summary>
|
||||||
|
/// <remarks> Also used as a reference count tracker. </remarks>
|
||||||
internal readonly List<string> UserAssemblyNames;
|
internal readonly List<string> UserAssemblyNames;
|
||||||
|
|
||||||
/// <summary> The type the data was registered as. </summary>
|
/// <summary> The type the data was registered as. </summary>
|
||||||
|
|
@ -23,14 +32,83 @@ internal readonly struct DataCache
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DataCache"/> struct.
|
/// Initializes a new instance of the <see cref="DataCache"/> struct.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="tag">Name of the data.</param>
|
||||||
/// <param name="creatorAssemblyName">The assembly name of the initial creator.</param>
|
/// <param name="creatorAssemblyName">The assembly name of the initial creator.</param>
|
||||||
/// <param name="data">A reference to data.</param>
|
/// <param name="data">A reference to data.</param>
|
||||||
/// <param name="type">The type of the data.</param>
|
/// <param name="type">The type of the data.</param>
|
||||||
public DataCache(string creatorAssemblyName, object? data, Type type)
|
public DataCache(string tag, string creatorAssemblyName, object? data, Type type)
|
||||||
{
|
{
|
||||||
|
this.Tag = tag;
|
||||||
this.CreatorAssemblyName = creatorAssemblyName;
|
this.CreatorAssemblyName = creatorAssemblyName;
|
||||||
this.UserAssemblyNames = new List<string> { creatorAssemblyName };
|
this.UserAssemblyNames = new();
|
||||||
this.Data = data;
|
this.Data = data;
|
||||||
this.Type = type;
|
this.Type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the <see cref="DataCache"/> struct, using the given data generator function.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tag">The name for the data cache.</param>
|
||||||
|
/// <param name="creatorAssemblyName">The assembly name of the initial creator.</param>
|
||||||
|
/// <param name="dataGenerator">The function that generates the data if it does not already exist.</param>
|
||||||
|
/// <typeparam name="T">The type of the stored data - needs to be a reference type that is shared through Dalamud itself, not loaded by the plugin.</typeparam>
|
||||||
|
/// <returns>The new instance of <see cref="DataCache"/>.</returns>
|
||||||
|
public static DataCache From<T>(string tag, string creatorAssemblyName, Func<T> dataGenerator)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = new DataCache(tag, creatorAssemblyName, dataGenerator.Invoke(), typeof(T));
|
||||||
|
Log.Verbose(
|
||||||
|
"[{who}] Created new data for [{Tag:l}] for creator {Creator:l}.",
|
||||||
|
nameof(DataShare),
|
||||||
|
tag,
|
||||||
|
creatorAssemblyName);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw ExceptionDispatchInfo.SetCurrentStackTrace(
|
||||||
|
new DataCacheCreationError(tag, creatorAssemblyName, typeof(T), e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to fetch the data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="callerName">The name of the caller assembly.</param>
|
||||||
|
/// <param name="value">The value, if succeeded.</param>
|
||||||
|
/// <param name="ex">The exception, if failed.</param>
|
||||||
|
/// <typeparam name="T">Desired type of the data.</typeparam>
|
||||||
|
/// <returns><c>true</c> on success.</returns>
|
||||||
|
public bool TryGetData<T>(
|
||||||
|
string callerName,
|
||||||
|
[NotNullWhen(true)] out T? value,
|
||||||
|
[NotNullWhen(false)] out Exception? ex)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
switch (this.Data)
|
||||||
|
{
|
||||||
|
case null:
|
||||||
|
value = null;
|
||||||
|
ex = ExceptionDispatchInfo.SetCurrentStackTrace(new DataCacheValueNullError(this.Tag, this.Type));
|
||||||
|
return false;
|
||||||
|
|
||||||
|
case T data:
|
||||||
|
value = data;
|
||||||
|
ex = null;
|
||||||
|
|
||||||
|
// Register the access history
|
||||||
|
lock (this.UserAssemblyNames)
|
||||||
|
this.UserAssemblyNames.Add(callerName);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
value = null;
|
||||||
|
ex = ExceptionDispatchInfo.SetCurrentStackTrace(
|
||||||
|
new DataCacheTypeMismatchError(this.Tag, this.CreatorAssemblyName, typeof(T), this.Type));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
using Dalamud.Plugin.Ipc.Exceptions;
|
using Dalamud.Plugin.Ipc.Exceptions;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
@ -16,7 +14,11 @@ namespace Dalamud.Plugin.Ipc.Internal;
|
||||||
[ServiceManager.BlockingEarlyLoadedService]
|
[ServiceManager.BlockingEarlyLoadedService]
|
||||||
internal class DataShare : IServiceType
|
internal class DataShare : IServiceType
|
||||||
{
|
{
|
||||||
private readonly Dictionary<string, DataCache> caches = new();
|
/// <summary>
|
||||||
|
/// Dictionary of cached values. Note that <see cref="Lazy{T}"/> is being used, as it does its own locking,
|
||||||
|
/// effectively preventing calling the data generator multiple times concurrently.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<string, Lazy<DataCache>> caches = new();
|
||||||
|
|
||||||
[ServiceManager.ServiceConstructor]
|
[ServiceManager.ServiceConstructor]
|
||||||
private DataShare()
|
private DataShare()
|
||||||
|
|
@ -39,38 +41,15 @@ internal class DataShare : IServiceType
|
||||||
where T : class
|
where T : class
|
||||||
{
|
{
|
||||||
var callerName = GetCallerName();
|
var callerName = GetCallerName();
|
||||||
|
|
||||||
|
Lazy<DataCache> cacheLazy;
|
||||||
lock (this.caches)
|
lock (this.caches)
|
||||||
{
|
{
|
||||||
if (this.caches.TryGetValue(tag, out var cache))
|
if (!this.caches.TryGetValue(tag, out cacheLazy))
|
||||||
{
|
this.caches[tag] = cacheLazy = new(() => DataCache.From(tag, callerName, dataGenerator));
|
||||||
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;
|
|
||||||
|
|
||||||
Log.Verbose("[DataShare] Created new data for [{Tag:l}] for creator {Creator:l}.", tag, callerName);
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
throw new DataCacheCreationError(tag, callerName, typeof(T), e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return cacheLazy.Value.TryGetData<T>(callerName, out var value, out var ex) ? value : throw ex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -80,34 +59,36 @@ internal class DataShare : IServiceType
|
||||||
/// <param name="tag">The name for the data cache.</param>
|
/// <param name="tag">The name for the data cache.</param>
|
||||||
public void RelinquishData(string tag)
|
public void RelinquishData(string tag)
|
||||||
{
|
{
|
||||||
|
DataCache cache;
|
||||||
lock (this.caches)
|
lock (this.caches)
|
||||||
{
|
{
|
||||||
if (!this.caches.TryGetValue(tag, out var cache))
|
if (!this.caches.TryGetValue(tag, out var cacheLazy))
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
var callerName = GetCallerName();
|
var callerName = GetCallerName();
|
||||||
lock (this.caches)
|
|
||||||
{
|
|
||||||
if (!cache.UserAssemblyNames.Remove(callerName) || cache.UserAssemblyNames.Count > 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.caches.Remove(tag))
|
cache = cacheLazy.Value;
|
||||||
{
|
if (!cache.UserAssemblyNames.Remove(callerName) || cache.UserAssemblyNames.Count > 0)
|
||||||
if (cache.Data is IDisposable disposable)
|
return;
|
||||||
{
|
if (!this.caches.Remove(tag))
|
||||||
disposable.Dispose();
|
return;
|
||||||
Log.Verbose("[DataShare] Disposed [{Tag:l}] after it was removed from all shares.", tag);
|
}
|
||||||
}
|
|
||||||
else
|
if (cache.Data is IDisposable disposable)
|
||||||
{
|
{
|
||||||
Log.Verbose("[DataShare] Removed [{Tag:l}] from all shares.", tag);
|
try
|
||||||
}
|
{
|
||||||
}
|
disposable.Dispose();
|
||||||
|
Log.Verbose("[DataShare] Disposed [{Tag:l}] after it was removed from all shares.", tag);
|
||||||
}
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error(e, "[DataShare] Failed to dispose [{Tag:l}] after it was removed from all shares.", tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log.Verbose("[DataShare] Removed [{Tag:l}] from all shares.", tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,23 +104,14 @@ internal class DataShare : IServiceType
|
||||||
where T : class
|
where T : class
|
||||||
{
|
{
|
||||||
data = null;
|
data = null;
|
||||||
|
Lazy<DataCache> cacheLazy;
|
||||||
lock (this.caches)
|
lock (this.caches)
|
||||||
{
|
{
|
||||||
if (!this.caches.TryGetValue(tag, out var cache) || !cache.Type.IsAssignableTo(typeof(T)))
|
if (!this.caches.TryGetValue(tag, out cacheLazy))
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
var callerName = GetCallerName();
|
|
||||||
data = cache.Data as T;
|
|
||||||
if (data == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
cache.UserAssemblyNames.Add(callerName);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return cacheLazy.Value.TryGetData(GetCallerName(), out data, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -155,27 +127,14 @@ internal class DataShare : IServiceType
|
||||||
public T GetData<T>(string tag)
|
public T GetData<T>(string tag)
|
||||||
where T : class
|
where T : class
|
||||||
{
|
{
|
||||||
|
Lazy<DataCache> cacheLazy;
|
||||||
lock (this.caches)
|
lock (this.caches)
|
||||||
{
|
{
|
||||||
if (!this.caches.TryGetValue(tag, out var cache))
|
if (!this.caches.TryGetValue(tag, out cacheLazy))
|
||||||
{
|
|
||||||
throw new KeyNotFoundException($"The data cache [{tag}] is not registered.");
|
throw new KeyNotFoundException($"The data cache [{tag}] is not registered.");
|
||||||
}
|
|
||||||
|
|
||||||
var callerName = Assembly.GetCallingAssembly().GetName().Name ?? string.Empty;
|
|
||||||
if (!cache.Type.IsAssignableTo(typeof(T)))
|
|
||||||
{
|
|
||||||
throw new DataCacheTypeMismatchError(tag, callerName, typeof(T), cache.Type);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cache.Data is not T data)
|
|
||||||
{
|
|
||||||
throw new DataCacheValueNullError(tag, typeof(T));
|
|
||||||
}
|
|
||||||
|
|
||||||
cache.UserAssemblyNames.Add(callerName);
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return cacheLazy.Value.TryGetData<T>(GetCallerName(), out var value, out var ex) ? value : throw ex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -186,7 +145,8 @@ internal class DataShare : IServiceType
|
||||||
{
|
{
|
||||||
lock (this.caches)
|
lock (this.caches)
|
||||||
{
|
{
|
||||||
return this.caches.Select(kvp => (kvp.Key, kvp.Value.CreatorAssemblyName, kvp.Value.UserAssemblyNames.ToArray()));
|
return this.caches.Select(
|
||||||
|
kvp => (kvp.Key, kvp.Value.Value.CreatorAssemblyName, kvp.Value.Value.UserAssemblyNames.ToArray()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
106
Dalamud/Plugin/Services/IGameInventory.cs
Normal file
106
Dalamud/Plugin/Services/IGameInventory.cs
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
using Dalamud.Game.Inventory;
|
||||||
|
using Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||||
|
|
||||||
|
namespace Dalamud.Plugin.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This class provides events for the in-game inventory.
|
||||||
|
/// </summary>
|
||||||
|
public interface IGameInventory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Delegate function to be called when inventories have been changed.
|
||||||
|
/// This delegate sends the entire set of changes recorded.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="events">The events.</param>
|
||||||
|
public delegate void InventoryChangelogDelegate(IReadOnlyCollection<InventoryEventArgs> events);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delegate function to be called for each change to inventories.
|
||||||
|
/// This delegate sends individual events for changes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The event try that triggered this message.</param>
|
||||||
|
/// <param name="data">Data for the triggered event.</param>
|
||||||
|
public delegate void InventoryChangedDelegate(GameInventoryEvent type, InventoryEventArgs data);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delegate function to be called for each change to inventories.
|
||||||
|
/// This delegate sends individual events for changes.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The event arg type.</typeparam>
|
||||||
|
/// <param name="data">Data for the triggered event.</param>
|
||||||
|
public delegate void InventoryChangedDelegate<in T>(T data) where T : InventoryEventArgs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event that is fired when the inventory has been changed.<br />
|
||||||
|
/// Note that some events, such as <see cref="ItemAdded"/>, <see cref="ItemRemoved"/>, and <see cref="ItemChanged"/>
|
||||||
|
/// currently is subject to reinterpretation as <see cref="ItemMoved"/>, <see cref="ItemMerged"/>, and
|
||||||
|
/// <see cref="ItemSplit"/>.<br />
|
||||||
|
/// Use <see cref="InventoryChangedRaw"/> if you do not want such reinterpretation.
|
||||||
|
/// </summary>
|
||||||
|
public event InventoryChangelogDelegate InventoryChanged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event that is fired when the inventory has been changed, without trying to interpret two inventory slot changes
|
||||||
|
/// as a move event as appropriate.<br />
|
||||||
|
/// In other words, <see cref="GameInventoryEvent.Moved"/>, <see cref="GameInventoryEvent.Merged"/>, and
|
||||||
|
/// <see cref="GameInventoryEvent.Split"/> currently do not fire in this event.
|
||||||
|
/// </summary>
|
||||||
|
public event InventoryChangelogDelegate InventoryChangedRaw;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event that is fired when an item is added to an inventory.<br />
|
||||||
|
/// If this event is a part of multi-step event, then this event will not be called.<br />
|
||||||
|
/// Use <see cref="InventoryChangedRaw"/> if you do not want such reinterpretation.
|
||||||
|
/// </summary>
|
||||||
|
public event InventoryChangedDelegate ItemAdded;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event that is fired when an item is removed from an inventory.<br />
|
||||||
|
/// If this event is a part of multi-step event, then this event will not be called.<br />
|
||||||
|
/// Use <see cref="InventoryChangedRaw"/> if you do not want such reinterpretation.
|
||||||
|
/// </summary>
|
||||||
|
public event InventoryChangedDelegate ItemRemoved;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event that is fired when an items properties are changed.<br />
|
||||||
|
/// If this event is a part of multi-step event, then this event will not be called.<br />
|
||||||
|
/// Use <see cref="InventoryChangedRaw"/> if you do not want such reinterpretation.
|
||||||
|
/// </summary>
|
||||||
|
public event InventoryChangedDelegate ItemChanged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event that is fired when an item is moved from one inventory into another.
|
||||||
|
/// </summary>
|
||||||
|
public event InventoryChangedDelegate ItemMoved;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event that is fired when an item is split from one stack into two.
|
||||||
|
/// </summary>
|
||||||
|
public event InventoryChangedDelegate ItemSplit;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event that is fired when an item is merged from two stacks into one.
|
||||||
|
/// </summary>
|
||||||
|
public event InventoryChangedDelegate ItemMerged;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="ItemAdded"/>
|
||||||
|
public event InventoryChangedDelegate<InventoryItemAddedArgs> ItemAddedExplicit;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="ItemRemoved"/>
|
||||||
|
public event InventoryChangedDelegate<InventoryItemRemovedArgs> ItemRemovedExplicit;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="ItemChanged"/>
|
||||||
|
public event InventoryChangedDelegate<InventoryItemChangedArgs> ItemChangedExplicit;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="ItemMoved"/>
|
||||||
|
public event InventoryChangedDelegate<InventoryItemMovedArgs> ItemMovedExplicit;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="ItemSplit"/>
|
||||||
|
public event InventoryChangedDelegate<InventoryItemSplitArgs> ItemSplitExplicit;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="ItemMerged"/>
|
||||||
|
public event InventoryChangedDelegate<InventoryItemMergedArgs> ItemMergedExplicit;
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ using Dalamud.Game;
|
||||||
using Dalamud.IoC.Internal;
|
using Dalamud.IoC.Internal;
|
||||||
using Dalamud.Logging.Internal;
|
using Dalamud.Logging.Internal;
|
||||||
using Dalamud.Storage;
|
using Dalamud.Storage;
|
||||||
|
using Dalamud.Utility;
|
||||||
using Dalamud.Utility.Timing;
|
using Dalamud.Utility.Timing;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
|
|
@ -21,7 +22,7 @@ namespace Dalamud;
|
||||||
// - Visualize/output .dot or imgui thing
|
// - Visualize/output .dot or imgui thing
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class to initialize Service<T>s.
|
/// Class to initialize <see cref="Service{T}"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class ServiceManager
|
internal static class ServiceManager
|
||||||
{
|
{
|
||||||
|
|
@ -43,6 +44,26 @@ internal static class ServiceManager
|
||||||
private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new();
|
private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new();
|
||||||
|
|
||||||
private static ManualResetEvent unloadResetEvent = new(false);
|
private static ManualResetEvent unloadResetEvent = new(false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delegate for registering startup blocker task.<br />
|
||||||
|
/// Do not use this delegate outside the constructor.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="t">The blocker task.</param>
|
||||||
|
/// <param name="justification">The justification for using this feature.</param>
|
||||||
|
[InjectableType]
|
||||||
|
public delegate void RegisterStartupBlockerDelegate(Task t, string justification);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delegate for registering services that should be unloaded before self.<br />
|
||||||
|
/// Intended for use with <see cref="Plugin.Internal.PluginManager"/>. If you think you need to use this outside
|
||||||
|
/// of that, consider having a discussion first.<br />
|
||||||
|
/// Do not use this delegate outside the constructor.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="unloadAfter">Services that should be unloaded first.</param>
|
||||||
|
/// <param name="justification">The justification for using this feature.</param>
|
||||||
|
[InjectableType]
|
||||||
|
public delegate void RegisterUnloadAfterDelegate(IEnumerable<Type> unloadAfter, string justification);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Kinds of services.
|
/// Kinds of services.
|
||||||
|
|
@ -125,6 +146,15 @@ internal static class ServiceManager
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the concrete types of services, i.e. the non-abstract non-interface types.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The enumerable of service types, that may be enumerated only once per call.</returns>
|
||||||
|
public static IEnumerable<Type> GetConcreteServiceTypes() =>
|
||||||
|
Assembly.GetExecutingAssembly()
|
||||||
|
.GetTypes()
|
||||||
|
.Where(x => x.IsAssignableTo(typeof(IServiceType)) && !x.IsInterface && !x.IsAbstract);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Kicks off construction of services that can handle early loading.
|
/// Kicks off construction of services that can handle early loading.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -141,7 +171,7 @@ internal static class ServiceManager
|
||||||
|
|
||||||
var serviceContainer = Service<ServiceContainer>.Get();
|
var serviceContainer = Service<ServiceContainer>.Get();
|
||||||
|
|
||||||
foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes().Where(x => x.IsAssignableTo(typeof(IServiceType)) && !x.IsInterface && !x.IsAbstract))
|
foreach (var serviceType in GetConcreteServiceTypes())
|
||||||
{
|
{
|
||||||
var serviceKind = serviceType.GetServiceKind();
|
var serviceKind = serviceType.GetServiceKind();
|
||||||
Debug.Assert(serviceKind != ServiceKind.None, $"Service<{serviceType.FullName}> did not specify a kind");
|
Debug.Assert(serviceKind != ServiceKind.None, $"Service<{serviceType.FullName}> did not specify a kind");
|
||||||
|
|
@ -157,7 +187,7 @@ internal static class ServiceManager
|
||||||
|
|
||||||
var getTask = (Task)genericWrappedServiceType
|
var getTask = (Task)genericWrappedServiceType
|
||||||
.InvokeMember(
|
.InvokeMember(
|
||||||
"GetAsync",
|
nameof(Service<IServiceType>.GetAsync),
|
||||||
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
|
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
|
@ -184,17 +214,42 @@ internal static class ServiceManager
|
||||||
}
|
}
|
||||||
|
|
||||||
var typeAsServiceT = ServiceHelpers.GetAsService(serviceType);
|
var typeAsServiceT = ServiceHelpers.GetAsService(serviceType);
|
||||||
dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT)
|
dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT, false)
|
||||||
.Select(x => typeof(Service<>).MakeGenericType(x))
|
.Select(x => typeof(Service<>).MakeGenericType(x))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var blockerTasks = new List<Task>();
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var whenBlockingComplete = Task.WhenAll(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x]));
|
// Wait for all blocking constructors to complete first.
|
||||||
while (await Task.WhenAny(whenBlockingComplete, Task.Delay(30000)) != whenBlockingComplete)
|
await WaitWithTimeoutConsent(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x]));
|
||||||
|
|
||||||
|
// All the BlockingEarlyLoadedService constructors have been run,
|
||||||
|
// and blockerTasks now will not change. Now wait for them.
|
||||||
|
// Note that ServiceManager.CallWhenServicesReady does not get to register a blocker.
|
||||||
|
await WaitWithTimeoutConsent(blockerTasks);
|
||||||
|
|
||||||
|
BlockingServicesLoadedTaskCompletionSource.SetResult();
|
||||||
|
Timings.Event("BlockingServices Initialized");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
BlockingServicesLoadedTaskCompletionSource.SetException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
async Task WaitWithTimeoutConsent(IEnumerable<Task> tasksEnumerable)
|
||||||
|
{
|
||||||
|
var tasks = tasksEnumerable.AsReadOnlyCollection();
|
||||||
|
if (tasks.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var aggregatedTask = Task.WhenAll(tasks);
|
||||||
|
while (await Task.WhenAny(aggregatedTask, Task.Delay(120000)) != aggregatedTask)
|
||||||
{
|
{
|
||||||
if (NativeFunctions.MessageBoxW(
|
if (NativeFunctions.MessageBoxW(
|
||||||
IntPtr.Zero,
|
IntPtr.Zero,
|
||||||
|
|
@ -208,13 +263,6 @@ internal static class ServiceManager
|
||||||
"and the user chose to continue without Dalamud.");
|
"and the user chose to continue without Dalamud.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BlockingServicesLoadedTaskCompletionSource.SetResult();
|
|
||||||
Timings.Event("BlockingServices Initialized");
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
BlockingServicesLoadedTaskCompletionSource.SetException(e);
|
|
||||||
}
|
}
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
|
@ -249,6 +297,25 @@ internal static class ServiceManager
|
||||||
if (!hasDeps)
|
if (!hasDeps)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// This object will be used in a task. Each task must receive a new object.
|
||||||
|
var startLoaderArgs = new List<object>();
|
||||||
|
if (serviceType.GetCustomAttribute<BlockingEarlyLoadedServiceAttribute>() is not null)
|
||||||
|
{
|
||||||
|
startLoaderArgs.Add(
|
||||||
|
new RegisterStartupBlockerDelegate(
|
||||||
|
(task, justification) =>
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
if (CurrentConstructorServiceType.Value != serviceType)
|
||||||
|
throw new InvalidOperationException("Forbidden.");
|
||||||
|
#endif
|
||||||
|
blockerTasks.Add(task);
|
||||||
|
|
||||||
|
// No need to store the justification; the fact that the reason is specified is good enough.
|
||||||
|
_ = justification;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
tasks.Add((Task)typeof(Service<>)
|
tasks.Add((Task)typeof(Service<>)
|
||||||
.MakeGenericType(serviceType)
|
.MakeGenericType(serviceType)
|
||||||
.InvokeMember(
|
.InvokeMember(
|
||||||
|
|
@ -256,7 +323,7 @@ internal static class ServiceManager
|
||||||
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic,
|
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null));
|
new object[] { startLoaderArgs }));
|
||||||
servicesToLoad.Remove(serviceType);
|
servicesToLoad.Remove(serviceType);
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|
@ -328,13 +395,13 @@ internal static class ServiceManager
|
||||||
|
|
||||||
unloadResetEvent.Reset();
|
unloadResetEvent.Reset();
|
||||||
|
|
||||||
var dependencyServicesMap = new Dictionary<Type, List<Type>>();
|
var dependencyServicesMap = new Dictionary<Type, IReadOnlyCollection<Type>>();
|
||||||
var allToUnload = new HashSet<Type>();
|
var allToUnload = new HashSet<Type>();
|
||||||
var unloadOrder = new List<Type>();
|
var unloadOrder = new List<Type>();
|
||||||
|
|
||||||
Log.Information("==== COLLECTING SERVICES TO UNLOAD ====");
|
Log.Information("==== COLLECTING SERVICES TO UNLOAD ====");
|
||||||
|
|
||||||
foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes())
|
foreach (var serviceType in GetConcreteServiceTypes())
|
||||||
{
|
{
|
||||||
if (!serviceType.IsAssignableTo(typeof(IServiceType)))
|
if (!serviceType.IsAssignableTo(typeof(IServiceType)))
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -347,7 +414,7 @@ internal static class ServiceManager
|
||||||
Log.Verbose("Calling GetDependencyServices for '{ServiceName}'", serviceType.FullName!);
|
Log.Verbose("Calling GetDependencyServices for '{ServiceName}'", serviceType.FullName!);
|
||||||
|
|
||||||
var typeAsServiceT = ServiceHelpers.GetAsService(serviceType);
|
var typeAsServiceT = ServiceHelpers.GetAsService(serviceType);
|
||||||
dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT);
|
dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT, true);
|
||||||
|
|
||||||
allToUnload.Add(serviceType);
|
allToUnload.Add(serviceType);
|
||||||
}
|
}
|
||||||
|
|
@ -541,11 +608,35 @@ internal static class ServiceManager
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates that the method should be called when the services given in the constructor are ready.
|
/// Indicates that the method should be called when the services given in the marked method's parameters are ready.
|
||||||
|
/// This will be executed immediately after the constructor has run, if all services specified as its parameters
|
||||||
|
/// are already ready, or no parameter is given.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Method)]
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
[MeansImplicitUse]
|
[MeansImplicitUse]
|
||||||
public class CallWhenServicesReady : Attribute
|
public class CallWhenServicesReady : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="CallWhenServicesReady"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="justification">Specify the reason here.</param>
|
||||||
|
public CallWhenServicesReady(string justification)
|
||||||
|
{
|
||||||
|
// No need to store the justification; the fact that the reason is specified is good enough.
|
||||||
|
_ = justification;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that something is a candidate for being considered as an injected parameter for constructors.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(
|
||||||
|
AttributeTargets.Delegate
|
||||||
|
| AttributeTargets.Class
|
||||||
|
| AttributeTargets.Struct
|
||||||
|
| AttributeTargets.Enum
|
||||||
|
| AttributeTargets.Interface)]
|
||||||
|
public class InjectableTypeAttribute : Attribute
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ using System.Threading.Tasks;
|
||||||
|
|
||||||
using Dalamud.IoC;
|
using Dalamud.IoC;
|
||||||
using Dalamud.IoC.Internal;
|
using Dalamud.IoC.Internal;
|
||||||
using Dalamud.Plugin.Internal;
|
|
||||||
using Dalamud.Utility.Timing;
|
using Dalamud.Utility.Timing;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
|
|
@ -25,6 +24,7 @@ internal static class Service<T> where T : IServiceType
|
||||||
private static readonly ServiceManager.ServiceAttribute ServiceAttribute;
|
private static readonly ServiceManager.ServiceAttribute ServiceAttribute;
|
||||||
private static TaskCompletionSource<T> instanceTcs = new();
|
private static TaskCompletionSource<T> instanceTcs = new();
|
||||||
private static List<Type>? dependencyServices;
|
private static List<Type>? dependencyServices;
|
||||||
|
private static List<Type>? dependencyServicesForUnload;
|
||||||
|
|
||||||
static Service()
|
static Service()
|
||||||
{
|
{
|
||||||
|
|
@ -95,7 +95,7 @@ internal static class Service<T> where T : IServiceType
|
||||||
if (ServiceAttribute.Kind != ServiceManager.ServiceKind.ProvidedService
|
if (ServiceAttribute.Kind != ServiceManager.ServiceKind.ProvidedService
|
||||||
&& ServiceManager.CurrentConstructorServiceType.Value is { } currentServiceType)
|
&& ServiceManager.CurrentConstructorServiceType.Value is { } currentServiceType)
|
||||||
{
|
{
|
||||||
var deps = ServiceHelpers.GetDependencies(currentServiceType);
|
var deps = ServiceHelpers.GetDependencies(typeof(Service<>).MakeGenericType(currentServiceType), false);
|
||||||
if (!deps.Contains(typeof(T)))
|
if (!deps.Contains(typeof(T)))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
|
|
@ -115,7 +115,6 @@ internal static class Service<T> where T : IServiceType
|
||||||
/// Pull the instance out of the service locator, waiting if necessary.
|
/// Pull the instance out of the service locator, waiting if necessary.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The object.</returns>
|
/// <returns>The object.</returns>
|
||||||
[UsedImplicitly]
|
|
||||||
public static Task<T> GetAsync() => instanceTcs.Task;
|
public static Task<T> GetAsync() => instanceTcs.Task;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -141,11 +140,15 @@ internal static class Service<T> where T : IServiceType
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets an enumerable containing <see cref="Service{T}"/>s that are required for this Service to initialize
|
/// Gets an enumerable containing <see cref="Service{T}"/>s that are required for this Service to initialize
|
||||||
/// without blocking.
|
/// without blocking.
|
||||||
|
/// These are NOT returned as <see cref="Service{T}"/> types; raw types will be returned.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="includeUnloadDependencies">Whether to include the unload dependencies.</param>
|
||||||
/// <returns>List of dependency services.</returns>
|
/// <returns>List of dependency services.</returns>
|
||||||
[UsedImplicitly]
|
public static IReadOnlyCollection<Type> GetDependencyServices(bool includeUnloadDependencies)
|
||||||
public static List<Type> GetDependencyServices()
|
|
||||||
{
|
{
|
||||||
|
if (includeUnloadDependencies && dependencyServicesForUnload is not null)
|
||||||
|
return dependencyServicesForUnload;
|
||||||
|
|
||||||
if (dependencyServices is not null)
|
if (dependencyServices is not null)
|
||||||
return dependencyServices;
|
return dependencyServices;
|
||||||
|
|
||||||
|
|
@ -158,7 +161,8 @@ internal static class Service<T> where T : IServiceType
|
||||||
{
|
{
|
||||||
res.AddRange(ctor
|
res.AddRange(ctor
|
||||||
.GetParameters()
|
.GetParameters()
|
||||||
.Select(x => x.ParameterType));
|
.Select(x => x.ParameterType)
|
||||||
|
.Where(x => x.GetServiceKind() != ServiceManager.ServiceKind.None));
|
||||||
}
|
}
|
||||||
|
|
||||||
res.AddRange(typeof(T)
|
res.AddRange(typeof(T)
|
||||||
|
|
@ -171,50 +175,8 @@ internal static class Service<T> where T : IServiceType
|
||||||
.OfType<InherentDependencyAttribute>()
|
.OfType<InherentDependencyAttribute>()
|
||||||
.Select(x => x.GetType().GetGenericArguments().First()));
|
.Select(x => x.GetType().GetGenericArguments().First()));
|
||||||
|
|
||||||
// HACK: PluginManager needs to depend on ALL plugin exposed services
|
|
||||||
if (typeof(T) == typeof(PluginManager))
|
|
||||||
{
|
|
||||||
foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes())
|
|
||||||
{
|
|
||||||
if (!serviceType.IsAssignableTo(typeof(IServiceType)))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (serviceType == typeof(PluginManager))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Scoped plugin services lifetime is tied to their scopes. They go away when LocalPlugin goes away.
|
|
||||||
// Nonetheless, their direct dependencies must be considered.
|
|
||||||
if (serviceType.GetServiceKind() == ServiceManager.ServiceKind.ScopedService)
|
|
||||||
{
|
|
||||||
var typeAsServiceT = ServiceHelpers.GetAsService(serviceType);
|
|
||||||
var dependencies = ServiceHelpers.GetDependencies(typeAsServiceT);
|
|
||||||
ServiceManager.Log.Verbose("Found dependencies of scoped plugin service {Type} ({Cnt})", serviceType.FullName!, dependencies!.Count);
|
|
||||||
|
|
||||||
foreach (var scopedDep in dependencies)
|
|
||||||
{
|
|
||||||
if (scopedDep == typeof(PluginManager))
|
|
||||||
throw new Exception("Scoped plugin services cannot depend on PluginManager.");
|
|
||||||
|
|
||||||
ServiceManager.Log.Verbose("PluginManager MUST depend on {Type} via {BaseType}", scopedDep.FullName!, serviceType.FullName!);
|
|
||||||
res.Add(scopedDep);
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var pluginInterfaceAttribute = serviceType.GetCustomAttribute<PluginInterfaceAttribute>(true);
|
|
||||||
if (pluginInterfaceAttribute == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
ServiceManager.Log.Verbose("PluginManager MUST depend on {Type}", serviceType.FullName!);
|
|
||||||
res.Add(serviceType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var type in res)
|
foreach (var type in res)
|
||||||
{
|
|
||||||
ServiceManager.Log.Verbose("Service<{0}>: => Dependency: {1}", typeof(T).Name, type.Name);
|
ServiceManager.Log.Verbose("Service<{0}>: => Dependency: {1}", typeof(T).Name, type.Name);
|
||||||
}
|
|
||||||
|
|
||||||
var deps = res
|
var deps = res
|
||||||
.Distinct()
|
.Distinct()
|
||||||
|
|
@ -244,8 +206,9 @@ internal static class Service<T> where T : IServiceType
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts the service loader. Only to be called from <see cref="ServiceManager"/>.
|
/// Starts the service loader. Only to be called from <see cref="ServiceManager"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="additionalProvidedTypedObjects">Additional objects available to constructors.</param>
|
||||||
/// <returns>The loader task.</returns>
|
/// <returns>The loader task.</returns>
|
||||||
internal static Task<T> StartLoader()
|
internal static Task<T> StartLoader(IReadOnlyCollection<object> additionalProvidedTypedObjects)
|
||||||
{
|
{
|
||||||
if (instanceTcs.Task.IsCompleted)
|
if (instanceTcs.Task.IsCompleted)
|
||||||
throw new InvalidOperationException($"{typeof(T).Name} is already loaded or disposed.");
|
throw new InvalidOperationException($"{typeof(T).Name} is already loaded or disposed.");
|
||||||
|
|
@ -256,10 +219,27 @@ internal static class Service<T> where T : IServiceType
|
||||||
|
|
||||||
return Task.Run(Timings.AttachTimingHandle(async () =>
|
return Task.Run(Timings.AttachTimingHandle(async () =>
|
||||||
{
|
{
|
||||||
|
var ctorArgs = new List<object>(additionalProvidedTypedObjects.Count + 1);
|
||||||
|
ctorArgs.AddRange(additionalProvidedTypedObjects);
|
||||||
|
ctorArgs.Add(
|
||||||
|
new ServiceManager.RegisterUnloadAfterDelegate(
|
||||||
|
(additionalDependencies, justification) =>
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
if (ServiceManager.CurrentConstructorServiceType.Value != typeof(T))
|
||||||
|
throw new InvalidOperationException("Forbidden.");
|
||||||
|
#endif
|
||||||
|
dependencyServicesForUnload ??= new(GetDependencyServices(false));
|
||||||
|
dependencyServicesForUnload.AddRange(additionalDependencies);
|
||||||
|
|
||||||
|
// No need to store the justification; the fact that the reason is specified is good enough.
|
||||||
|
_ = justification;
|
||||||
|
}));
|
||||||
|
|
||||||
ServiceManager.Log.Debug("Service<{0}>: Begin construction", typeof(T).Name);
|
ServiceManager.Log.Debug("Service<{0}>: Begin construction", typeof(T).Name);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var instance = await ConstructObject();
|
var instance = await ConstructObject(ctorArgs).ConfigureAwait(false);
|
||||||
instanceTcs.SetResult(instance);
|
instanceTcs.SetResult(instance);
|
||||||
|
|
||||||
List<Task>? tasks = null;
|
List<Task>? tasks = null;
|
||||||
|
|
@ -270,8 +250,17 @@ internal static class Service<T> where T : IServiceType
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
ServiceManager.Log.Debug("Service<{0}>: Calling {1}", typeof(T).Name, method.Name);
|
ServiceManager.Log.Debug("Service<{0}>: Calling {1}", typeof(T).Name, method.Name);
|
||||||
var args = await Task.WhenAll(method.GetParameters().Select(
|
var args = await ResolveInjectedParameters(
|
||||||
x => ResolveServiceFromTypeAsync(x.ParameterType)));
|
method.GetParameters(),
|
||||||
|
Array.Empty<object>()).ConfigureAwait(false);
|
||||||
|
if (args.Length == 0)
|
||||||
|
{
|
||||||
|
ServiceManager.Log.Warning(
|
||||||
|
"Service<{0}>: Method {1} does not have any arguments. Consider merging it with the ctor.",
|
||||||
|
typeof(T).Name,
|
||||||
|
method.Name);
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (method.Invoke(instance, args) is Task task)
|
if (method.Invoke(instance, args) is Task task)
|
||||||
|
|
@ -331,24 +320,6 @@ internal static class Service<T> where T : IServiceType
|
||||||
instanceTcs.SetException(new UnloadedException());
|
instanceTcs.SetException(new UnloadedException());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<object?> ResolveServiceFromTypeAsync(Type type)
|
|
||||||
{
|
|
||||||
var task = (Task)typeof(Service<>)
|
|
||||||
.MakeGenericType(type)
|
|
||||||
.InvokeMember(
|
|
||||||
"GetAsync",
|
|
||||||
BindingFlags.InvokeMethod |
|
|
||||||
BindingFlags.Static |
|
|
||||||
BindingFlags.Public,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null)!;
|
|
||||||
await task;
|
|
||||||
return typeof(Task<>).MakeGenericType(type)
|
|
||||||
.GetProperty("Result", BindingFlags.Instance | BindingFlags.Public)!
|
|
||||||
.GetValue(task);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ConstructorInfo? GetServiceConstructor()
|
private static ConstructorInfo? GetServiceConstructor()
|
||||||
{
|
{
|
||||||
const BindingFlags ctorBindingFlags =
|
const BindingFlags ctorBindingFlags =
|
||||||
|
|
@ -359,18 +330,18 @@ internal static class Service<T> where T : IServiceType
|
||||||
.SingleOrDefault(x => x.GetCustomAttributes(typeof(ServiceManager.ServiceConstructor), true).Any());
|
.SingleOrDefault(x => x.GetCustomAttributes(typeof(ServiceManager.ServiceConstructor), true).Any());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<T> ConstructObject()
|
private static async Task<T> ConstructObject(IReadOnlyCollection<object> additionalProvidedTypedObjects)
|
||||||
{
|
{
|
||||||
var ctor = GetServiceConstructor();
|
var ctor = GetServiceConstructor();
|
||||||
if (ctor == null)
|
if (ctor == null)
|
||||||
throw new Exception($"Service \"{typeof(T).FullName}\" had no applicable constructor");
|
throw new Exception($"Service \"{typeof(T).FullName}\" had no applicable constructor");
|
||||||
|
|
||||||
var args = await Task.WhenAll(
|
var args = await ResolveInjectedParameters(ctor.GetParameters(), additionalProvidedTypedObjects)
|
||||||
ctor.GetParameters().Select(x => ResolveServiceFromTypeAsync(x.ParameterType)));
|
.ConfigureAwait(false);
|
||||||
using (Timings.Start($"{typeof(T).Name} Construct"))
|
using (Timings.Start($"{typeof(T).Name} Construct"))
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
ServiceManager.CurrentConstructorServiceType.Value = typeof(Service<T>);
|
ServiceManager.CurrentConstructorServiceType.Value = typeof(T);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return (T)ctor.Invoke(args)!;
|
return (T)ctor.Invoke(args)!;
|
||||||
|
|
@ -385,6 +356,43 @@ internal static class Service<T> where T : IServiceType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Task<object[]> ResolveInjectedParameters(
|
||||||
|
IReadOnlyList<ParameterInfo> argDefs,
|
||||||
|
IReadOnlyCollection<object> additionalProvidedTypedObjects)
|
||||||
|
{
|
||||||
|
var argTasks = new Task<object>[argDefs.Count];
|
||||||
|
for (var i = 0; i < argDefs.Count; i++)
|
||||||
|
{
|
||||||
|
var argType = argDefs[i].ParameterType;
|
||||||
|
ref var argTask = ref argTasks[i];
|
||||||
|
|
||||||
|
if (argType.GetCustomAttribute<ServiceManager.InjectableTypeAttribute>() is not null)
|
||||||
|
{
|
||||||
|
argTask = Task.FromResult(additionalProvidedTypedObjects.Single(x => x.GetType() == argType));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
argTask = (Task<object>)typeof(Service<>)
|
||||||
|
.MakeGenericType(argType)
|
||||||
|
.InvokeMember(
|
||||||
|
nameof(GetAsyncAsObject),
|
||||||
|
BindingFlags.InvokeMethod |
|
||||||
|
BindingFlags.Static |
|
||||||
|
BindingFlags.NonPublic,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.WhenAll(argTasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pull the instance out of the service locator, waiting if necessary.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The object.</returns>
|
||||||
|
private static Task<object> GetAsyncAsObject() => instanceTcs.Task.ContinueWith(r => (object)r.Result);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exception thrown when service is attempted to be retrieved when it's unloaded.
|
/// Exception thrown when service is attempted to be retrieved when it's unloaded.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -407,11 +415,12 @@ internal static class ServiceHelpers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a list of dependencies for a service. Only accepts <see cref="Service{T}"/> types.
|
/// Get a list of dependencies for a service. Only accepts <see cref="Service{T}"/> types.
|
||||||
/// These are returned as <see cref="Service{T}"/> types.
|
/// These are NOT returned as <see cref="Service{T}"/> types; raw types will be returned.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serviceType">The dependencies for this service.</param>
|
/// <param name="serviceType">The dependencies for this service.</param>
|
||||||
|
/// <param name="includeUnloadDependencies">Whether to include the unload dependencies.</param>
|
||||||
/// <returns>A list of dependencies.</returns>
|
/// <returns>A list of dependencies.</returns>
|
||||||
public static List<Type> GetDependencies(Type serviceType)
|
public static IReadOnlyCollection<Type> GetDependencies(Type serviceType, bool includeUnloadDependencies)
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
if (!serviceType.IsGenericType || serviceType.GetGenericTypeDefinition() != typeof(Service<>))
|
if (!serviceType.IsGenericType || serviceType.GetGenericTypeDefinition() != typeof(Service<>))
|
||||||
|
|
@ -422,12 +431,12 @@ internal static class ServiceHelpers
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return (List<Type>)serviceType.InvokeMember(
|
return (IReadOnlyCollection<Type>)serviceType.InvokeMember(
|
||||||
nameof(Service<IServiceType>.GetDependencyServices),
|
nameof(Service<IServiceType>.GetDependencyServices),
|
||||||
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
|
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null) ?? new List<Type>();
|
new object?[] { includeUnloadDependencies }) ?? new List<Type>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,10 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
|
||||||
private bool isDisposed;
|
private bool isDisposed;
|
||||||
|
|
||||||
[ServiceManager.ServiceConstructor]
|
[ServiceManager.ServiceConstructor]
|
||||||
private DalamudAssetManager(Dalamud dalamud, HappyHttpClient httpClient)
|
private DalamudAssetManager(
|
||||||
|
Dalamud dalamud,
|
||||||
|
HappyHttpClient httpClient,
|
||||||
|
ServiceManager.RegisterStartupBlockerDelegate registerStartupBlocker)
|
||||||
{
|
{
|
||||||
this.dalamud = dalamud;
|
this.dalamud = dalamud;
|
||||||
this.httpClient = httpClient;
|
this.httpClient = httpClient;
|
||||||
|
|
@ -55,8 +58,17 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
|
||||||
this.fileStreams = Enum.GetValues<DalamudAsset>().ToDictionary(x => x, _ => (Task<FileStream>?)null);
|
this.fileStreams = Enum.GetValues<DalamudAsset>().ToDictionary(x => x, _ => (Task<FileStream>?)null);
|
||||||
this.textureWraps = Enum.GetValues<DalamudAsset>().ToDictionary(x => x, _ => (Task<IDalamudTextureWrap>?)null);
|
this.textureWraps = Enum.GetValues<DalamudAsset>().ToDictionary(x => x, _ => (Task<IDalamudTextureWrap>?)null);
|
||||||
|
|
||||||
|
// Block until all the required assets to be ready.
|
||||||
var loadTimings = Timings.Start("DAM LoadAll");
|
var loadTimings = Timings.Start("DAM LoadAll");
|
||||||
this.WaitForAllRequiredAssets().ContinueWith(_ => loadTimings.Dispose());
|
registerStartupBlocker(
|
||||||
|
Task.WhenAll(
|
||||||
|
Enum.GetValues<DalamudAsset>()
|
||||||
|
.Where(x => x is not DalamudAsset.Empty4X4)
|
||||||
|
.Where(x => x.GetAttribute<DalamudAssetAttribute>()?.Required is true)
|
||||||
|
.Select(this.CreateStreamAsync)
|
||||||
|
.Select(x => x.ToContentDisposedTask()))
|
||||||
|
.ContinueWith(_ => loadTimings.Dispose()),
|
||||||
|
"Prevent Dalamud from loading more stuff, until we've ensured that all required assets are available.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
|
@ -83,25 +95,6 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
|
||||||
this.scopedFinalizer.Dispose();
|
this.scopedFinalizer.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Waits for all the required assets to be ready. Will result in a faulted task, if any of the required assets
|
|
||||||
/// has failed to load.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The task.</returns>
|
|
||||||
[Pure]
|
|
||||||
public Task WaitForAllRequiredAssets()
|
|
||||||
{
|
|
||||||
lock (this.syncRoot)
|
|
||||||
{
|
|
||||||
return Task.WhenAll(
|
|
||||||
Enum.GetValues<DalamudAsset>()
|
|
||||||
.Where(x => x is not DalamudAsset.Empty4X4)
|
|
||||||
.Where(x => x.GetAttribute<DalamudAssetAttribute>()?.Required is true)
|
|
||||||
.Select(this.CreateStreamAsync)
|
|
||||||
.Select(x => x.ToContentDisposedTask()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
[Pure]
|
[Pure]
|
||||||
public bool IsStreamImmediatelyAvailable(DalamudAsset asset) =>
|
public bool IsStreamImmediatelyAvailable(DalamudAsset asset) =>
|
||||||
|
|
|
||||||
|
|
@ -87,4 +87,14 @@ internal static class ArrayExtensions
|
||||||
result = default;
|
result = default;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interprets the given array as an <see cref="IReadOnlyCollection{T}"/>, so that you can enumerate it multiple
|
||||||
|
/// times, and know the number of elements within.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="array">The enumerable.</param>
|
||||||
|
/// <typeparam name="T">The element type.</typeparam>
|
||||||
|
/// <returns><paramref name="array"/> casted as a <see cref="IReadOnlyCollection{T}"/> if it is one; otherwise the result of <see cref="Enumerable.ToArray{TSource}"/>.</returns>
|
||||||
|
public static IReadOnlyCollection<T> AsReadOnlyCollection<T>(this IEnumerable<T> array) =>
|
||||||
|
array as IReadOnlyCollection<T> ?? array.ToArray();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit cc668752416a8459a3c23345c51277e359803de8
|
Subproject commit 3364dfea769b79e43aebaa955b6b98ec1d6eb458
|
||||||
Loading…
Add table
Add a link
Reference in a new issue