mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
Merge remote-tracking branch 'origin/master' into net8-rollup
This commit is contained in:
commit
c993be9c97
69 changed files with 3619 additions and 915 deletions
|
|
@ -104,13 +104,14 @@ resharper_can_use_global_alias = false
|
|||
resharper_csharp_align_multiline_parameter = true
|
||||
resharper_csharp_align_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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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+201↑p
|
||||
.text:00000001405CD210 ; sub_140141D10+220↑p ...
|
||||
.text:00000001405CD210
|
||||
.text:00000001405CD210 var_220 = qword ptr -220h
|
||||
.text:00000001405CD210 var_218 = byte ptr -218h
|
||||
.text:00000001405CD210 var_210 = word ptr -210h
|
||||
.text:00000001405CD210 var_208 = byte ptr -208h
|
||||
.text:00000001405CD210 var_200 = word ptr -200h
|
||||
.text:00000001405CD210 var_1FC = dword ptr -1FCh
|
||||
.text:00000001405CD210 var_1F8 = qword ptr -1F8h
|
||||
.text:00000001405CD210 var_1F0 = qword ptr -1F0h
|
||||
.text:00000001405CD210 var_1E8 = qword ptr -1E8h
|
||||
.text:00000001405CD210 var_1E0 = dword ptr -1E0h
|
||||
.text:00000001405CD210 var_1DC = word ptr -1DCh
|
||||
.text:00000001405CD210 var_1DA = word ptr -1DAh
|
||||
.text:00000001405CD210 var_1D8 = qword ptr -1D8h
|
||||
.text:00000001405CD210 var_1D0 = byte ptr -1D0h
|
||||
.text:00000001405CD210 var_1C8 = qword ptr -1C8h
|
||||
.text:00000001405CD210 var_1B0 = dword ptr -1B0h
|
||||
.text:00000001405CD210 var_1AC = dword ptr -1ACh
|
||||
.text:00000001405CD210 var_1A8 = dword ptr -1A8h
|
||||
.text:00000001405CD210 var_1A4 = dword ptr -1A4h
|
||||
.text:00000001405CD210 var_1A0 = dword ptr -1A0h
|
||||
.text:00000001405CD210 var_160 = dword ptr -160h
|
||||
.text:00000001405CD210 var_15C = dword ptr -15Ch
|
||||
.text:00000001405CD210 var_140 = dword ptr -140h
|
||||
.text:00000001405CD210 var_138 = dword ptr -138h
|
||||
.text:00000001405CD210 var_130 = byte ptr -130h
|
||||
.text:00000001405CD210 var_C0 = byte ptr -0C0h
|
||||
.text:00000001405CD210 var_50 = qword ptr -50h
|
||||
.text:00000001405CD210 var_38 = qword ptr -38h
|
||||
.text:00000001405CD210 var_30 = qword ptr -30h
|
||||
.text:00000001405CD210 var_28 = qword ptr -28h
|
||||
.text:00000001405CD210 var_20 = qword ptr -20h
|
||||
.text:00000001405CD210 senderActorId = dword ptr 30h
|
||||
.text:00000001405CD210 isLocal = byte ptr 38h
|
||||
.text:00000001405CD210
|
||||
.text:00000001405CD210 ; __unwind { // __GSHandlerCheck
|
||||
.text:00000001405CD210 push rbp
|
||||
.text:00000001405CD212 push rdi
|
||||
.text:00000001405CD213 push r14
|
||||
.text:00000001405CD215 push r15
|
||||
.text:00000001405CD217 lea rbp, [rsp-128h]
|
||||
.text:00000001405CD21F sub rsp, 228h
|
||||
.text:00000001405CD226 mov rax, cs:__security_cookie
|
||||
.text:00000001405CD22D xor rax, rsp
|
||||
.text:00000001405CD230 mov [rbp+140h+var_50], rax
|
||||
.text:00000001405CD237 xor r10b, r10b
|
||||
.text:00000001405CD23A mov [rsp+240h+var_1F8], rcx
|
||||
.text:00000001405CD23F xor eax, eax
|
||||
.text:00000001405CD241 mov r11, r9
|
||||
.text:00000001405CD244 mov r14, r8
|
||||
.text:00000001405CD247 mov r9d, eax
|
||||
.text:00000001405CD24A movzx r15d, dx
|
||||
.text:00000001405CD24E lea r8, [rcx+0C10h]
|
||||
.text:00000001405CD255 mov rdi, rcx
|
||||
*/
|
||||
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
547
Dalamud/Game/Inventory/GameInventory.cs
Normal file
547
Dalamud/Game/Inventory/GameInventory.cs
Normal file
|
|
@ -0,0 +1,547 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Plugin.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
|
||||
namespace Dalamud.Game.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// This class provides events for the players in-game inventory.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal class GameInventory : IDisposable, IServiceType
|
||||
{
|
||||
private readonly List<GameInventoryPluginScoped> subscribersPendingChange = new();
|
||||
private readonly List<GameInventoryPluginScoped> subscribers = new();
|
||||
|
||||
private readonly List<InventoryItemAddedArgs> addedEvents = new();
|
||||
private readonly List<InventoryItemRemovedArgs> removedEvents = new();
|
||||
private readonly List<InventoryItemChangedArgs> changedEvents = new();
|
||||
private readonly List<InventoryItemMovedArgs> movedEvents = new();
|
||||
private readonly List<InventoryItemSplitArgs> splitEvents = new();
|
||||
private readonly List<InventoryItemMergedArgs> mergedEvents = new();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly Framework framework = Service<Framework>.Get();
|
||||
|
||||
private readonly Hook<RaptureAtkModuleUpdateDelegate> raptureAtkModuleUpdateHook;
|
||||
|
||||
private readonly GameInventoryType[] inventoryTypes;
|
||||
private readonly GameInventoryItem[]?[] inventoryItems;
|
||||
|
||||
private bool subscribersChanged;
|
||||
private bool inventoriesMightBeChanged;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private GameInventory()
|
||||
{
|
||||
this.inventoryTypes = Enum.GetValues<GameInventoryType>();
|
||||
this.inventoryItems = new GameInventoryItem[this.inventoryTypes.Length][];
|
||||
|
||||
unsafe
|
||||
{
|
||||
this.raptureAtkModuleUpdateHook = Hook<RaptureAtkModuleUpdateDelegate>.FromFunctionPointerVariable(
|
||||
new(&((RaptureAtkModule.RaptureAtkModuleVTable*)RaptureAtkModule.StaticAddressPointers.VTable)->Update),
|
||||
this.RaptureAtkModuleUpdateDetour);
|
||||
}
|
||||
|
||||
this.raptureAtkModuleUpdateHook.Enable();
|
||||
}
|
||||
|
||||
private unsafe delegate void RaptureAtkModuleUpdateDelegate(RaptureAtkModule* ram, float f1);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
lock (this.subscribersPendingChange)
|
||||
{
|
||||
this.subscribers.Clear();
|
||||
this.subscribersPendingChange.Clear();
|
||||
this.subscribersChanged = false;
|
||||
this.framework.Update -= this.OnFrameworkUpdate;
|
||||
this.raptureAtkModuleUpdateHook.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to events.
|
||||
/// </summary>
|
||||
/// <param name="s">The event target.</param>
|
||||
public void Subscribe(GameInventoryPluginScoped s)
|
||||
{
|
||||
lock (this.subscribersPendingChange)
|
||||
{
|
||||
this.subscribersPendingChange.Add(s);
|
||||
this.subscribersChanged = true;
|
||||
if (this.subscribersPendingChange.Count == 1)
|
||||
{
|
||||
this.inventoriesMightBeChanged = true;
|
||||
this.framework.Update += this.OnFrameworkUpdate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe from events.
|
||||
/// </summary>
|
||||
/// <param name="s">The event target.</param>
|
||||
public void Unsubscribe(GameInventoryPluginScoped s)
|
||||
{
|
||||
lock (this.subscribersPendingChange)
|
||||
{
|
||||
if (!this.subscribersPendingChange.Remove(s))
|
||||
return;
|
||||
this.subscribersChanged = true;
|
||||
if (this.subscribersPendingChange.Count == 0)
|
||||
this.framework.Update -= this.OnFrameworkUpdate;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFrameworkUpdate(IFramework framework1)
|
||||
{
|
||||
if (!this.inventoriesMightBeChanged)
|
||||
return;
|
||||
|
||||
this.inventoriesMightBeChanged = false;
|
||||
|
||||
for (var i = 0; i < this.inventoryTypes.Length; i++)
|
||||
{
|
||||
var newItems = GameInventoryItem.GetReadOnlySpanOfInventory(this.inventoryTypes[i]);
|
||||
if (newItems.IsEmpty)
|
||||
continue;
|
||||
|
||||
// Assumption: newItems is sorted by slots, and the last item has the highest slot number.
|
||||
var oldItems = this.inventoryItems[i] ??= new GameInventoryItem[newItems[^1].InternalItem.Slot + 1];
|
||||
|
||||
foreach (ref readonly var newItem in newItems)
|
||||
{
|
||||
ref var oldItem = ref oldItems[newItem.InternalItem.Slot];
|
||||
|
||||
if (oldItem.IsEmpty)
|
||||
{
|
||||
if (!newItem.IsEmpty)
|
||||
{
|
||||
this.addedEvents.Add(new(newItem));
|
||||
oldItem = newItem;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (newItem.IsEmpty)
|
||||
{
|
||||
this.removedEvents.Add(new(oldItem));
|
||||
oldItem = newItem;
|
||||
}
|
||||
else if (!oldItem.Equals(newItem))
|
||||
{
|
||||
this.changedEvents.Add(new(oldItem, newItem));
|
||||
oldItem = newItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Was there any change? If not, stop further processing.
|
||||
// Note that only these three are checked; the rest will be populated after this check.
|
||||
if (this.addedEvents.Count == 0 && this.removedEvents.Count == 0 && this.changedEvents.Count == 0)
|
||||
return;
|
||||
|
||||
// Make a copy of subscribers, to accommodate self removal during the loop.
|
||||
if (this.subscribersChanged)
|
||||
{
|
||||
bool isNew;
|
||||
lock (this.subscribersPendingChange)
|
||||
{
|
||||
isNew = this.subscribersPendingChange.Any() && !this.subscribers.Any();
|
||||
this.subscribers.Clear();
|
||||
this.subscribers.AddRange(this.subscribersPendingChange);
|
||||
this.subscribersChanged = false;
|
||||
}
|
||||
|
||||
// Is this the first time (resuming) scanning for changes? Then discard the "changes".
|
||||
if (isNew)
|
||||
{
|
||||
this.addedEvents.Clear();
|
||||
this.removedEvents.Clear();
|
||||
this.changedEvents.Clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast InventoryChangedRaw.
|
||||
// Same reason with the above on why are there 3 lists of events involved.
|
||||
var allRawEventsCollection = new DeferredReadOnlyCollection<InventoryEventArgs>(
|
||||
this.addedEvents.Count +
|
||||
this.removedEvents.Count +
|
||||
this.changedEvents.Count,
|
||||
() => Array.Empty<InventoryEventArgs>()
|
||||
.Concat(this.addedEvents)
|
||||
.Concat(this.removedEvents)
|
||||
.Concat(this.changedEvents));
|
||||
foreach (var s in this.subscribers)
|
||||
s.InvokeChangedRaw(allRawEventsCollection);
|
||||
|
||||
// Resolve moved items, from 1 added + 1 removed event.
|
||||
for (var iAdded = this.addedEvents.Count - 1; iAdded >= 0; --iAdded)
|
||||
{
|
||||
var added = this.addedEvents[iAdded];
|
||||
for (var iRemoved = this.removedEvents.Count - 1; iRemoved >= 0; --iRemoved)
|
||||
{
|
||||
var removed = this.removedEvents[iRemoved];
|
||||
if (added.Item.ItemId != removed.Item.ItemId)
|
||||
continue;
|
||||
|
||||
this.movedEvents.Add(new(removed, added));
|
||||
|
||||
// Remove the reinterpreted entries.
|
||||
this.addedEvents.RemoveAt(iAdded);
|
||||
this.removedEvents.RemoveAt(iRemoved);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve moved items, from 2 changed events.
|
||||
for (var i = this.changedEvents.Count - 1; i >= 0; --i)
|
||||
{
|
||||
var e1 = this.changedEvents[i];
|
||||
for (var j = i - 1; j >= 0; --j)
|
||||
{
|
||||
var e2 = this.changedEvents[j];
|
||||
if (e1.Item.ItemId != e2.OldItemState.ItemId || e1.OldItemState.ItemId != e2.Item.ItemId)
|
||||
continue;
|
||||
|
||||
// Move happened, and e2 has an item.
|
||||
if (!e2.Item.IsEmpty)
|
||||
this.movedEvents.Add(new(e1, e2));
|
||||
|
||||
// Move happened, and e1 has an item.
|
||||
if (!e1.Item.IsEmpty)
|
||||
this.movedEvents.Add(new(e2, e1));
|
||||
|
||||
// Remove the reinterpreted entries. Note that i > j.
|
||||
this.changedEvents.RemoveAt(i);
|
||||
this.changedEvents.RemoveAt(j);
|
||||
|
||||
// We've removed two. Adjust the outer counter.
|
||||
--i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve split items, from 1 added + 1 changed event.
|
||||
for (var iAdded = this.addedEvents.Count - 1; iAdded >= 0; --iAdded)
|
||||
{
|
||||
var added = this.addedEvents[iAdded];
|
||||
for (var iChanged = this.changedEvents.Count - 1; iChanged >= 0; --iChanged)
|
||||
{
|
||||
var changed = this.changedEvents[iChanged];
|
||||
if (added.Item.ItemId != changed.Item.ItemId || added.Item.ItemId != changed.OldItemState.ItemId)
|
||||
continue;
|
||||
|
||||
this.splitEvents.Add(new(changed, added));
|
||||
|
||||
// Remove the reinterpreted entries.
|
||||
this.addedEvents.RemoveAt(iAdded);
|
||||
this.changedEvents.RemoveAt(iChanged);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve merged items, from 1 removed + 1 changed event.
|
||||
for (var iRemoved = this.removedEvents.Count - 1; iRemoved >= 0; --iRemoved)
|
||||
{
|
||||
var removed = this.removedEvents[iRemoved];
|
||||
for (var iChanged = this.changedEvents.Count - 1; iChanged >= 0; --iChanged)
|
||||
{
|
||||
var changed = this.changedEvents[iChanged];
|
||||
if (removed.Item.ItemId != changed.Item.ItemId || removed.Item.ItemId != changed.OldItemState.ItemId)
|
||||
continue;
|
||||
|
||||
this.mergedEvents.Add(new(removed, changed));
|
||||
|
||||
// Remove the reinterpreted entries.
|
||||
this.removedEvents.RemoveAt(iRemoved);
|
||||
this.changedEvents.RemoveAt(iChanged);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a collection view of all events.
|
||||
var allEventsCollection = new DeferredReadOnlyCollection<InventoryEventArgs>(
|
||||
this.addedEvents.Count +
|
||||
this.removedEvents.Count +
|
||||
this.changedEvents.Count +
|
||||
this.movedEvents.Count +
|
||||
this.splitEvents.Count +
|
||||
this.mergedEvents.Count,
|
||||
() => Array.Empty<InventoryEventArgs>()
|
||||
.Concat(this.addedEvents)
|
||||
.Concat(this.removedEvents)
|
||||
.Concat(this.changedEvents)
|
||||
.Concat(this.movedEvents)
|
||||
.Concat(this.splitEvents)
|
||||
.Concat(this.mergedEvents));
|
||||
|
||||
// Broadcast the rest.
|
||||
foreach (var s in this.subscribers)
|
||||
{
|
||||
s.InvokeChanged(allEventsCollection);
|
||||
s.Invoke(this.addedEvents);
|
||||
s.Invoke(this.removedEvents);
|
||||
s.Invoke(this.changedEvents);
|
||||
s.Invoke(this.movedEvents);
|
||||
s.Invoke(this.splitEvents);
|
||||
s.Invoke(this.mergedEvents);
|
||||
}
|
||||
|
||||
// We're done using the lists. Clean them up.
|
||||
this.addedEvents.Clear();
|
||||
this.removedEvents.Clear();
|
||||
this.changedEvents.Clear();
|
||||
this.movedEvents.Clear();
|
||||
this.splitEvents.Clear();
|
||||
this.mergedEvents.Clear();
|
||||
}
|
||||
|
||||
private unsafe void RaptureAtkModuleUpdateDetour(RaptureAtkModule* ram, float f1)
|
||||
{
|
||||
this.inventoriesMightBeChanged |= ram->AgentUpdateFlag != 0;
|
||||
this.raptureAtkModuleUpdateHook.Original(ram, f1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="IReadOnlyCollection{T}"/> view of <see cref="IEnumerable{T}"/>, so that the number of items
|
||||
/// contained within can be known in advance, and it can be enumerated multiple times.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of elements being enumerated.</typeparam>
|
||||
private class DeferredReadOnlyCollection<T> : IReadOnlyCollection<T>
|
||||
{
|
||||
private readonly Func<IEnumerable<T>> enumerableGenerator;
|
||||
|
||||
public DeferredReadOnlyCollection(int count, Func<IEnumerable<T>> enumerableGenerator)
|
||||
{
|
||||
this.enumerableGenerator = enumerableGenerator;
|
||||
this.Count = count;
|
||||
}
|
||||
|
||||
public int Count { get; }
|
||||
|
||||
public IEnumerator<T> GetEnumerator() => this.enumerableGenerator().GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.enumerableGenerator().GetEnumerator();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plugin-scoped version of a GameInventory service.
|
||||
/// </summary>
|
||||
[PluginInterface]
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.ScopedService]
|
||||
#pragma warning disable SA1015
|
||||
[ResolveVia<IGameInventory>]
|
||||
#pragma warning restore SA1015
|
||||
internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInventory
|
||||
{
|
||||
private static readonly ModuleLog Log = new(nameof(GameInventoryPluginScoped));
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly GameInventory gameInventoryService = Service<GameInventory>.Get();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameInventoryPluginScoped"/> class.
|
||||
/// </summary>
|
||||
public GameInventoryPluginScoped() => this.gameInventoryService.Subscribe(this);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IGameInventory.InventoryChangelogDelegate? InventoryChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IGameInventory.InventoryChangelogDelegate? InventoryChangedRaw;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IGameInventory.InventoryChangedDelegate? ItemAdded;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IGameInventory.InventoryChangedDelegate? ItemRemoved;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IGameInventory.InventoryChangedDelegate? ItemChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IGameInventory.InventoryChangedDelegate? ItemMoved;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IGameInventory.InventoryChangedDelegate? ItemSplit;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IGameInventory.InventoryChangedDelegate? ItemMerged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IGameInventory.InventoryChangedDelegate<InventoryItemAddedArgs>? ItemAddedExplicit;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IGameInventory.InventoryChangedDelegate<InventoryItemRemovedArgs>? ItemRemovedExplicit;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IGameInventory.InventoryChangedDelegate<InventoryItemChangedArgs>? ItemChangedExplicit;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IGameInventory.InventoryChangedDelegate<InventoryItemMovedArgs>? ItemMovedExplicit;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IGameInventory.InventoryChangedDelegate<InventoryItemSplitArgs>? ItemSplitExplicit;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IGameInventory.InventoryChangedDelegate<InventoryItemMergedArgs>? ItemMergedExplicit;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.gameInventoryService.Unsubscribe(this);
|
||||
|
||||
this.InventoryChanged = null;
|
||||
this.InventoryChangedRaw = null;
|
||||
this.ItemAdded = null;
|
||||
this.ItemRemoved = null;
|
||||
this.ItemChanged = null;
|
||||
this.ItemMoved = null;
|
||||
this.ItemSplit = null;
|
||||
this.ItemMerged = null;
|
||||
this.ItemAddedExplicit = null;
|
||||
this.ItemRemovedExplicit = null;
|
||||
this.ItemChangedExplicit = null;
|
||||
this.ItemMovedExplicit = null;
|
||||
this.ItemSplitExplicit = null;
|
||||
this.ItemMergedExplicit = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke <see cref="InventoryChanged"/>.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
internal void InvokeChanged(IReadOnlyCollection<InventoryEventArgs> data)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.InventoryChanged?.Invoke(data);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(
|
||||
e,
|
||||
"[{plugin}] Exception during {argType} callback",
|
||||
Service<PluginManager>.GetNullable()?.FindCallingPlugin(new(e))?.Name ?? "(unknown plugin)",
|
||||
nameof(this.InventoryChanged));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke <see cref="InventoryChangedRaw"/>.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
internal void InvokeChangedRaw(IReadOnlyCollection<InventoryEventArgs> data)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.InventoryChangedRaw?.Invoke(data);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(
|
||||
e,
|
||||
"[{plugin}] Exception during {argType} callback",
|
||||
Service<PluginManager>.GetNullable()?.FindCallingPlugin(new(e))?.Name ?? "(unknown plugin)",
|
||||
nameof(this.InventoryChangedRaw));
|
||||
}
|
||||
}
|
||||
|
||||
// Note below: using List<T> instead of IEnumerable<T>, since List<T> has a specialized lightweight enumerator.
|
||||
|
||||
/// <summary>
|
||||
/// Invoke the appropriate event handler.
|
||||
/// </summary>
|
||||
/// <param name="events">The data.</param>
|
||||
internal void Invoke(List<InventoryItemAddedArgs> events) =>
|
||||
Invoke(this.ItemAdded, this.ItemAddedExplicit, events);
|
||||
|
||||
/// <summary>
|
||||
/// Invoke the appropriate event handler.
|
||||
/// </summary>
|
||||
/// <param name="events">The data.</param>
|
||||
internal void Invoke(List<InventoryItemRemovedArgs> events) =>
|
||||
Invoke(this.ItemRemoved, this.ItemRemovedExplicit, events);
|
||||
|
||||
/// <summary>
|
||||
/// Invoke the appropriate event handler.
|
||||
/// </summary>
|
||||
/// <param name="events">The data.</param>
|
||||
internal void Invoke(List<InventoryItemChangedArgs> events) =>
|
||||
Invoke(this.ItemChanged, this.ItemChangedExplicit, events);
|
||||
|
||||
/// <summary>
|
||||
/// Invoke the appropriate event handler.
|
||||
/// </summary>
|
||||
/// <param name="events">The data.</param>
|
||||
internal void Invoke(List<InventoryItemMovedArgs> events) =>
|
||||
Invoke(this.ItemMoved, this.ItemMovedExplicit, events);
|
||||
|
||||
/// <summary>
|
||||
/// Invoke the appropriate event handler.
|
||||
/// </summary>
|
||||
/// <param name="events">The data.</param>
|
||||
internal void Invoke(List<InventoryItemSplitArgs> events) =>
|
||||
Invoke(this.ItemSplit, this.ItemSplitExplicit, events);
|
||||
|
||||
/// <summary>
|
||||
/// Invoke the appropriate event handler.
|
||||
/// </summary>
|
||||
/// <param name="events">The data.</param>
|
||||
internal void Invoke(List<InventoryItemMergedArgs> events) =>
|
||||
Invoke(this.ItemMerged, this.ItemMergedExplicit, events);
|
||||
|
||||
private static void Invoke<T>(
|
||||
IGameInventory.InventoryChangedDelegate? cb,
|
||||
IGameInventory.InventoryChangedDelegate<T>? cbt,
|
||||
List<T> events) where T : InventoryEventArgs
|
||||
{
|
||||
foreach (var evt in events)
|
||||
{
|
||||
try
|
||||
{
|
||||
cb?.Invoke(evt.Type, evt);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(
|
||||
e,
|
||||
"[{plugin}] Exception during untyped callback for {evt}",
|
||||
Service<PluginManager>.GetNullable()?.FindCallingPlugin(new(e))?.Name ?? "(unknown plugin)",
|
||||
evt);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
cbt?.Invoke(evt);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(
|
||||
e,
|
||||
"[{plugin}] Exception during typed callback for {evt}",
|
||||
Service<PluginManager>.GetNullable()?.FindCallingPlugin(new(e))?.Name ?? "(unknown plugin)",
|
||||
evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
Dalamud/Game/Inventory/GameInventoryEvent.cs
Normal file
43
Dalamud/Game/Inventory/GameInventoryEvent.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
namespace Dalamud.Game.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// Class representing a item's changelog state.
|
||||
/// </summary>
|
||||
public enum GameInventoryEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// A value indicating that there was no event.<br />
|
||||
/// You should not see this value, unless you explicitly used it yourself, or APIs using this enum say otherwise.
|
||||
/// </summary>
|
||||
Empty = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Item was added to an inventory.
|
||||
/// </summary>
|
||||
Added = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Item was removed from an inventory.
|
||||
/// </summary>
|
||||
Removed = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Properties are changed for an item in an inventory.
|
||||
/// </summary>
|
||||
Changed = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Item has been moved, possibly across different inventories.
|
||||
/// </summary>
|
||||
Moved = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Item has been split into two stacks from one, possibly across different inventories.
|
||||
/// </summary>
|
||||
Split = 5,
|
||||
|
||||
/// <summary>
|
||||
/// Item has been merged into one stack from two, possibly across different inventories.
|
||||
/// </summary>
|
||||
Merged = 6,
|
||||
}
|
||||
203
Dalamud/Game/Inventory/GameInventoryItem.cs
Normal file
203
Dalamud/Game/Inventory/GameInventoryItem.cs
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
|
||||
namespace Dalamud.Game.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// Dalamud wrapper around a ClientStructs InventoryItem.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Explicit, Size = StructSizeInBytes)]
|
||||
public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
|
||||
{
|
||||
/// <summary>
|
||||
/// The actual data.
|
||||
/// </summary>
|
||||
[FieldOffset(0)]
|
||||
internal readonly InventoryItem InternalItem;
|
||||
|
||||
private const int StructSizeInBytes = 0x38;
|
||||
|
||||
/// <summary>
|
||||
/// The view of the backing data, in <see cref="ulong"/>.
|
||||
/// </summary>
|
||||
[FieldOffset(0)]
|
||||
private fixed ulong dataUInt64[StructSizeInBytes / 0x8];
|
||||
|
||||
static GameInventoryItem()
|
||||
{
|
||||
Debug.Assert(
|
||||
sizeof(InventoryItem) == StructSizeInBytes,
|
||||
$"Definition of {nameof(InventoryItem)} has been changed. " +
|
||||
$"Update {nameof(StructSizeInBytes)} to {sizeof(InventoryItem)} to accommodate for the size change.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameInventoryItem"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="item">Inventory item to wrap.</param>
|
||||
internal GameInventoryItem(InventoryItem item) => this.InternalItem = item;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the this <see cref="GameInventoryItem"/> is empty.
|
||||
/// </summary>
|
||||
public bool IsEmpty => this.InternalItem.ItemID == 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the container inventory type.
|
||||
/// </summary>
|
||||
public GameInventoryType ContainerType => (GameInventoryType)this.InternalItem.Container;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the inventory slot index this item is in.
|
||||
/// </summary>
|
||||
public uint InventorySlot => (uint)this.InternalItem.Slot;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item id.
|
||||
/// </summary>
|
||||
public uint ItemId => this.InternalItem.ItemID;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the quantity of items in this item stack.
|
||||
/// </summary>
|
||||
public uint Quantity => this.InternalItem.Quantity;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the spiritbond of this item.
|
||||
/// </summary>
|
||||
public uint Spiritbond => this.InternalItem.Spiritbond;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repair condition of this item.
|
||||
/// </summary>
|
||||
public uint Condition => this.InternalItem.Condition;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the item is High Quality.
|
||||
/// </summary>
|
||||
public bool IsHq => (this.InternalItem.Flags & InventoryItem.ItemFlags.HQ) != 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the item has a company crest applied.
|
||||
/// </summary>
|
||||
public bool IsCompanyCrestApplied => (this.InternalItem.Flags & InventoryItem.ItemFlags.CompanyCrestApplied) != 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the item is a relic.
|
||||
/// </summary>
|
||||
public bool IsRelic => (this.InternalItem.Flags & InventoryItem.ItemFlags.Relic) != 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the is a collectable.
|
||||
/// </summary>
|
||||
public bool IsCollectable => (this.InternalItem.Flags & InventoryItem.ItemFlags.Collectable) != 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the array of materia types.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<ushort> Materia => new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.Materia[0])), 5);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the array of materia grades.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<ushort> MateriaGrade =>
|
||||
new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.MateriaGrade[0])), 5);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of native inventory item in the game.<br />
|
||||
/// Can be 0 if this instance of <see cref="GameInventoryItem"/> does not point to a valid set of container type and slot.<br />
|
||||
/// Note that this instance of <see cref="GameInventoryItem"/> can be a snapshot; it may not necessarily match the
|
||||
/// data you can query from the game using this address value.
|
||||
/// </summary>
|
||||
public nint Address
|
||||
{
|
||||
get
|
||||
{
|
||||
var s = GetReadOnlySpanOfInventory(this.ContainerType);
|
||||
if (s.IsEmpty)
|
||||
return 0;
|
||||
|
||||
foreach (ref readonly var i in s)
|
||||
{
|
||||
if (i.InventorySlot == this.InventorySlot)
|
||||
return (nint)Unsafe.AsPointer(ref Unsafe.AsRef(in i));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the color used for this item.
|
||||
/// </summary>
|
||||
public byte Stain => this.InternalItem.Stain;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the glamour id for this item.
|
||||
/// </summary>
|
||||
public uint GlamourId => this.InternalItem.GlamourID;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the items crafter's content id.
|
||||
/// NOTE: I'm not sure if this is a good idea to include or not in the dalamud api. Marked internal for now.
|
||||
/// </summary>
|
||||
internal ulong CrafterContentId => this.InternalItem.CrafterContentID;
|
||||
|
||||
public static bool operator ==(in GameInventoryItem l, in GameInventoryItem r) => l.Equals(r);
|
||||
|
||||
public static bool operator !=(in GameInventoryItem l, in GameInventoryItem r) => !l.Equals(r);
|
||||
|
||||
/// <inheritdoc/>
|
||||
readonly bool IEquatable<GameInventoryItem>.Equals(GameInventoryItem other) => this.Equals(other);
|
||||
|
||||
/// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
|
||||
/// <param name="other">An object to compare with this object.</param>
|
||||
/// <returns><c>true</c> if the current object is equal to the <paramref name="other" /> parameter; otherwise, <c>false</c>.</returns>
|
||||
public readonly bool Equals(in GameInventoryItem other)
|
||||
{
|
||||
for (var i = 0; i < StructSizeInBytes / 8; i++)
|
||||
{
|
||||
if (this.dataUInt64[i] != other.dataUInt64[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="object.Equals(object?)" />
|
||||
public override bool Equals(object obj) => obj is GameInventoryItem gii && this.Equals(gii);
|
||||
|
||||
/// <inheritdoc cref="object.GetHashCode" />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var k = 0x5a8447b91aff51b4UL;
|
||||
for (var i = 0; i < StructSizeInBytes / 8; i++)
|
||||
k ^= this.dataUInt64[i];
|
||||
return unchecked((int)(k ^ (k >> 32)));
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="object.ToString"/>
|
||||
public override string ToString() =>
|
||||
this.IsEmpty
|
||||
? "empty"
|
||||
: $"item({this.ItemId}@{this.ContainerType}#{this.InventorySlot})";
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="Span{T}"/> view of <see cref="InventoryItem"/>s, wrapped as <see cref="GameInventoryItem"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The inventory type.</param>
|
||||
/// <returns>The span.</returns>
|
||||
internal static ReadOnlySpan<GameInventoryItem> GetReadOnlySpanOfInventory(GameInventoryType type)
|
||||
{
|
||||
var inventoryManager = InventoryManager.Instance();
|
||||
if (inventoryManager is null) return default;
|
||||
|
||||
var inventory = inventoryManager->GetInventoryContainer((InventoryType)type);
|
||||
if (inventory is null) return default;
|
||||
|
||||
return new ReadOnlySpan<GameInventoryItem>(inventory->Items, (int)inventory->Size);
|
||||
}
|
||||
}
|
||||
356
Dalamud/Game/Inventory/GameInventoryType.cs
Normal file
356
Dalamud/Game/Inventory/GameInventoryType.cs
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
namespace Dalamud.Game.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// Enum representing various player inventories.
|
||||
/// </summary>
|
||||
public enum GameInventoryType : ushort
|
||||
{
|
||||
/// <summary>
|
||||
/// First panel of main player inventory.
|
||||
/// </summary>
|
||||
Inventory1 = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Second panel of main player inventory.
|
||||
/// </summary>
|
||||
Inventory2 = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Third panel of main player inventory.
|
||||
/// </summary>
|
||||
Inventory3 = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Fourth panel of main player inventory.
|
||||
/// </summary>
|
||||
Inventory4 = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Items that are currently equipped by the player.
|
||||
/// </summary>
|
||||
EquippedItems = 1000,
|
||||
|
||||
/// <summary>
|
||||
/// Player currency container.
|
||||
/// ie, gil, serpent seals, sacks of nuts.
|
||||
/// </summary>
|
||||
Currency = 2000,
|
||||
|
||||
/// <summary>
|
||||
/// Crystal container.
|
||||
/// </summary>
|
||||
Crystals = 2001,
|
||||
|
||||
/// <summary>
|
||||
/// Mail container.
|
||||
/// </summary>
|
||||
Mail = 2003,
|
||||
|
||||
/// <summary>
|
||||
/// Key item container.
|
||||
/// </summary>
|
||||
KeyItems = 2004,
|
||||
|
||||
/// <summary>
|
||||
/// Quest item hand-in inventory.
|
||||
/// </summary>
|
||||
HandIn = 2005,
|
||||
|
||||
/// <summary>
|
||||
/// DamagedGear container.
|
||||
/// </summary>
|
||||
DamagedGear = 2007,
|
||||
|
||||
/// <summary>
|
||||
/// Examine window container.
|
||||
/// </summary>
|
||||
Examine = 2009,
|
||||
|
||||
/// <summary>
|
||||
/// Doman Enclave Reconstruction Reclamation Box.
|
||||
/// </summary>
|
||||
ReconstructionBuyback = 2013,
|
||||
|
||||
/// <summary>
|
||||
/// Armory off-hand weapon container.
|
||||
/// </summary>
|
||||
ArmoryOffHand = 3200,
|
||||
|
||||
/// <summary>
|
||||
/// Armory head container.
|
||||
/// </summary>
|
||||
ArmoryHead = 3201,
|
||||
|
||||
/// <summary>
|
||||
/// Armory body container.
|
||||
/// </summary>
|
||||
ArmoryBody = 3202,
|
||||
|
||||
/// <summary>
|
||||
/// Armory hand/gloves container.
|
||||
/// </summary>
|
||||
ArmoryHands = 3203,
|
||||
|
||||
/// <summary>
|
||||
/// Armory waist container.
|
||||
/// <remarks>
|
||||
/// This container should be unused as belt items were removed from the game in Shadowbringers.
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
ArmoryWaist = 3204,
|
||||
|
||||
/// <summary>
|
||||
/// Armory legs/pants/skirt container.
|
||||
/// </summary>
|
||||
ArmoryLegs = 3205,
|
||||
|
||||
/// <summary>
|
||||
/// Armory feet/boots/shoes container.
|
||||
/// </summary>
|
||||
ArmoryFeets = 3206,
|
||||
|
||||
/// <summary>
|
||||
/// Armory earring container.
|
||||
/// </summary>
|
||||
ArmoryEar = 3207,
|
||||
|
||||
/// <summary>
|
||||
/// Armory necklace container.
|
||||
/// </summary>
|
||||
ArmoryNeck = 3208,
|
||||
|
||||
/// <summary>
|
||||
/// Armory bracelet container.
|
||||
/// </summary>
|
||||
ArmoryWrist = 3209,
|
||||
|
||||
/// <summary>
|
||||
/// Armory ring container.
|
||||
/// </summary>
|
||||
ArmoryRings = 3300,
|
||||
|
||||
/// <summary>
|
||||
/// Armory soul crystal container.
|
||||
/// </summary>
|
||||
ArmorySoulCrystal = 3400,
|
||||
|
||||
/// <summary>
|
||||
/// Armory main-hand weapon container.
|
||||
/// </summary>
|
||||
ArmoryMainHand = 3500,
|
||||
|
||||
/// <summary>
|
||||
/// First panel of saddelbag inventory.
|
||||
/// </summary>
|
||||
SaddleBag1 = 4000,
|
||||
|
||||
/// <summary>
|
||||
/// Second panel of Saddlebag inventory.
|
||||
/// </summary>
|
||||
SaddleBag2 = 4001,
|
||||
|
||||
/// <summary>
|
||||
/// First panel of premium saddlebag inventory.
|
||||
/// </summary>
|
||||
PremiumSaddleBag1 = 4100,
|
||||
|
||||
/// <summary>
|
||||
/// Second panel of premium saddlebag inventory.
|
||||
/// </summary>
|
||||
PremiumSaddleBag2 = 4101,
|
||||
|
||||
/// <summary>
|
||||
/// First panel of retainer inventory.
|
||||
/// </summary>
|
||||
RetainerPage1 = 10000,
|
||||
|
||||
/// <summary>
|
||||
/// Second panel of retainer inventory.
|
||||
/// </summary>
|
||||
RetainerPage2 = 10001,
|
||||
|
||||
/// <summary>
|
||||
/// Third panel of retainer inventory.
|
||||
/// </summary>
|
||||
RetainerPage3 = 10002,
|
||||
|
||||
/// <summary>
|
||||
/// Fourth panel of retainer inventory.
|
||||
/// </summary>
|
||||
RetainerPage4 = 10003,
|
||||
|
||||
/// <summary>
|
||||
/// Fifth panel of retainer inventory.
|
||||
/// </summary>
|
||||
RetainerPage5 = 10004,
|
||||
|
||||
/// <summary>
|
||||
/// Sixth panel of retainer inventory.
|
||||
/// </summary>
|
||||
RetainerPage6 = 10005,
|
||||
|
||||
/// <summary>
|
||||
/// Seventh panel of retainer inventory.
|
||||
/// </summary>
|
||||
RetainerPage7 = 10006,
|
||||
|
||||
/// <summary>
|
||||
/// Retainer equipment container.
|
||||
/// </summary>
|
||||
RetainerEquippedItems = 11000,
|
||||
|
||||
/// <summary>
|
||||
/// Retainer currency container.
|
||||
/// </summary>
|
||||
RetainerGil = 12000,
|
||||
|
||||
/// <summary>
|
||||
/// Retainer crystal container.
|
||||
/// </summary>
|
||||
RetainerCrystals = 12001,
|
||||
|
||||
/// <summary>
|
||||
/// Retainer market item container.
|
||||
/// </summary>
|
||||
RetainerMarket = 12002,
|
||||
|
||||
/// <summary>
|
||||
/// First panel of Free Company inventory.
|
||||
/// </summary>
|
||||
FreeCompanyPage1 = 20000,
|
||||
|
||||
/// <summary>
|
||||
/// Second panel of Free Company inventory.
|
||||
/// </summary>
|
||||
FreeCompanyPage2 = 20001,
|
||||
|
||||
/// <summary>
|
||||
/// Third panel of Free Company inventory.
|
||||
/// </summary>
|
||||
FreeCompanyPage3 = 20002,
|
||||
|
||||
/// <summary>
|
||||
/// Fourth panel of Free Company inventory.
|
||||
/// </summary>
|
||||
FreeCompanyPage4 = 20003,
|
||||
|
||||
/// <summary>
|
||||
/// Fifth panel of Free Company inventory.
|
||||
/// </summary>
|
||||
FreeCompanyPage5 = 20004,
|
||||
|
||||
/// <summary>
|
||||
/// Free Company currency container.
|
||||
/// </summary>
|
||||
FreeCompanyGil = 22000,
|
||||
|
||||
/// <summary>
|
||||
/// Free Company crystal container.
|
||||
/// </summary>
|
||||
FreeCompanyCrystals = 22001,
|
||||
|
||||
/// <summary>
|
||||
/// Housing exterior appearance container.
|
||||
/// </summary>
|
||||
HousingExteriorAppearance = 25000,
|
||||
|
||||
/// <summary>
|
||||
/// Housing exterior placed items container.
|
||||
/// </summary>
|
||||
HousingExteriorPlacedItems = 25001,
|
||||
|
||||
/// <summary>
|
||||
/// Housing interior appearance container.
|
||||
/// </summary>
|
||||
HousingInteriorAppearance = 25002,
|
||||
|
||||
/// <summary>
|
||||
/// First panel of housing interior inventory.
|
||||
/// </summary>
|
||||
HousingInteriorPlacedItems1 = 25003,
|
||||
|
||||
/// <summary>
|
||||
/// Second panel of housing interior inventory.
|
||||
/// </summary>
|
||||
HousingInteriorPlacedItems2 = 25004,
|
||||
|
||||
/// <summary>
|
||||
/// Third panel of housing interior inventory.
|
||||
/// </summary>
|
||||
HousingInteriorPlacedItems3 = 25005,
|
||||
|
||||
/// <summary>
|
||||
/// Fourth panel of housing interior inventory.
|
||||
/// </summary>
|
||||
HousingInteriorPlacedItems4 = 25006,
|
||||
|
||||
/// <summary>
|
||||
/// Fifth panel of housing interior inventory.
|
||||
/// </summary>
|
||||
HousingInteriorPlacedItems5 = 25007,
|
||||
|
||||
/// <summary>
|
||||
/// Sixth panel of housing interior inventory.
|
||||
/// </summary>
|
||||
HousingInteriorPlacedItems6 = 25008,
|
||||
|
||||
/// <summary>
|
||||
/// Seventh panel of housing interior inventory.
|
||||
/// </summary>
|
||||
HousingInteriorPlacedItems7 = 25009,
|
||||
|
||||
/// <summary>
|
||||
/// Eighth panel of housing interior inventory.
|
||||
/// </summary>
|
||||
HousingInteriorPlacedItems8 = 25010,
|
||||
|
||||
/// <summary>
|
||||
/// Housing exterior storeroom inventory.
|
||||
/// </summary>
|
||||
HousingExteriorStoreroom = 27000,
|
||||
|
||||
/// <summary>
|
||||
/// First panel of housing interior storeroom inventory.
|
||||
/// </summary>
|
||||
HousingInteriorStoreroom1 = 27001,
|
||||
|
||||
/// <summary>
|
||||
/// Second panel of housing interior storeroom inventory.
|
||||
/// </summary>
|
||||
HousingInteriorStoreroom2 = 27002,
|
||||
|
||||
/// <summary>
|
||||
/// Third panel of housing interior storeroom inventory.
|
||||
/// </summary>
|
||||
HousingInteriorStoreroom3 = 27003,
|
||||
|
||||
/// <summary>
|
||||
/// Fourth panel of housing interior storeroom inventory.
|
||||
/// </summary>
|
||||
HousingInteriorStoreroom4 = 27004,
|
||||
|
||||
/// <summary>
|
||||
/// Fifth panel of housing interior storeroom inventory.
|
||||
/// </summary>
|
||||
HousingInteriorStoreroom5 = 27005,
|
||||
|
||||
/// <summary>
|
||||
/// Sixth panel of housing interior storeroom inventory.
|
||||
/// </summary>
|
||||
HousingInteriorStoreroom6 = 27006,
|
||||
|
||||
/// <summary>
|
||||
/// Seventh panel of housing interior storeroom inventory.
|
||||
/// </summary>
|
||||
HousingInteriorStoreroom7 = 27007,
|
||||
|
||||
/// <summary>
|
||||
/// Eighth panel of housing interior storeroom inventory.
|
||||
/// </summary>
|
||||
HousingInteriorStoreroom8 = 27008,
|
||||
|
||||
/// <summary>
|
||||
/// An invalid value.
|
||||
/// </summary>
|
||||
Invalid = ushort.MaxValue,
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
namespace Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the data associated with an item being affected across different slots, possibly in different containers.
|
||||
/// </summary>
|
||||
public abstract class InventoryComplexEventArgs : InventoryEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InventoryComplexEventArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="type">Type of the event.</param>
|
||||
/// <param name="sourceEvent">The item at before slot.</param>
|
||||
/// <param name="targetEvent">The item at after slot.</param>
|
||||
internal InventoryComplexEventArgs(
|
||||
GameInventoryEvent type, InventoryEventArgs sourceEvent, InventoryEventArgs targetEvent)
|
||||
: base(type, targetEvent.Item)
|
||||
{
|
||||
this.SourceEvent = sourceEvent;
|
||||
this.TargetEvent = targetEvent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the inventory this item was at.
|
||||
/// </summary>
|
||||
public GameInventoryType SourceInventory => this.SourceEvent.Item.ContainerType;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the inventory this item now is.
|
||||
/// </summary>
|
||||
public GameInventoryType TargetInventory => this.Item.ContainerType;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the slot this item was at.
|
||||
/// </summary>
|
||||
public uint SourceSlot => this.SourceEvent.Item.InventorySlot;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the slot this item now is.
|
||||
/// </summary>
|
||||
public uint TargetSlot => this.Item.InventorySlot;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the associated source event.
|
||||
/// </summary>
|
||||
public InventoryEventArgs SourceEvent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the associated target event.
|
||||
/// </summary>
|
||||
public InventoryEventArgs TargetEvent { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => $"{this.Type}({this.SourceEvent}, {this.TargetEvent})";
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
namespace Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract base class representing inventory changed events.
|
||||
/// </summary>
|
||||
public abstract class InventoryEventArgs
|
||||
{
|
||||
private readonly GameInventoryItem item;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InventoryEventArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="type">Type of the event.</param>
|
||||
/// <param name="item">Item about the event.</param>
|
||||
protected InventoryEventArgs(GameInventoryEvent type, in GameInventoryItem item)
|
||||
{
|
||||
this.Type = type;
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of event for these args.
|
||||
/// </summary>
|
||||
public GameInventoryEvent Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item associated with this event.
|
||||
/// <remarks><em>This is a copy of the item data.</em></remarks>
|
||||
/// </summary>
|
||||
// impl note: we return a ref readonly view, to avoid making copies every time this property is accessed.
|
||||
// see: https://devblogs.microsoft.com/premier-developer/avoiding-struct-and-readonly-reference-performance-pitfalls-with-errorprone-net/
|
||||
// "Consider using ref readonly locals and ref return for library code"
|
||||
public ref readonly GameInventoryItem Item => ref this.item;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => $"{this.Type}({this.Item})";
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
namespace Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the data associated with an item being added to an inventory.
|
||||
/// </summary>
|
||||
public sealed class InventoryItemAddedArgs : InventoryEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InventoryItemAddedArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
internal InventoryItemAddedArgs(in GameInventoryItem item)
|
||||
: base(GameInventoryEvent.Added, item)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the inventory this item was added to.
|
||||
/// </summary>
|
||||
public GameInventoryType Inventory => this.Item.ContainerType;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the slot this item was added to.
|
||||
/// </summary>
|
||||
public uint Slot => this.Item.InventorySlot;
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
namespace Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the data associated with an items properties being changed.
|
||||
/// This also includes an items stack count changing.
|
||||
/// </summary>
|
||||
public sealed class InventoryItemChangedArgs : InventoryEventArgs
|
||||
{
|
||||
private readonly GameInventoryItem oldItemState;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InventoryItemChangedArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="oldItem">The item before change.</param>
|
||||
/// <param name="newItem">The item after change.</param>
|
||||
internal InventoryItemChangedArgs(in GameInventoryItem oldItem, in GameInventoryItem newItem)
|
||||
: base(GameInventoryEvent.Changed, newItem)
|
||||
{
|
||||
this.oldItemState = oldItem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the inventory this item is in.
|
||||
/// </summary>
|
||||
public GameInventoryType Inventory => this.Item.ContainerType;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the inventory slot this item is in.
|
||||
/// </summary>
|
||||
public uint Slot => this.Item.InventorySlot;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the state of the item from before it was changed.
|
||||
/// <remarks><em>This is a copy of the item data.</em></remarks>
|
||||
/// </summary>
|
||||
// impl note: see InventoryEventArgs.Item.
|
||||
public ref readonly GameInventoryItem OldItemState => ref this.oldItemState;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
namespace Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the data associated with an item being merged from two stacks into one.
|
||||
/// </summary>
|
||||
public sealed class InventoryItemMergedArgs : InventoryComplexEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InventoryItemMergedArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="sourceEvent">The item at before slot.</param>
|
||||
/// <param name="targetEvent">The item at after slot.</param>
|
||||
internal InventoryItemMergedArgs(InventoryEventArgs sourceEvent, InventoryEventArgs targetEvent)
|
||||
: base(GameInventoryEvent.Merged, sourceEvent, targetEvent)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() =>
|
||||
this.TargetEvent is InventoryItemChangedArgs iica
|
||||
? $"{this.Type}(" +
|
||||
$"item({this.Item.ItemId}), " +
|
||||
$"{this.SourceInventory}#{this.SourceSlot}({this.SourceEvent.Item.Quantity} to 0), " +
|
||||
$"{this.TargetInventory}#{this.TargetSlot}({iica.OldItemState.Quantity} to {iica.Item.Quantity}))"
|
||||
: base.ToString();
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
namespace Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the data associated with an item being moved from one inventory and added to another.
|
||||
/// </summary>
|
||||
public sealed class InventoryItemMovedArgs : InventoryComplexEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InventoryItemMovedArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="sourceEvent">The item at before slot.</param>
|
||||
/// <param name="targetEvent">The item at after slot.</param>
|
||||
internal InventoryItemMovedArgs(InventoryEventArgs sourceEvent, InventoryEventArgs targetEvent)
|
||||
: base(GameInventoryEvent.Moved, sourceEvent, targetEvent)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() =>
|
||||
$"{this.Type}(item({this.Item.ItemId}) from {this.SourceInventory}#{this.SourceSlot} to {this.TargetInventory}#{this.TargetSlot})";
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
namespace Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the data associated with an item being removed from an inventory.
|
||||
/// </summary>
|
||||
public sealed class InventoryItemRemovedArgs : InventoryEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InventoryItemRemovedArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
internal InventoryItemRemovedArgs(in GameInventoryItem item)
|
||||
: base(GameInventoryEvent.Removed, item)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the inventory this item was removed from.
|
||||
/// </summary>
|
||||
public GameInventoryType Inventory => this.Item.ContainerType;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the slot this item was removed from.
|
||||
/// </summary>
|
||||
public uint Slot => this.Item.InventorySlot;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
namespace Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the data associated with an item being split from one stack into two.
|
||||
/// </summary>
|
||||
public sealed class InventoryItemSplitArgs : InventoryComplexEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InventoryItemSplitArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="sourceEvent">The item at before slot.</param>
|
||||
/// <param name="targetEvent">The item at after slot.</param>
|
||||
internal InventoryItemSplitArgs(InventoryEventArgs sourceEvent, InventoryEventArgs targetEvent)
|
||||
: base(GameInventoryEvent.Split, sourceEvent, targetEvent)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() =>
|
||||
this.SourceEvent is InventoryItemChangedArgs iica
|
||||
? $"{this.Type}(" +
|
||||
$"item({this.Item.ItemId}), " +
|
||||
$"{this.SourceInventory}#{this.SourceSlot}({iica.OldItemState.Quantity} to {iica.Item.Quantity}), " +
|
||||
$"{this.TargetInventory}#{this.TargetSlot}(0 to {this.Item.Quantity}))"
|
||||
: base.ToString();
|
||||
}
|
||||
|
|
@ -44,6 +44,9 @@ internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork
|
|||
|
||||
this.processZonePacketDownHook = Hook<ProcessZonePacketDownDelegate>.FromAddress(this.address.ProcessZonePacketDown, this.ProcessZonePacketDownDetour);
|
||||
this.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;
|
||||
|
|
|
|||
199
Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs
Normal file
199
Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
using CheapLoc;
|
||||
|
||||
using Dalamud.Game.Gui.Toast;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Logging.Internal;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
using static TerraFX.Interop.Windows.Windows;
|
||||
|
||||
namespace Dalamud.Interface.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Configures the ImGui clipboard behaviour to work nicely with XIV.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// XIV uses '\r' for line endings and will truncate all text after a '\n' character.
|
||||
/// This means that copy/pasting multi-line text from ImGui to XIV will only copy the first line.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// ImGui uses '\n' for line endings and will ignore '\r' entirely.
|
||||
/// This means that copy/pasting multi-line text from XIV to ImGui will copy all the text
|
||||
/// without line breaks.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// To fix this we normalize all clipboard line endings entering/exiting ImGui to '\r\n' which
|
||||
/// works for both ImGui and XIV.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDisposable
|
||||
{
|
||||
private static readonly ModuleLog Log = new(nameof(ImGuiClipboardFunctionProvider));
|
||||
private readonly nint clipboardUserDataOriginal;
|
||||
private readonly nint setTextOriginal;
|
||||
private readonly nint getTextOriginal;
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly ToastGui toastGui = Service<ToastGui>.Get();
|
||||
|
||||
private ImVectorWrapper<byte> clipboardData;
|
||||
private GCHandle clipboardUserData;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private ImGuiClipboardFunctionProvider(InterfaceManager.InterfaceManagerWithScene imws)
|
||||
{
|
||||
// Effectively waiting for ImGui to become available.
|
||||
_ = imws;
|
||||
Debug.Assert(ImGuiHelpers.IsImGuiInitialized, "IMWS initialized but IsImGuiInitialized is false?");
|
||||
|
||||
var io = ImGui.GetIO();
|
||||
this.clipboardUserDataOriginal = io.ClipboardUserData;
|
||||
this.setTextOriginal = io.SetClipboardTextFn;
|
||||
this.getTextOriginal = io.GetClipboardTextFn;
|
||||
io.ClipboardUserData = GCHandle.ToIntPtr(this.clipboardUserData = GCHandle.Alloc(this));
|
||||
io.SetClipboardTextFn = (nint)(delegate* unmanaged<nint, byte*, void>)&StaticSetClipboardTextImpl;
|
||||
io.GetClipboardTextFn = (nint)(delegate* unmanaged<nint, byte*>)&StaticGetClipboardTextImpl;
|
||||
|
||||
this.clipboardData = new(0);
|
||||
return;
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
static void StaticSetClipboardTextImpl(nint userData, byte* text) =>
|
||||
((ImGuiClipboardFunctionProvider)GCHandle.FromIntPtr(userData).Target)!.SetClipboardTextImpl(text);
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
static byte* StaticGetClipboardTextImpl(nint userData) =>
|
||||
((ImGuiClipboardFunctionProvider)GCHandle.FromIntPtr(userData).Target)!.GetClipboardTextImpl();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (!this.clipboardUserData.IsAllocated)
|
||||
return;
|
||||
|
||||
var io = ImGui.GetIO();
|
||||
io.SetClipboardTextFn = this.setTextOriginal;
|
||||
io.GetClipboardTextFn = this.getTextOriginal;
|
||||
io.ClipboardUserData = this.clipboardUserDataOriginal;
|
||||
|
||||
this.clipboardUserData.Free();
|
||||
this.clipboardData.Dispose();
|
||||
}
|
||||
|
||||
private bool OpenClipboardOrShowError()
|
||||
{
|
||||
if (!OpenClipboard(default))
|
||||
{
|
||||
this.toastGui.ShowError(
|
||||
Loc.Localize(
|
||||
"ImGuiClipboardFunctionProviderClipboardInUse",
|
||||
"Some other application is using the clipboard. Try again later."));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void SetClipboardTextImpl(byte* text)
|
||||
{
|
||||
if (!this.OpenClipboardOrShowError())
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var len = 0;
|
||||
while (text[len] != 0)
|
||||
len++;
|
||||
var str = Encoding.UTF8.GetString(text, len);
|
||||
str = str.ReplaceLineEndings("\r\n");
|
||||
var hMem = GlobalAlloc(GMEM.GMEM_MOVEABLE, (nuint)((str.Length + 1) * 2));
|
||||
if (hMem == 0)
|
||||
throw new OutOfMemoryException();
|
||||
|
||||
var ptr = (char*)GlobalLock(hMem);
|
||||
if (ptr == null)
|
||||
{
|
||||
throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error())
|
||||
?? throw new InvalidOperationException($"{nameof(GlobalLock)} failed.");
|
||||
}
|
||||
|
||||
str.AsSpan().CopyTo(new(ptr, str.Length));
|
||||
ptr[str.Length] = default;
|
||||
GlobalUnlock(hMem);
|
||||
|
||||
SetClipboardData(CF.CF_UNICODETEXT, hMem);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, $"Error in {nameof(this.SetClipboardTextImpl)}");
|
||||
this.toastGui.ShowError(
|
||||
Loc.Localize(
|
||||
"ImGuiClipboardFunctionProviderErrorCopy",
|
||||
"Failed to copy. See logs for details."));
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseClipboard();
|
||||
}
|
||||
}
|
||||
|
||||
private byte* GetClipboardTextImpl()
|
||||
{
|
||||
this.clipboardData.Clear();
|
||||
|
||||
var formats = stackalloc uint[] { CF.CF_UNICODETEXT, CF.CF_TEXT };
|
||||
if (GetPriorityClipboardFormat(formats, 2) < 1 || !this.OpenClipboardOrShowError())
|
||||
{
|
||||
this.clipboardData.Add(0);
|
||||
return this.clipboardData.Data;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var hMem = (HGLOBAL)GetClipboardData(CF.CF_UNICODETEXT);
|
||||
if (hMem != default)
|
||||
{
|
||||
var ptr = (char*)GlobalLock(hMem);
|
||||
if (ptr == null)
|
||||
{
|
||||
throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error())
|
||||
?? throw new InvalidOperationException($"{nameof(GlobalLock)} failed.");
|
||||
}
|
||||
|
||||
var str = new string(ptr);
|
||||
str = str.ReplaceLineEndings("\r\n");
|
||||
this.clipboardData.Resize(Encoding.UTF8.GetByteCount(str) + 1);
|
||||
Encoding.UTF8.GetBytes(str, this.clipboardData.DataSpan);
|
||||
this.clipboardData[^1] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.clipboardData.Add(0);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, $"Error in {nameof(this.GetClipboardTextImpl)}");
|
||||
this.toastGui.ShowError(
|
||||
Loc.Localize(
|
||||
"ImGuiClipboardFunctionProviderErrorPaste",
|
||||
"Failed to paste. See logs for details."));
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseClipboard();
|
||||
}
|
||||
|
||||
return this.clipboardData.Data;
|
||||
}
|
||||
}
|
||||
|
|
@ -1063,14 +1063,10 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
}
|
||||
}
|
||||
|
||||
[ServiceManager.CallWhenServicesReady]
|
||||
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(() =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ internal class DataWindow : Window
|
|||
new FateTableWidget(),
|
||||
new FlyTextWidget(),
|
||||
new FontAwesomeTestWidget(),
|
||||
new GameInventoryTestWidget(),
|
||||
new GamepadWidget(),
|
||||
new GaugeWidget(),
|
||||
new HookWidget(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,163 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Game.Inventory;
|
||||
using Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Logging.Internal;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Dalamud.Interface.Internal.Windows.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Tester for <see cref="GameInventory"/>.
|
||||
/// </summary>
|
||||
internal class GameInventoryTestWidget : IDataWindowWidget
|
||||
{
|
||||
private static readonly ModuleLog Log = new(nameof(GameInventoryTestWidget));
|
||||
|
||||
private GameInventoryPluginScoped? scoped;
|
||||
private bool standardEnabled;
|
||||
private bool rawEnabled;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string[]? CommandShortcuts { get; init; } = { "gameinventorytest" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "GameInventory Test";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Load() => this.Ready = true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Draw()
|
||||
{
|
||||
if (Service<DalamudConfiguration>.Get().LogLevel > LogEventLevel.Information)
|
||||
{
|
||||
ImGuiHelpers.SafeTextColoredWrapped(
|
||||
ImGuiColors.DalamudRed,
|
||||
"Enable LogLevel=Information display to see the logs.");
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table(this.DisplayName, 3, ImGuiTableFlags.SizingFixedFit);
|
||||
if (!table.Success)
|
||||
return;
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted("Standard Logging");
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
using (ImRaii.Disabled(this.standardEnabled))
|
||||
{
|
||||
if (ImGui.Button("Enable##standard-enable") && !this.standardEnabled)
|
||||
{
|
||||
this.scoped ??= new();
|
||||
this.scoped.InventoryChanged += ScopedOnInventoryChanged;
|
||||
this.standardEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
using (ImRaii.Disabled(!this.standardEnabled))
|
||||
{
|
||||
if (ImGui.Button("Disable##standard-disable") && this.scoped is not null && this.standardEnabled)
|
||||
{
|
||||
this.scoped.InventoryChanged -= ScopedOnInventoryChanged;
|
||||
this.standardEnabled = false;
|
||||
if (!this.rawEnabled)
|
||||
{
|
||||
this.scoped.Dispose();
|
||||
this.scoped = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.TableNextRow();
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted("Raw Logging");
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
using (ImRaii.Disabled(this.rawEnabled))
|
||||
{
|
||||
if (ImGui.Button("Enable##raw-enable") && !this.rawEnabled)
|
||||
{
|
||||
this.scoped ??= new();
|
||||
this.scoped.InventoryChangedRaw += ScopedOnInventoryChangedRaw;
|
||||
this.rawEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
using (ImRaii.Disabled(!this.rawEnabled))
|
||||
{
|
||||
if (ImGui.Button("Disable##raw-disable") && this.scoped is not null && this.rawEnabled)
|
||||
{
|
||||
this.scoped.InventoryChangedRaw -= ScopedOnInventoryChangedRaw;
|
||||
this.rawEnabled = false;
|
||||
if (!this.standardEnabled)
|
||||
{
|
||||
this.scoped.Dispose();
|
||||
this.scoped = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.TableNextRow();
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted("All");
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
using (ImRaii.Disabled(this.standardEnabled && this.rawEnabled))
|
||||
{
|
||||
if (ImGui.Button("Enable##all-enable"))
|
||||
{
|
||||
this.scoped ??= new();
|
||||
if (!this.standardEnabled)
|
||||
this.scoped.InventoryChanged += ScopedOnInventoryChanged;
|
||||
if (!this.rawEnabled)
|
||||
this.scoped.InventoryChangedRaw += ScopedOnInventoryChangedRaw;
|
||||
this.standardEnabled = this.rawEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
using (ImRaii.Disabled(this.scoped is null))
|
||||
{
|
||||
if (ImGui.Button("Disable##all-disable"))
|
||||
{
|
||||
this.scoped?.Dispose();
|
||||
this.scoped = null;
|
||||
this.standardEnabled = this.rawEnabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ScopedOnInventoryChangedRaw(IReadOnlyCollection<InventoryEventArgs> events)
|
||||
{
|
||||
var i = 0;
|
||||
foreach (var e in events)
|
||||
Log.Information($"[{++i}/{events.Count}] Raw: {e}");
|
||||
}
|
||||
|
||||
private static void ScopedOnInventoryChanged(IReadOnlyCollection<InventoryEventArgs> events)
|
||||
{
|
||||
var i = 0;
|
||||
foreach (var e in events)
|
||||
{
|
||||
if (e is InventoryComplexEventArgs icea)
|
||||
Log.Information($"[{++i}/{events.Count}] {icea}\n\t├ {icea.SourceEvent}\n\t└ {icea.TargetEvent}");
|
||||
else
|
||||
Log.Information($"[{++i}/{events.Count}] {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +1,44 @@
|
|||
using Dalamud.Interface.Utility;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Plugin.Ipc.Internal;
|
||||
|
||||
using 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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Support;
|
||||
using Dalamud.Utility.Timing;
|
||||
|
||||
namespace Dalamud.Plugin.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Class responsible for loading plugins on startup.
|
||||
/// </summary>
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
public class StartupPluginLoader : IServiceType
|
||||
{
|
||||
private static readonly ModuleLog Log = new("SPL");
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private StartupPluginLoader(PluginManager pluginManager)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (Timings.Start("PM Load Plugin Repos"))
|
||||
{
|
||||
_ = pluginManager.SetPluginReposFromConfigAsync(false);
|
||||
pluginManager.OnInstalledPluginsChanged += () => Task.Run(Troubleshooting.LogTroubleshooting);
|
||||
|
||||
Log.Information("[T3] PM repos OK!");
|
||||
}
|
||||
|
||||
using (Timings.Start("PM Cleanup Plugins"))
|
||||
{
|
||||
pluginManager.CleanupPlugins();
|
||||
Log.Information("[T3] PMC OK!");
|
||||
}
|
||||
|
||||
using (Timings.Start("PM Load Sync Plugins"))
|
||||
{
|
||||
pluginManager.LoadAllPlugins().Wait();
|
||||
Log.Information("[T3] PML OK!");
|
||||
}
|
||||
|
||||
Task.Run(Troubleshooting.LogTroubleshooting);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Plugin load failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
106
Dalamud/Plugin/Services/IGameInventory.cs
Normal file
106
Dalamud/Plugin/Services/IGameInventory.cs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Game.Inventory;
|
||||
using Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||
|
||||
namespace Dalamud.Plugin.Services;
|
||||
|
||||
/// <summary>
|
||||
/// This class provides events for the in-game inventory.
|
||||
/// </summary>
|
||||
public interface IGameInventory
|
||||
{
|
||||
/// <summary>
|
||||
/// Delegate function to be called when inventories have been changed.
|
||||
/// This delegate sends the entire set of changes recorded.
|
||||
/// </summary>
|
||||
/// <param name="events">The events.</param>
|
||||
public delegate void InventoryChangelogDelegate(IReadOnlyCollection<InventoryEventArgs> events);
|
||||
|
||||
/// <summary>
|
||||
/// Delegate function to be called for each change to inventories.
|
||||
/// This delegate sends individual events for changes.
|
||||
/// </summary>
|
||||
/// <param name="type">The event try that triggered this message.</param>
|
||||
/// <param name="data">Data for the triggered event.</param>
|
||||
public delegate void InventoryChangedDelegate(GameInventoryEvent type, InventoryEventArgs data);
|
||||
|
||||
/// <summary>
|
||||
/// Delegate function to be called for each change to inventories.
|
||||
/// This delegate sends individual events for changes.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The event arg type.</typeparam>
|
||||
/// <param name="data">Data for the triggered event.</param>
|
||||
public delegate void InventoryChangedDelegate<in T>(T data) where T : InventoryEventArgs;
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired when the inventory has been changed.<br />
|
||||
/// Note that some events, such as <see cref="ItemAdded"/>, <see cref="ItemRemoved"/>, and <see cref="ItemChanged"/>
|
||||
/// currently is subject to reinterpretation as <see cref="ItemMoved"/>, <see cref="ItemMerged"/>, and
|
||||
/// <see cref="ItemSplit"/>.<br />
|
||||
/// Use <see cref="InventoryChangedRaw"/> if you do not want such reinterpretation.
|
||||
/// </summary>
|
||||
public event InventoryChangelogDelegate InventoryChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired when the inventory has been changed, without trying to interpret two inventory slot changes
|
||||
/// as a move event as appropriate.<br />
|
||||
/// In other words, <see cref="GameInventoryEvent.Moved"/>, <see cref="GameInventoryEvent.Merged"/>, and
|
||||
/// <see cref="GameInventoryEvent.Split"/> currently do not fire in this event.
|
||||
/// </summary>
|
||||
public event InventoryChangelogDelegate InventoryChangedRaw;
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired when an item is added to an inventory.<br />
|
||||
/// If this event is a part of multi-step event, then this event will not be called.<br />
|
||||
/// Use <see cref="InventoryChangedRaw"/> if you do not want such reinterpretation.
|
||||
/// </summary>
|
||||
public event InventoryChangedDelegate ItemAdded;
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired when an item is removed from an inventory.<br />
|
||||
/// If this event is a part of multi-step event, then this event will not be called.<br />
|
||||
/// Use <see cref="InventoryChangedRaw"/> if you do not want such reinterpretation.
|
||||
/// </summary>
|
||||
public event InventoryChangedDelegate ItemRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired when an items properties are changed.<br />
|
||||
/// If this event is a part of multi-step event, then this event will not be called.<br />
|
||||
/// Use <see cref="InventoryChangedRaw"/> if you do not want such reinterpretation.
|
||||
/// </summary>
|
||||
public event InventoryChangedDelegate ItemChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired when an item is moved from one inventory into another.
|
||||
/// </summary>
|
||||
public event InventoryChangedDelegate ItemMoved;
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired when an item is split from one stack into two.
|
||||
/// </summary>
|
||||
public event InventoryChangedDelegate ItemSplit;
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired when an item is merged from two stacks into one.
|
||||
/// </summary>
|
||||
public event InventoryChangedDelegate ItemMerged;
|
||||
|
||||
/// <inheritdoc cref="ItemAdded"/>
|
||||
public event InventoryChangedDelegate<InventoryItemAddedArgs> ItemAddedExplicit;
|
||||
|
||||
/// <inheritdoc cref="ItemRemoved"/>
|
||||
public event InventoryChangedDelegate<InventoryItemRemovedArgs> ItemRemovedExplicit;
|
||||
|
||||
/// <inheritdoc cref="ItemChanged"/>
|
||||
public event InventoryChangedDelegate<InventoryItemChangedArgs> ItemChangedExplicit;
|
||||
|
||||
/// <inheritdoc cref="ItemMoved"/>
|
||||
public event InventoryChangedDelegate<InventoryItemMovedArgs> ItemMovedExplicit;
|
||||
|
||||
/// <inheritdoc cref="ItemSplit"/>
|
||||
public event InventoryChangedDelegate<InventoryItemSplitArgs> ItemSplitExplicit;
|
||||
|
||||
/// <inheritdoc cref="ItemMerged"/>
|
||||
public event InventoryChangedDelegate<InventoryItemMergedArgs> ItemMergedExplicit;
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ using Dalamud.Game;
|
|||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.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<T>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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue