Merge remote-tracking branch 'origin/master' into net8-rollup

This commit is contained in:
github-actions[bot] 2023-12-16 20:06:06 +00:00
commit c993be9c97
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_multiple_declaration = true
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_wrap_after_declaration_lpar = true
resharper_csharp_wrap_after_invocation_lpar = true
resharper_csharp_wrap_arguments_style = chop_if_long
resharper_enforce_line_ending_style = true
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_local_highlighting = none
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.NoExceptionHandlers = json.value("NoExceptionHandlers", config.NoExceptionHandlers);
}
void DalamudStartInfo::from_envvars() {

View file

@ -49,6 +49,7 @@ struct DalamudStartInfo {
std::set<std::string> BootUnhookDlls{};
bool CrashHandlerShow = false;
bool NoExceptionHandlers = false;
friend void from_json(const nlohmann::json&, DalamudStartInfo&);
void from_envvars();

View file

@ -133,7 +133,9 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
// ============================== 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");
} else if (g_startInfo.BootVehEnabled) {
if (veh::add_handler(g_startInfo.BootVehFull, g_startInfo.WorkingDirectory))

View file

@ -17,38 +17,6 @@ public record DalamudStartInfo
// 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>
/// Gets or sets the working directory of the XIVLauncher installations.
/// </summary>
@ -169,4 +137,9 @@ public record DalamudStartInfo
/// Gets or sets a value indicating whether to show crash handler console window.
/// </summary>
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-3rd-plugin");
args.Remove("--crash-handler-console");
args.Remove("--no-exception-handlers");
var mainCommand = args[1].ToLowerInvariant();
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.BootUnhookDlls = new List<string>() { "kernel32.dll", "ntdll.dll", "user32.dll" };
startInfo.CrashHandlerShow = args.Contains("--crash-handler-console");
startInfo.NoExceptionHandlers = args.Contains("--no-exception-handlers");
return startInfo;
}
@ -434,7 +436,7 @@ namespace Dalamud.Injector
Console.WriteLine("Verbose logging:\t[-v]");
Console.WriteLine("Show Console:\t[--console] [--crash-handler-console]");
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("No plugins:\t[--no-plugin] [--no-3rd-plugin]");
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 gameVer = GameVersion.Parse(gameVerStr);
return new DalamudStartInfo(startInfo)
return startInfo with
{
GameVersion = gameVer,
};

View file

@ -8,7 +8,7 @@
</PropertyGroup>
<PropertyGroup Label="Feature">
<DalamudVersion>9.0.0.13</DalamudVersion>
<DalamudVersion>9.0.0.14</DalamudVersion>
<Description>XIV Launcher addon framework</Description>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version>
@ -89,6 +89,7 @@
<PackageReference Include="System.Reactive" Version="5.0.0" />
<PackageReference Include="System.Reflection.MetadataLoadContext" 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>
<ProjectReference Include="..\Dalamud.Common\Dalamud.Common.csproj" />

View file

@ -147,7 +147,8 @@ public sealed class EntryPoint
LogLevelSwitch.MinimumLevel = configuration.LogLevel;
// Log any unhandled exception.
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
if (!info.NoExceptionHandlers)
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
var unloadFailed = false;
@ -196,7 +197,8 @@ public sealed class EntryPoint
finally
{
TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException;
AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException;
if (!info.NoExceptionHandlers)
AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException;
Log.Information("Session has ended.");
Log.CloseAndFlush();

View file

@ -9,6 +9,8 @@ using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
@ -31,6 +33,9 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
[ServiceManager.ServiceDependency]
private readonly AddonLifecycle addonLifecycle = Service<AddonLifecycle>.Get();
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private readonly AddonLifecycleEventListener finalizeEventListener;
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.addonLifecycle.RegisterListener(this.finalizeEventListener);
this.onUpdateCursor.Enable();
}
private delegate nint UpdateCursorDelegate(RaptureAtkModule* module);
@ -85,6 +92,8 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
/// <returns>IAddonEventHandle used to remove the event.</returns>
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)
{
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>
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)
{
eventController.RemoveEvent(eventHandle);
@ -128,11 +139,14 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
/// <param name="pluginId">Unique ID for this plugin.</param>
internal void AddPluginEventController(string pluginId)
{
if (this.pluginEventControllers.All(entry => entry.PluginId != pluginId))
this.framework.RunOnFrameworkThread(() =>
{
Log.Verbose($"Creating new PluginEventController for: {pluginId}");
this.pluginEventControllers.Add(new PluginEventController(pluginId));
}
if (this.pluginEventControllers.All(entry => entry.PluginId != pluginId))
{
Log.Verbose($"Creating new PluginEventController for: {pluginId}");
this.pluginEventControllers.Add(new PluginEventController(pluginId));
}
});
}
/// <summary>
@ -141,18 +155,15 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
/// <param name="pluginId">Unique ID for this plugin.</param>
internal void RemovePluginEventController(string pluginId)
{
if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } controller)
this.framework.RunOnFrameworkThread(() =>
{
Log.Verbose($"Removing PluginEventController for: {pluginId}");
this.pluginEventControllers.Remove(controller);
controller.Dispose();
}
}
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction()
{
this.onUpdateCursor.Enable();
if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } controller)
{
Log.Verbose($"Removing PluginEventController for: {pluginId}");
this.pluginEventControllers.Remove(controller);
controller.Dispose();
}
});
}
/// <summary>

View file

@ -1,4 +1,5 @@
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Component.GUI;
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.
/// </summary>
public const string InvalidAddon = "NullAddon";
private string? addonName;
private IntPtr addon;
/// <summary>
/// Gets the name of the addon this args referrers to.
/// </summary>
public string AddonName => this.GetAddonName();
/// <summary>
/// Gets the pointer to the addons AtkUnitBase.
/// </summary>
public nint Addon { get; init; }
public nint Addon
{
get => this.AddonInternal;
init => this.AddonInternal = value;
}
/// <summary>
/// Gets the type of these args.
/// </summary>
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>
/// Helper method for ensuring the name of the addon is valid.
/// </summary>

View file

