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:
goat 2023-12-16 21:17:16 +01:00 committed by GitHub
commit 68ffaf2440
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 3619 additions and 915 deletions

View file

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

View file

@ -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() {

View file

@ -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();

View file

@ -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))

View file

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

View file

@ -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,
}; };

View file

@ -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" />

View file

@ -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();

View file

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

View file

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

View file

@ -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();
} }

View file

@ -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();
} }

View file

@ -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();
} }

View file

@ -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();
} }

View file

@ -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();
} }

View file

@ -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();
} }

View file

@ -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();
} }

View file

@ -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.");
}
} }
} }

View file

@ -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.");
}
} }
} }

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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;

View file

@ -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)

View file

@ -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));

View file

@ -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+201p
.text:00000001405CD210 ; sub_140141D10+220p ...
.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

View file

@ -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,

View file

@ -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);

View file

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

View file

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

View file

@ -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>();

View file

@ -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)
{ {

View 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);
}
}
}
}

View 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,
}

View 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);
}
}

View 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,
}

View file

@ -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})";
}

View file

@ -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})";
}

View file

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

View file

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

View file

@ -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();
}

View file

@ -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})";
}

View file

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

View file

@ -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();
}

View file

@ -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;

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

View file

@ -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(() =>
{ {

View file

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

View file

@ -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(),

View file

@ -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}");
}
}
}

View file

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

View file

@ -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);
} }
} }
} }

View file

@ -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());
} }
} }

View file

@ -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);

View file

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

View file

@ -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);

View file

@ -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");
}
}
}

View file

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

View file

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

View file

@ -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);
} }

View file

@ -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.

View file

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

View file

@ -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()));
} }
} }

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

View file

@ -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&lt;T&gt;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
{ {
} }
} }

View file

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

View file

@ -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) =>

View file

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