@ -3,8 +3,22 @@
/// <summary>
/// Addon argument data for Draw events.
/// </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/>
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>
/// Addon argument data for ReceiveEvent events.
/// </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/>
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>
/// Addon argument data for ReceiveEvent events.
/// </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/>
public override AddonArgsType Type => AddonArgsType.ReceiveEvent;
/// <summary>
/// Gets the AtkEventType for this event message.
/// Gets or sets the AtkEventType for this event message.
/// </summary>
public byte AtkEventType { get; init; }
public byte AtkEventType { get; set; }
/// <summary>
/// Gets the event id for this event message.
/// Gets or sets the event id for this event message.
/// </summary>
public int EventParam { get; init; }
public int EventParam { get; set; }
/// <summary>
/// Gets the pointer to an AtkEvent for this event message.
/// Gets or sets the pointer to an AtkEvent for this event message.
/// </summary>
public nint AtkEvent { get; init; }
public nint AtkEvent { get; set; }
/// <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>
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>
/// Addon argument data for Refresh events.
/// </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/>
public override AddonArgsType Type => AddonArgsType.Refresh;
/// <summary>
/// Gets the number of AtkValues.
/// Gets or sets the number of AtkValues.
/// </summary>
public uint AtkValueCount { get; init; }
public uint AtkValueCount { get; set; }
/// <summary>
/// Gets the address of the AtkValue array.
/// Gets or sets the address of the AtkValue array.
/// </summary>
public nint AtkValues { get; init; }
public nint AtkValues { get; set; }
/// <summary>
/// Gets the AtkValues in the form of a span.
/// </summary>
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>
/// Addon argument data for OnRequestedUpdate events.
/// </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/>
public override AddonArgsType Type => AddonArgsType.RequestedUpdate;
/// <summary>
/// Gets the NumberArrayData** for this event.
/// Gets or sets the NumberArrayData** for this event.
/// </summary>
public nint NumberArrayData { get; init; }
public nint NumberArrayData { get; set; }
/// <summary>
/// Gets the StringArrayData** for this event.
/// Gets or sets the StringArrayData** for this event.
/// </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>
/// Addon argument data for Setup events.
/// </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/>
public override AddonArgsType Type => AddonArgsType.Setup;
/// <summary>
/// Gets the number of AtkValues.
/// Gets or sets the number of AtkValues.
/// </summary>
public uint AtkValueCount { get; init; }
public uint AtkValueCount { get; set; }
/// <summary>
/// Gets the address of the AtkValue array.
/// Gets or sets the address of the AtkValue array.
/// </summary>
public nint AtkValues { get; init; }
public nint AtkValues { get; set; }
/// <summary>
/// Gets the AtkValues in the form of a span.
/// </summary>
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>
/// Addon argument data for Update events.
/// </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/>
public override AddonArgsType Type => AddonArgsType.Update;
/// <summary>
/// Gets the time since the last update.
/// </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.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Hooking;
@ -37,8 +38,17 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
private readonly Hook<AddonOnRefreshDelegate> onAddonRefreshHook;
private readonly CallHook<AddonOnRequestedUpdateDelegate> onAddonRequestedUpdateHook;
private readonly ConcurrentBag<AddonLifecycleEventListener> newEventListeners = new();
private readonly ConcurrentBag<AddonLifecycleEventListener> removeEventListeners = new();
// 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 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]
private AddonLifecycle(TargetSigScanner sigScanner)
@ -48,8 +58,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
// We want value of the function pointer at vFunc[2]
this.disallowedReceiveEventAddress = ((nint*)this.address.AtkEventListener)![2];
this.framework.Update += this.OnFrameworkUpdate;
this.onAddonSetupHook = new CallHook<AddonSetupDelegate>(this.address.AddonSetup, 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.onAddonRefreshHook = Hook<AddonOnRefreshDelegate>.FromAddress(this.address.AddonOnRefresh, this.OnAddonRefresh);
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);
@ -85,8 +101,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
/// <inheritdoc/>
public void Dispose()
{
this.framework.Update -= this.OnFrameworkUpdate;
this.onAddonSetupHook.Dispose();
this.onAddonSetup2Hook.Dispose();
this.onAddonFinalizeHook.Dispose();
@ -107,7 +121,20 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
/// <param name="listener">The listener to register.</param>
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>
@ -116,7 +143,24 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
/// <param name="listener">The listener to unregister.</param>
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>
@ -124,75 +168,30 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
/// </summary>
/// <param name="eventType">Event Type.</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.
foreach (var listener in this.EventListeners.Where(listener => listener.EventType == eventType && (listener.AddonName == args.AddonName || listener.AddonName == string.Empty)))
// Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better.
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.
private void OnFrameworkUpdate(IFramework unused)
{
if (this.newEventListeners.Any())
{
foreach (var toAddListener in this.newEventListeners)
// Match on string.empty for listeners that want events for all addons.
if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName))
continue;
try
{
this.EventListeners.Add(toAddListener);
// 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();
}
}
listener.FunctionDelegate.Invoke(eventType, args);
}
this.newEventListeners.Clear();
}
if (this.removeEventListeners.Any())
{
foreach (var toRemoveListener in this.removeEventListeners)
catch (Exception e)
{
this.EventListeners.Remove(toRemoveListener);
// 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();
}
}
}
Log.Error(e, $"Exception in {blame} during {eventType} invoke.");
}
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)
{
// 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.");
}
try
{
this.InvokeListeners(AddonEvent.PreSetup, new AddonSetupArgs
{
Addon = (nint)addon,
AtkValueCount = valueCount,
AtkValues = (nint)values,
});
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonSetup pre-setup invoke.");
}
this.recyclingSetupArgs.AddonInternal = (nint)addon;
this.recyclingSetupArgs.AtkValueCount = valueCount;
this.recyclingSetupArgs.AtkValues = (nint)values;
this.InvokeListenersSafely(AddonEvent.PreSetup, this.recyclingSetupArgs);
valueCount = this.recyclingSetupArgs.AtkValueCount;
values = (AtkValue*)this.recyclingSetupArgs.AtkValues;
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.");
}
try
{
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.");
}
this.InvokeListenersSafely(AddonEvent.PostSetup, this.recyclingSetupArgs);
}
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.");
}
try
{
this.InvokeListeners(AddonEvent.PreFinalize, new AddonFinalizeArgs { Addon = (nint)atkUnitBase[0] });
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonFinalize pre-finalize invoke.");
}
this.recyclingFinalizeArgs.AddonInternal = (nint)atkUnitBase[0];
this.InvokeListenersSafely(AddonEvent.PreFinalize, this.recyclingFinalizeArgs);
try
{
@ -325,14 +299,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
private void OnAddonDraw(AtkUnitBase* addon)
{
try
{
this.InvokeListeners(AddonEvent.PreDraw, new AddonDrawArgs { Addon = (nint)addon });
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonDraw pre-draw invoke.");
}
this.recyclingDrawArgs.AddonInternal = (nint)addon;
this.InvokeListenersSafely(AddonEvent.PreDraw, this.recyclingDrawArgs);
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.");
}
try
{
this.InvokeListeners(AddonEvent.PostDraw, new AddonDrawArgs { Addon = (nint)addon });
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonDraw post-draw invoke.");
}
this.InvokeListenersSafely(AddonEvent.PostDraw, this.recyclingDrawArgs);
}
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
{
try
{
this.InvokeListeners(AddonEvent.PreUpdate, new AddonUpdateArgs { Addon = (nint)addon, TimeDelta = delta });
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonUpdate pre-update invoke.");
}
this.recyclingUpdateArgs.AddonInternal = (nint)addon;
this.recyclingUpdateArgs.TimeDeltaInternal = delta;
this.InvokeListenersSafely(AddonEvent.PreUpdate, this.recyclingUpdateArgs);
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.");
}
try
{
this.InvokeListeners(AddonEvent.PostUpdate, new AddonUpdateArgs { Addon = (nint)addon, TimeDelta = delta });
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonUpdate post-update invoke.");
}
this.InvokeListenersSafely(AddonEvent.PostUpdate, this.recyclingUpdateArgs);
}
private byte OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values)
{
byte result = 0;
try
{
this.InvokeListeners(AddonEvent.PreRefresh, new AddonRefreshArgs
{
Addon = (nint)addon,
AtkValueCount = valueCount,
AtkValues = (nint)values,
});
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonRefresh pre-refresh invoke.");
}
this.recyclingRefreshArgs.AddonInternal = (nint)addon;
this.recyclingRefreshArgs.AtkValueCount = valueCount;
this.recyclingRefreshArgs.AtkValues = (nint)values;
this.InvokeListenersSafely(AddonEvent.PreRefresh, this.recyclingRefreshArgs);
valueCount = this.recyclingRefreshArgs.AtkValueCount;
values = (AtkValue*)this.recyclingRefreshArgs.AtkValues;
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.");
}
try
{
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.");
}
this.InvokeListenersSafely(AddonEvent.PostRefresh, this.recyclingRefreshArgs);
return result;
}
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
{
try
{
this.InvokeListeners(AddonEvent.PreRequestedUpdate, new AddonRequestedUpdateArgs
{
Addon = (nint)addon,
NumberArrayData = (nint)numberArrayData,
StringArrayData = (nint)stringArrayData,
});
}
catch (Exception e)
{
Log.Error(e, "Exception in OnRequestedUpdate pre-requestedUpdate invoke.");
}
this.recyclingRequestedUpdateArgs.AddonInternal = (nint)addon;
this.recyclingRequestedUpdateArgs.NumberArrayData = (nint)numberArrayData;
this.recyclingRequestedUpdateArgs.StringArrayData = (nint)stringArrayData;
this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.recyclingRequestedUpdateArgs);
numberArrayData = (NumberArrayData**)this.recyclingRequestedUpdateArgs.NumberArrayData;
stringArrayData = (StringArrayData**)this.recyclingRequestedUpdateArgs.StringArrayData;
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.");
}
try
{
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.");
}
this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.recyclingRequestedUpdateArgs);
}
}

View file

@ -16,6 +16,13 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
{
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>
/// Initializes a new instance of the <see cref="AddonLifecycleReceiveEventListener"/> class.
/// </summary>
@ -74,22 +81,17 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
this.Hook!.Original(addon, eventType, eventParam, atkEvent, data);
return;
}
try
{
this.AddonLifecycle.InvokeListeners(AddonEvent.PreReceiveEvent, new AddonReceiveEventArgs
{
Addon = (nint)addon,
AtkEventType = (byte)eventType,
EventParam = eventParam,
AtkEvent = (nint)atkEvent,
Data = data,
});
}
catch (Exception e)
{
Log.Error(e, "Exception in OnReceiveEvent pre-receiveEvent invoke.");
}
this.recyclingReceiveEventArgs.AddonInternal = (nint)addon;
this.recyclingReceiveEventArgs.AtkEventType = (byte)eventType;
this.recyclingReceiveEventArgs.EventParam = eventParam;
this.recyclingReceiveEventArgs.AtkEvent = (IntPtr)atkEvent;
this.recyclingReceiveEventArgs.Data = data;
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.recyclingReceiveEventArgs);
eventType = (AtkEventType)this.recyclingReceiveEventArgs.AtkEventType;
eventParam = this.recyclingReceiveEventArgs.EventParam;
atkEvent = (AtkEvent*)this.recyclingReceiveEventArgs.AtkEvent;
data = this.recyclingReceiveEventArgs.Data;
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.");
}
try
{
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.");
}
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.recyclingReceiveEventArgs);
}
}

View file

@ -58,6 +58,8 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState
this.framework.Update += this.FrameworkOnOnUpdateEvent;
this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop;
this.setupTerritoryTypeHook.Enable();
}
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
@ -120,12 +122,6 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState
this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop;
}
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction()
{
this.setupTerritoryTypeHook.Enable();
}
private IntPtr SetupTerritoryTypeDetour(IntPtr manager, ushort 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.
/// </summary>
internal const int MaxConditionEntries = 104;
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private readonly bool[] cache = new bool[MaxConditionEntries];
@ -24,6 +27,12 @@ internal sealed partial class Condition : IServiceType, ICondition
{
var resolver = clientState.AddressResolver;
this.Address = resolver.ConditionFlags;
// Initialization
for (var i = 0; i < MaxConditionEntries; i++)
this.cache[i] = this[i];
this.framework.Update += this.FrameworkUpdate;
}
/// <inheritdoc/>
@ -80,17 +89,7 @@ internal sealed partial class Condition : IServiceType, ICondition
return false;
}
[ServiceManager.CallWhenServicesReady]
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)
private void FrameworkUpdate(IFramework unused)
{
for (var i = 0; i < MaxConditionEntries; i++)
{
@ -144,7 +143,7 @@ internal sealed partial class Condition : IDisposable
if (disposing)
{
Service<Framework>.Get().Update -= this.FrameworkUpdate;
this.framework.Update -= this.FrameworkUpdate;
}
this.isDisposed = true;

View file

@ -38,6 +38,7 @@ internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState
var resolver = clientState.AddressResolver;
Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}");
this.gamepadPoll = Hook<ControllerPoll>.FromAddress(resolver.GamepadPoll, this.GamepadPollDetour);
this.gamepadPoll?.Enable();
}
private delegate int ControllerPoll(IntPtr controllerInput);
@ -114,12 +115,6 @@ internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState
GC.SuppressFinalize(this);
}
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction()
{
this.gamepadPoll?.Enable();
}
private int GamepadPollDetour(IntPtr 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.clientState.TerritoryChanged += this.TerritoryOnChangedEvent;
this.contentDirectorNetworkMessageHook.Enable();
}
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
@ -67,12 +69,6 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState
this.clientState.TerritoryChanged -= this.TerritoryOnChangedEvent;
}
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction()
{
this.contentDirectorNetworkMessageHook.Enable();
}
private byte ContentDirectorNetworkMessageDetour(IntPtr a1, IntPtr a2, ushort* 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.destroyHook = Hook<OnRealDestroyDelegate>.FromAddress(this.addressResolver.DestroyAddress, this.HandleFrameworkDestroy);
this.updateHook.Enable();
this.destroyHook.Enable();
}
/// <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()
{
if (this.runOnNextTickTaskList.Count == 0 && this.runOnNextTickTaskList2.Count == 0)

View file

@ -1,29 +1,38 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.InteropServices;
using Dalamud.Configuration.Internal;
using Dalamud.Game.Libc;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Memory;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Serilog;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
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>
/// This class handles interacting with the native chat UI.
/// </summary>
[InterfaceVersion("1.0")]
[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 Queue<XivChatEntry> chatQueue = new();
@ -36,10 +45,7 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
[ServiceManager.ServiceDependency]
private readonly LibcFunction libcFunction = Service<LibcFunction>.Get();
private IntPtr baseAddress = IntPtr.Zero;
private ImmutableDictionary<(string PluginName, uint CommandId), Action<uint, SeString>>? dalamudLinkHandlersCopy;
[ServiceManager.ServiceConstructor]
private ChatGui(TargetSigScanner sigScanner)
@ -47,13 +53,17 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
this.address = new ChatGuiAddressResolver();
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.interactableLinkClickedHook = Hook<InteractableLinkClickedDelegate>.FromAddress(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour);
this.printMessageHook.Enable();
this.populateItemLinkHook.Enable();
this.interactableLinkClickedHook.Enable();
}
[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)]
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; }
/// <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>
/// Dispose of managed and unmanaged resources.
@ -131,18 +155,13 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
{
var chat = this.chatQueue.Dequeue();
if (this.baseAddress == IntPtr.Zero)
{
continue;
}
var sender = Utf8String.FromSequence(chat.Name.Encode());
var message = Utf8String.FromSequence(chat.Message.Encode());
var senderRaw = (chat.Name ?? string.Empty).Encode();
using var senderOwned = this.libcFunction.NewString(senderRaw);
this.HandlePrintMessageDetour(RaptureLogModule.Instance(), chat.Type, sender, message, (int)chat.SenderId, (byte)(chat.Parameters != 0 ? 1 : 0));
var messageRaw = (chat.Message ?? string.Empty).Encode();
using var messageOwned = this.libcFunction.NewString(messageRaw);
this.HandlePrintMessageDetour(this.baseAddress, chat.Type, senderOwned.Address, messageOwned.Address, chat.SenderId, chat.Parameters);
sender->Dtor(true);
message->Dtor(true);
}
}
@ -156,7 +175,12 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
internal DalamudLinkPayload AddChatLinkHandler(string pluginName, uint commandId, Action<uint, SeString> commandAction)
{
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;
}
@ -166,9 +190,14 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
/// <param name="pluginName">The name of the plugin handling the links.</param>
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>
internal void RemoveChatLinkHandler(string pluginName, uint commandId)
{
this.dalamudLinkHandlers.Remove((pluginName, commandId));
}
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction()
{
this.printMessageHook.Enable();
this.populateItemLinkHook.Enable();
this.interactableLinkClickedHook.Enable();
lock (this.dalamudLinkHandlers)
{
if (this.dalamudLinkHandlers.Remove((pluginName, commandId)))
this.dalamudLinkHandlersCopy = null;
}
}
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
{
var sender = StdString.ReadFromPointer(pSenderName);
var parsedSender = SeString.Parse(sender.RawData);
var originalSenderData = (byte[])sender.RawData.Clone();
var oldEditedSender = parsedSender.Encode();
var senderPtr = pSenderName;
OwnedStdString allocatedString = null;
var originalSenderData = sender->AsSpan().ToArray();
var originalMessageData = message->AsSpan().ToArray();
var message = StdString.ReadFromPointer(pMessage);
var parsedMessage = SeString.Parse(message.RawData);
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}");
var parsedSender = SeString.Parse(originalSenderData);
var parsedMessage = SeString.Parse(originalMessageData);
// Call events
var isHandled = false;
@ -287,7 +300,7 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
try
{
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)
{
@ -303,7 +316,7 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
try
{
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)
{
@ -312,61 +325,39 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
}
}
var newEdited = parsedMessage.Encode();
if (!Util.FastByteArrayCompare(oldEdited, newEdited))
var possiblyModifiedSenderData = parsedSender.Encode();
var possiblyModifiedMessageData = parsedMessage.Encode();
if (!Util.FastByteArrayCompare(originalSenderData, possiblyModifiedSenderData))
{
Log.Verbose("SeString was edited, taking precedence over StdString edit.");
message.RawData = newEdited;
// Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}");
Log.Verbose($"HandlePrintMessageDetour Sender modified: {SeString.Parse(originalSenderData)} -> {parsedSender}");
sender->SetString(possiblyModifiedSenderData);
}
if (!Util.FastByteArrayCompare(originalMessageData, message.RawData))
if (!Util.FastByteArrayCompare(originalMessageData, possiblyModifiedMessageData))
{
allocatedString = this.libcFunction.NewString(message.RawData);
Log.Debug($"HandlePrintMessageDetour String modified: {originalMessageData}({messagePtr}) -> {message}({allocatedString.Address})");
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;
Log.Verbose($"HandlePrintMessageDetour Message modified: {SeString.Parse(originalMessageData)} -> {parsedMessage}");
message->SetString(possiblyModifiedMessageData);
}
// Print the original chat if it's handled.
if (isHandled)
{
this.ChatMessageHandled?.Invoke(chatType, senderId, parsedSender, parsedMessage);
this.ChatMessageHandled?.Invoke(chatType, (uint)timestamp, parsedSender, parsedMessage);
}
else
{
retVal = this.printMessageHook.Original(manager, chatType, senderPtr, messagePtr, senderId, parameter);
this.ChatMessageUnhandled?.Invoke(chatType, senderId, parsedSender, parsedMessage);
messageId = this.printMessageHook.Original(manager, chatType, sender, message, timestamp, silent);
this.ChatMessageUnhandled?.Invoke(chatType, (uint)timestamp, parsedSender, parsedMessage);
}
if (this.baseAddress == IntPtr.Zero)
this.baseAddress = manager;
allocatedString?.Dispose();
allocatedStringSender?.Dispose();
}
catch (Exception ex)
{
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)
@ -384,18 +375,14 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}");
var payloadPtr = Marshal.ReadIntPtr(messagePtr, 0x10);
var messageSize = 0;
while (Marshal.ReadByte(payloadPtr, messageSize) != 0) messageSize++;
var payloadBytes = new byte[messageSize];
Marshal.Copy(payloadPtr, payloadBytes, 0, messageSize);
var seStr = SeString.Parse(payloadBytes);
var seStr = MemoryHelper.ReadSeStringNullTerminated(payloadPtr);
var terminatorIndex = seStr.Payloads.IndexOf(RawPayload.LinkTerminator);
var payloads = terminatorIndex >= 0 ? seStr.Payloads.Take(terminatorIndex + 1).ToList() : seStr.Payloads;
if (payloads.Count == 0) return;
var linkPayload = payloads[0];
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}");
value.Invoke(link.CommandId, new SeString(payloads));

View file

@ -5,11 +5,6 @@ namespace Dalamud.Game.Gui;
/// </summary>
internal sealed class ChatGuiAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the native PrintMessage method.
/// </summary>
public IntPtr PrintMessage { get; private set; }
/// <summary>
/// Gets the address of the native PopulateItemLinkObject method.
/// </summary>
@ -20,77 +15,9 @@ internal sealed class ChatGuiAddressResolver : BaseAddressResolver
/// </summary>
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/>
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 ?? ?? 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.createFlyTextHook = Hook<CreateFlyTextDelegate>.FromAddress(this.Address.CreateFlyText, this.CreateFlyTextDetour);
this.createFlyTextHook.Enable();
}
/// <summary>
@ -143,12 +145,6 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui
return terminated;
}
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction(GameGui gameGui)
{
this.createFlyTextHook.Enable();
}
private IntPtr CreateFlyTextDetour(
IntPtr addonFlyText,
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.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
@ -376,19 +385,6 @@ internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui
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)
{
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)
{
try

View file

@ -35,6 +35,7 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu
this.memory = Marshal.AllocHGlobal(PartyFinderPacket.PacketSize);
this.receiveListingHook = Hook<ReceiveListingDelegate>.FromAddress(this.address.ReceiveListing, this.HandleReceiveListingDetour);
this.receiveListingHook.Enable();
}
[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)
{
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.showQuestToastHook = Hook<ShowQuestToastDelegate>.FromAddress(this.address.ShowQuestToast, this.HandleQuestToastDetour);
this.showErrorToastHook = Hook<ShowErrorToastDelegate>.FromAddress(this.address.ShowErrorToast, this.HandleErrorToastDetour);
this.showNormalToastHook.Enable();
this.showQuestToastHook.Enable();
this.showErrorToastHook.Enable();
}
#region Marshal delegates
@ -109,14 +113,6 @@ internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui
return terminated;
}
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction(GameGui gameGui)
{
this.showNormalToastHook.Enable();
this.showQuestToastHook.Enable();
this.showErrorToastHook.Enable();
}
private SeString ParseString(IntPtr text)
{
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.contextMenu.ContextMenuOpened += this.ContextMenuOnContextMenuOpened;
this.hookAgentHudOpenSystemMenu.Enable();
this.hookUiModuleRequestMainCommand.Enable();
this.hookAtkUnitBaseReceiveGlobalEvent.Enable();
}
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);
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction(DalamudInterface dalamudInterface)
{
this.hookAgentHudOpenSystemMenu.Enable();
this.hookUiModuleRequestMainCommand.Enable();
this.hookAtkUnitBaseReceiveGlobalEvent.Enable();
}
/*
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.processZonePacketUpHook = Hook<ProcessZonePacketUpDelegate>.FromAddress(this.address.ProcessZonePacketUp, this.ProcessZonePacketUpDetour);
this.processZonePacketDownHook.Enable();
this.processZonePacketUpHook.Enable();
}
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
@ -62,13 +65,6 @@ internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork
this.processZonePacketUpHook.Dispose();
}
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction()
{
this.processZonePacketDownHook.Enable();
this.processZonePacketUpHook.Enable();
}
private void ProcessZonePacketDownDetour(IntPtr a, uint targetId, IntPtr dataPtr)
{
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]
private void ContinueConstruction(
TargetSigScanner sigScanner,
DalamudAssetManager dalamudAssetManager,
DalamudConfiguration configuration)
[ServiceManager.CallWhenServicesReady(
"InterfaceManager accepts event registration and stuff even when the game window is not ready.")]
private void ContinueConstruction(TargetSigScanner sigScanner, DalamudConfiguration configuration)
{
dalamudAssetManager.WaitForAllRequiredAssets().Wait();
this.address.Setup(sigScanner);
this.framework.RunOnFrameworkThread(() =>
{

View file

@ -679,6 +679,9 @@ internal class ConsoleWindow : Window, IDisposable
private bool IsFilterApplicable(LogEntry entry)
{
if (this.regexError)
return false;
try
{
// 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 FlyTextWidget(),
new FontAwesomeTestWidget(),
new GameInventoryTestWidget(),
new GamepadWidget(),
new GaugeWidget(),
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 ImGuiNET;
using Newtonsoft.Json;
using Formatting = Newtonsoft.Json.Formatting;
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
/// <summary>
/// Widget for displaying plugin data share modules.
/// </summary>
[SuppressMessage(
"StyleCop.CSharp.LayoutRules",
"SA1519:Braces should not be omitted from multi-line child statement",
Justification = "Multiple fixed blocks")]
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/>
public string[]? CommandShortcuts { get; init; } = { "datashare" };
/// <inheritdoc/>
public string DisplayName { get; init; } = "Data Share";
public string DisplayName { get; init; } = "Data Share & Call Gate";
/// <inheritdoc/>
public bool Ready { get; set; }
@ -25,28 +50,290 @@ internal class DataShareWidget : IDataWindowWidget
}
/// <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;
try
{
ImGui.TableSetupColumn("Shared Tag");
ImGui.TableSetupColumn("Show");
ImGui.TableSetupColumn("Creator Assembly");
ImGui.TableSetupColumn("#", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Consumers");
ImGui.TableHeadersRow();
foreach (var share in Service<DataShare>.Get().GetAllShares())
{
ImGui.TableNextRow();
this.DrawTextCell(share.Tag, null, true);
ImGui.TableNextColumn();
ImGui.TextUnformatted(share.Tag);
ImGui.TableNextColumn();
ImGui.TextUnformatted(share.CreatorAssembly);
ImGui.TableNextColumn();
ImGui.TextUnformatted(share.Users.Length.ToString());
ImGui.TableNextColumn();
ImGui.TextUnformatted(string.Join(", ", share.Users));
if (ImGui.Button($"Show##datasharetable-show-{share.Tag}"))
{
var index = 0;
for (; index < this.dataView.Count; index++)
{
if (this.dataView[index].Name == share.Tag)
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

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.Utility;
@ -13,6 +15,13 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
/// </summary>
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/>
public string[]? CommandShortcuts { get; init; } = { "services" };
@ -33,27 +42,294 @@ internal class ServicesWidget : IDataWindowWidget
{
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);
var isPublic = instance.Key.IsPublic;
if (ImGui.Button("Clear selection"))
this.selectedNodes.Clear();
ImGui.BulletText($"{instance.Key.FullName} ({instance.Key.GetServiceKind()})");
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface))
ImGui.SameLine();
switch (this.includeUnloadDependencies)
{
ImGui.Text(hasInterface
? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}"
: "\t => NO INTERFACE!!!");
case true when ImGui.Button("Show load-time dependencies"):
this.includeUnloadDependencies = false;
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);
ImGui.Text("\t => PUBLIC!!!");
const uint rectBaseBorderColor = 0xFFFFFFFF;
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.IO;
using System.Linq;
using Dalamud.Data;
using Dalamud.Interface.Internal;
using Dalamud.Utility;
using ImGuiScene;
using Lumina.Data.Files;
using Lumina.Data.Parsing.Uld;
@ -155,20 +155,27 @@ public class UldWrapper : IDisposable
// Try to load HD textures first.
var hrPath = texturePath.Replace(".tex", "_hr1.tex");
var substitution = Service<TextureManager>.Get();
hrPath = substitution.GetSubstitutedPath(hrPath);
var hd = true;
var file = this.data.GetFile<TexFile>(hrPath);
if (file == null)
var tex = Path.IsPathRooted(hrPath)
? this.data.GameData.GetFileFromDisk<TexFile>(hrPath)
: this.data.GetFile<TexFile>(hrPath);
if (tex == null)
{
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.
if (file == null)
if (tex == 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>
/// <param name="initialCapacity">The initial capacity.</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)
{
@ -394,7 +394,7 @@ public unsafe struct ImVectorWrapper<T> : IList<T>, IList, IReadOnlyList<T>, IDi
}
/// <inheritdoc cref="List{T}.AddRange"/>
public void AddRange(Span<T> items)
public void AddRange(ReadOnlySpan<T> items)
{
this.EnsureCapacityExponential(this.LengthUnsafe + items.Length);
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>
/// <returns>Whether the capacity has been changed.</returns>
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>
/// 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)
throw new IndexOutOfRangeException();
this.EnsureCapacityExponential(this.CapacityUnsafe + 1);
this.EnsureCapacityExponential(this.LengthUnsafe + 1);
var num = this.LengthUnsafe - index;
Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + 1, num * sizeof(T), num * sizeof(T));
this.DataUnsafe[index] = item;
this.LengthUnsafe += 1;
}
/// <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));
foreach (var item in items)
this.DataUnsafe[index++] = item;
this.LengthUnsafe += count;
}
else
{
@ -543,14 +545,15 @@ public unsafe struct ImVectorWrapper<T> : IList<T>, IList, IReadOnlyList<T>, IDi
}
}
/// <inheritdoc cref="List{T}.AddRange"/>
public void InsertRange(int index, Span<T> items)
/// <inheritdoc cref="List{T}.InsertRange"/>
public void InsertRange(int index, ReadOnlySpan<T> items)
{
this.EnsureCapacityExponential(this.LengthUnsafe + items.Length);
var num = this.LengthUnsafe - index;
Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + items.Length, num * sizeof(T), num * sizeof(T));
foreach (var item in items)
this.DataUnsafe[index++] = item;
this.LengthUnsafe += items.Length;
}
/// <summary>
@ -558,15 +561,7 @@ public unsafe struct ImVectorWrapper<T> : IList<T>, IList, IReadOnlyList<T>, IDi
/// </summary>
/// <param name="index">The index.</param>
/// <param name="skipDestroyer">Whether to skip calling the destroyer function.</param>
public void RemoveAt(int index, bool skipDestroyer = false)
{
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));
}
public void RemoveAt(int index, bool skipDestroyer = false) => this.RemoveRange(index, 1, skipDestroyer);
/// <inheritdoc/>
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/>
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>
/// Sets the capacity exactly as requested.
/// </summary>
@ -611,9 +673,6 @@ public unsafe struct ImVectorWrapper<T> : IList<T>, IList, IReadOnlyList<T>, IDi
if (!oldSpan.IsEmpty && !newSpan.IsEmpty)
oldSpan[..this.LengthUnsafe].CopyTo(newSpan);
// #if DEBUG
// new Span<byte>(newAlloc + this.LengthUnsafe, sizeof(T) * (capacity - this.LengthUnsafe)).Fill(0xCC);
// #endif
if (oldAlloc != null)
ImGuiNative.igMemFree(oldAlloc);

View file

@ -163,6 +163,38 @@ public static unsafe class MemoryHelper
#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>
/// Read a UTF-8 encoded string from a specified memory address.
/// </summary>

View file

@ -21,6 +21,7 @@ using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Networking.Http;
@ -29,6 +30,7 @@ using Dalamud.Plugin.Internal.Profiles;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Internal.Types.Manifest;
using Dalamud.Plugin.Ipc.Internal;
using Dalamud.Support;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
using Newtonsoft.Json;
@ -93,7 +95,9 @@ internal partial class PluginManager : IDisposable, IServiceType
}
[ServiceManager.ServiceConstructor]
private PluginManager()
private PluginManager(
ServiceManager.RegisterStartupBlockerDelegate registerStartupBlocker,
ServiceManager.RegisterUnloadAfterDelegate registerUnloadAfter)
{
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>
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)
{
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
{
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.Immutable;
namespace Dalamud.Plugin.Ipc.Internal;
@ -10,11 +11,28 @@ internal class CallGate : IServiceType
{
private readonly Dictionary<string, CallGateChannel> gates = new();
private ImmutableDictionary<string, CallGateChannel>? gatesCopy;
[ServiceManager.ServiceConstructor]
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>
/// Gets the provider associated with the specified name.
/// </summary>
@ -22,8 +40,34 @@ internal class CallGate : IServiceType
/// <returns>A CallGate registered under the given name.</returns>
public CallGateChannel GetOrCreateChannel(string name)
{
if (!this.gates.TryGetValue(name, out var gate))
gate = this.gates[name] = new CallGateChannel(name);
return gate;
lock (this.gates)
{
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.Immutable;
using System.Linq;
using System.Reflection;
@ -14,6 +14,17 @@ namespace Dalamud.Plugin.Ipc.Internal;
/// </summary>
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>
/// Initializes a new instance of the <see cref="CallGateChannel"/> class.
/// </summary>
@ -31,17 +42,52 @@ internal class CallGateChannel
/// <summary>
/// Gets a list of delegate subscriptions for when SendMessage is called.
/// </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>
/// Gets or sets an action for when InvokeAction is called.
/// </summary>
public Delegate Action { get; set; }
public Delegate? Action { get; set; }
/// <summary>
/// Gets or sets a func for when InvokeFunc is called.
/// </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>
/// Invoke all actions that have subscribed to this IPC.
@ -49,9 +95,6 @@ internal class CallGateChannel
/// <param name="args">Message arguments.</param>
internal void SendMessage(object?[]? args)
{
if (this.Subscriptions.Count == 0)
return;
foreach (var subscription in this.Subscriptions)
{
var methodInfo = subscription.GetMethodInfo();
@ -105,7 +148,14 @@ internal class CallGateChannel
var paramTypes = methodInfo.GetParameters()
.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);
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))
{
@ -148,6 +198,9 @@ internal class CallGateChannel
private object? ConvertObject(object? obj, Type type)
{
if (obj is null)
return null;
var json = JsonConvert.SerializeObject(obj);
try

View file

@ -1,5 +1,3 @@
using System;
#pragma warning disable SA1402 // File may only contain a single type
namespace Dalamud.Plugin.Ipc.Internal;
@ -37,7 +35,7 @@ internal class CallGatePubSub<TRet> : CallGatePubSubBase, ICallGateProvider<TRet
public void InvokeAction()
=> base.InvokeAction();
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc"/>
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc{TRet}"/>
public TRet InvokeFunc()
=> this.InvokeFunc<TRet>();
}
@ -75,7 +73,7 @@ internal class CallGatePubSub<T1, TRet> : CallGatePubSubBase, ICallGateProvider<
public void InvokeAction(T1 arg1)
=> base.InvokeAction(arg1);
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc"/>
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc{TRet}"/>
public TRet InvokeFunc(T1 arg1)
=> this.InvokeFunc<TRet>(arg1);
}
@ -113,7 +111,7 @@ internal class CallGatePubSub<T1, T2, TRet> : CallGatePubSubBase, ICallGateProvi
public void InvokeAction(T1 arg1, T2 arg2)
=> base.InvokeAction(arg1, arg2);
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc"/>
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc{TRet}"/>
public TRet InvokeFunc(T1 arg1, T2 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)
=> base.InvokeAction(arg1, arg2, arg3);
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc"/>
/// <inheritdoc cref="CallGatePubSubBase.InvokeFunc{TRet}"/>
public TRet InvokeFunc(T1 arg1, T2 arg2, T3 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)
=> 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)
=> 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)
=> 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)
=> 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)
=> 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)
=> 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)
=> 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)
=> 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)
=> 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)
=> this.InvokeFunc<TRet>(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8);
}

View file

@ -1,5 +1,3 @@
using System;
using Dalamud.Plugin.Ipc.Exceptions;
namespace Dalamud.Plugin.Ipc.Internal;
@ -13,7 +11,7 @@ internal abstract class CallGatePubSubBase
/// Initializes a new instance of the <see cref="CallGatePubSubBase"/> class.
/// </summary>
/// <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);
}
@ -54,14 +52,14 @@ internal abstract class CallGatePubSubBase
/// </summary>
/// <param name="action">Action to subscribe.</param>
private protected void Subscribe(Delegate action)
=> this.Channel.Subscriptions.Add(action);
=> this.Channel.Subscribe(action);
/// <summary>
/// Unsubscribe an expression from this registration.
/// </summary>
/// <param name="action">Action to unsubscribe.</param>
private protected void Unsubscribe(Delegate action)
=> this.Channel.Subscriptions.Remove(action);
=> this.Channel.Unsubscribe(action);
/// <summary>
/// Invoke an action registered for inter-plugin communication.

View file

@ -1,5 +1,10 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.ExceptionServices;
using Dalamud.Plugin.Ipc.Exceptions;
using Serilog;
namespace Dalamud.Plugin.Ipc.Internal;
@ -8,10 +13,14 @@ namespace Dalamud.Plugin.Ipc.Internal;
/// </summary>
internal readonly struct DataCache
{
/// <summary> Name of the data. </summary>
internal readonly string Tag;
/// <summary> The assembly name of the initial creator. </summary>
internal readonly string CreatorAssemblyName;
/// <summary> A not-necessarily distinct list of current users. </summary>
/// <remarks> Also used as a reference count tracker. </remarks>
internal readonly List<string> UserAssemblyNames;
/// <summary> The type the data was registered as. </summary>
@ -23,14 +32,83 @@ internal readonly struct DataCache
/// <summary>
/// Initializes a new instance of the <see cref="DataCache"/> struct.
/// </summary>
/// <param name="tag">Name of the data.</param>
/// <param name="creatorAssemblyName">The assembly name of the initial creator.</param>
/// <param name="data">A reference to 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.UserAssemblyNames = new List<string> { creatorAssemblyName };
this.UserAssemblyNames = new();
this.Data = data;
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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using Dalamud.Plugin.Ipc.Exceptions;
using Serilog;
@ -16,7 +14,11 @@ namespace Dalamud.Plugin.Ipc.Internal;
[ServiceManager.BlockingEarlyLoadedService]
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]
private DataShare()
@ -39,38 +41,15 @@ internal class DataShare : IServiceType
where T : class
{
var callerName = GetCallerName();
Lazy<DataCache> cacheLazy;
lock (this.caches)
{
if (this.caches.TryGetValue(tag, out var cache))
{
if (!cache.Type.IsAssignableTo(typeof(T)))
{
throw new DataCacheTypeMismatchError(tag, cache.CreatorAssemblyName, typeof(T), cache.Type);
}
cache.UserAssemblyNames.Add(callerName);
return cache.Data as T ?? throw new DataCacheValueNullError(tag, cache.Type);
}
try
{
var obj = dataGenerator.Invoke();
if (obj == null)
{
throw new Exception("Returned data was null.");
}
cache = new DataCache(callerName, obj, typeof(T));
this.caches[tag] = cache;
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);
}
if (!this.caches.TryGetValue(tag, out cacheLazy))
this.caches[tag] = cacheLazy = new(() => DataCache.From(tag, callerName, dataGenerator));
}
return cacheLazy.Value.TryGetData<T>(callerName, out var value, out var ex) ? value : throw ex;
}
/// <summary>
@ -80,34 +59,36 @@ internal class DataShare : IServiceType
/// <param name="tag">The name for the data cache.</param>
public void RelinquishData(string tag)
{
DataCache cache;
lock (this.caches)
{
if (!this.caches.TryGetValue(tag, out var cache))
{
if (!this.caches.TryGetValue(tag, out var cacheLazy))
return;
}
var callerName = GetCallerName();
lock (this.caches)
{
if (!cache.UserAssemblyNames.Remove(callerName) || cache.UserAssemblyNames.Count > 0)
{
return;
}
if (this.caches.Remove(tag))
{
if (cache.Data is IDisposable disposable)
{
disposable.Dispose();
Log.Verbose("[DataShare] Disposed [{Tag:l}] after it was removed from all shares.", tag);
}
else
{
Log.Verbose("[DataShare] Removed [{Tag:l}] from all shares.", tag);
}
}
cache = cacheLazy.Value;
if (!cache.UserAssemblyNames.Remove(callerName) || cache.UserAssemblyNames.Count > 0)
return;
if (!this.caches.Remove(tag))
return;
}
if (cache.Data is IDisposable disposable)
{
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
{
data = null;
Lazy<DataCache> cacheLazy;
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;
}
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>
@ -155,27 +127,14 @@ internal class DataShare : IServiceType
public T GetData<T>(string tag)
where T : class
{
Lazy<DataCache> cacheLazy;
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.");
}
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>
@ -186,7 +145,8 @@ internal class DataShare : IServiceType
{
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.Logging.Internal;
using Dalamud.Storage;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
using JetBrains.Annotations;
@ -21,7 +22,7 @@ namespace Dalamud;
// - Visualize/output .dot or imgui thing
/// <summary>
/// Class to initialize Service&lt;T&gt;s.
/// Class to initialize <see cref="Service{T}"/>.
/// </summary>
internal static class ServiceManager
{
@ -43,6 +44,26 @@ internal static class ServiceManager
private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new();
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>
/// Kinds of services.
@ -125,6 +146,15 @@ internal static class ServiceManager
#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>
/// Kicks off construction of services that can handle early loading.
/// </summary>
@ -141,7 +171,7 @@ internal static class ServiceManager
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();
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
.InvokeMember(
"GetAsync",
nameof(Service<IServiceType>.GetAsync),
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
null,
null,
@ -184,17 +214,42 @@ internal static class ServiceManager
}
var typeAsServiceT = ServiceHelpers.GetAsService(serviceType);
dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT)
dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT, false)
.Select(x => typeof(Service<>).MakeGenericType(x))
.ToList();
}
var blockerTasks = new List<Task>();
_ = Task.Run(async () =>
{
try
{
var whenBlockingComplete = Task.WhenAll(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x]));
while (await Task.WhenAny(whenBlockingComplete, Task.Delay(30000)) != whenBlockingComplete)
// Wait for all blocking constructors to complete first.
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(
IntPtr.Zero,
@ -208,13 +263,6 @@ internal static class ServiceManager
"and the user chose to continue without Dalamud.");
}
}
BlockingServicesLoadedTaskCompletionSource.SetResult();
Timings.Event("BlockingServices Initialized");
}
catch (Exception e)
{
BlockingServicesLoadedTaskCompletionSource.SetException(e);
}
}).ConfigureAwait(false);
@ -249,6 +297,25 @@ internal static class ServiceManager
if (!hasDeps)
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<>)
.MakeGenericType(serviceType)
.InvokeMember(
@ -256,7 +323,7 @@ internal static class ServiceManager
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic,
null,
null,
null));
new object[] { startLoaderArgs }));
servicesToLoad.Remove(serviceType);
#if DEBUG
@ -328,13 +395,13 @@ internal static class ServiceManager
unloadResetEvent.Reset();
var dependencyServicesMap = new Dictionary<Type, List<Type>>();
var dependencyServicesMap = new Dictionary<Type, IReadOnlyCollection<Type>>();
var allToUnload = new HashSet<Type>();
var unloadOrder = new List<Type>();
Log.Information("==== COLLECTING SERVICES TO UNLOAD ====");
foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes())
foreach (var serviceType in GetConcreteServiceTypes())
{
if (!serviceType.IsAssignableTo(typeof(IServiceType)))
continue;
@ -347,7 +414,7 @@ internal static class ServiceManager
Log.Verbose("Calling GetDependencyServices for '{ServiceName}'", serviceType.FullName!);
var typeAsServiceT = ServiceHelpers.GetAsService(serviceType);
dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT);
dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT, true);
allToUnload.Add(serviceType);
}
@ -541,11 +608,35 @@ internal static class ServiceManager
}
/// <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>
[AttributeUsage(AttributeTargets.Method)]
[MeansImplicitUse]
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.Internal;
using Dalamud.Plugin.Internal;
using Dalamud.Utility.Timing;
using JetBrains.Annotations;
@ -25,6 +24,7 @@ internal static class Service<T> where T : IServiceType
private static readonly ServiceManager.ServiceAttribute ServiceAttribute;
private static TaskCompletionSource<T> instanceTcs = new();
private static List<Type>? dependencyServices;
private static List<Type>? dependencyServicesForUnload;
static Service()
{
@ -95,7 +95,7 @@ internal static class Service<T> where T : IServiceType
if (ServiceAttribute.Kind != ServiceManager.ServiceKind.ProvidedService
&& ServiceManager.CurrentConstructorServiceType.Value is { } currentServiceType)
{
var deps = ServiceHelpers.GetDependencies(currentServiceType);
var deps = ServiceHelpers.GetDependencies(typeof(Service<>).MakeGenericType(currentServiceType), false);
if (!deps.Contains(typeof(T)))
{
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.
/// </summary>
/// <returns>The object.</returns>
[UsedImplicitly]
public static Task<T> GetAsync() => instanceTcs.Task;
/// <summary>
@ -141,11 +140,15 @@ internal static class Service<T> where T : IServiceType
/// <summary>
/// Gets an enumerable containing <see cref="Service{T}"/>s that are required for this Service to initialize
/// without blocking.
/// These are NOT returned as <see cref="Service{T}"/> types; raw types will be returned.
/// </summary>
/// <param name="includeUnloadDependencies">Whether to include the unload dependencies.</param>
/// <returns>List of dependency services.</returns>
[UsedImplicitly]
public static List<Type> GetDependencyServices()
public static IReadOnlyCollection<Type> GetDependencyServices(bool includeUnloadDependencies)
{
if (includeUnloadDependencies && dependencyServicesForUnload is not null)
return dependencyServicesForUnload;
if (dependencyServices is not null)
return dependencyServices;
@ -158,7 +161,8 @@ internal static class Service<T> where T : IServiceType
{
res.AddRange(ctor
.GetParameters()
.Select(x => x.ParameterType));
.Select(x => x.ParameterType)
.Where(x => x.GetServiceKind() != ServiceManager.ServiceKind.None));
}
res.AddRange(typeof(T)
@ -171,50 +175,8 @@ internal static class Service<T> where T : IServiceType
.OfType<InherentDependencyAttribute>()
.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)
{
ServiceManager.Log.Verbose("Service<{0}>: => Dependency: {1}", typeof(T).Name, type.Name);
}
var deps = res
.Distinct()
@ -244,8 +206,9 @@ internal static class Service<T> where T : IServiceType
/// <summary>
/// Starts the service loader. Only to be called from <see cref="ServiceManager"/>.
/// </summary>
/// <param name="additionalProvidedTypedObjects">Additional objects available to constructors.</param>
/// <returns>The loader task.</returns>
internal static Task<T> StartLoader()
internal static Task<T> StartLoader(IReadOnlyCollection<object> additionalProvidedTypedObjects)
{
if (instanceTcs.Task.IsCompleted)
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 () =>
{
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);
try
{
var instance = await ConstructObject();
var instance = await ConstructObject(ctorArgs).ConfigureAwait(false);
instanceTcs.SetResult(instance);
List<Task>? tasks = null;
@ -270,8 +250,17 @@ internal static class Service<T> where T : IServiceType
continue;
ServiceManager.Log.Debug("Service<{0}>: Calling {1}", typeof(T).Name, method.Name);
var args = await Task.WhenAll(method.GetParameters().Select(
x => ResolveServiceFromTypeAsync(x.ParameterType)));
var args = await ResolveInjectedParameters(
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
{
if (method.Invoke(instance, args) is Task task)
@ -331,24 +320,6 @@ internal static class Service<T> where T : IServiceType
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()
{
const BindingFlags ctorBindingFlags =
@ -359,18 +330,18 @@ internal static class Service<T> where T : IServiceType
.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();
if (ctor == null)
throw new Exception($"Service \"{typeof(T).FullName}\" had no applicable constructor");
var args = await Task.WhenAll(
ctor.GetParameters().Select(x => ResolveServiceFromTypeAsync(x.ParameterType)));
var args = await ResolveInjectedParameters(ctor.GetParameters(), additionalProvidedTypedObjects)
.ConfigureAwait(false);
using (Timings.Start($"{typeof(T).Name} Construct"))
{
#if DEBUG
ServiceManager.CurrentConstructorServiceType.Value = typeof(Service<T>);
ServiceManager.CurrentConstructorServiceType.Value = typeof(T);
try
{
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>
/// Exception thrown when service is attempted to be retrieved when it's unloaded.
/// </summary>
@ -407,11 +415,12 @@ internal static class ServiceHelpers
{
/// <summary>
/// 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>
/// <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>
public static List<Type> GetDependencies(Type serviceType)
public static IReadOnlyCollection<Type> GetDependencies(Type serviceType, bool includeUnloadDependencies)
{
#if DEBUG
if (!serviceType.IsGenericType || serviceType.GetGenericTypeDefinition() != typeof(Service<>))
@ -422,12 +431,12 @@ internal static class ServiceHelpers
}
#endif
return (List<Type>)serviceType.InvokeMember(
return (IReadOnlyCollection<Type>)serviceType.InvokeMember(
nameof(Service<IServiceType>.GetDependencyServices),
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
null,
null,
null) ?? new List<Type>();
new object?[] { includeUnloadDependencies }) ?? new List<Type>();
}
/// <summary>

View file

@ -44,7 +44,10 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
private bool isDisposed;
[ServiceManager.ServiceConstructor]
private DalamudAssetManager(Dalamud dalamud, HappyHttpClient httpClient)
private DalamudAssetManager(
Dalamud dalamud,
HappyHttpClient httpClient,
ServiceManager.RegisterStartupBlockerDelegate registerStartupBlocker)
{
this.dalamud = dalamud;
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.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");
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/>
@ -83,25 +95,6 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
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/>
[Pure]
public bool IsStreamImmediatelyAvailable(DalamudAsset asset) =>

View file

@ -87,4 +87,14 @@ internal static class ArrayExtensions
result = default;
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