diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index d78c87d68..2c564cc81 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -92,7 +92,7 @@ jobs:
foreach ($file in $FILES_TO_VALIDATE) {
$testout = ""
Write-Output "::group::=== API COMPATIBILITY CHECK: ${file} ==="
- apicompat -l "left\${file}" -r "right\${file}" | Tee-Object -Variable testout
+ apicompat -l "left\${file}" -r "right\${file}" --noWarn "CP0006" | Tee-Object -Variable testout
Write-Output "::endgroup::"
if ($testout -ne "APICompat ran successfully without finding any breaking changes.") {
Write-Output "::error::${file} did not pass. Please review it for problems."
@@ -137,6 +137,7 @@ jobs:
$newVersion = [System.IO.File]::ReadAllText("$(Get-Location)\scratch\TEMP_gitver.txt")
$revision = [System.IO.File]::ReadAllText("$(Get-Location)\scratch\revision.txt")
+ $commitHash = [System.IO.File]::ReadAllText("$(Get-Location)\scratch\commit_hash.txt")
Remove-Item -Force -Recurse .\scratch
if (Test-Path -Path $branchName) {
@@ -147,7 +148,7 @@ jobs:
} else {
Move-Item -Force ".\canary.zip" ".\${branchName}\latest.zip"
$versionData.AssemblyVersion = $newVersion
- $versionData | add-member -Force -Name "GitSha" $newVersion -MemberType NoteProperty
+ $versionData | add-member -Force -Name "GitSha" $commitHash -MemberType NoteProperty
$versionData | add-member -Force -Name "Revision" $revision -MemberType NoteProperty
$versionData | ConvertTo-Json -Compress | Out-File ".\${branchName}\version"
}
diff --git a/.github/workflows/rollup.yml b/.github/workflows/rollup.yml
index c3aba2a63..70a8fc7b7 100644
--- a/.github/workflows/rollup.yml
+++ b/.github/workflows/rollup.yml
@@ -11,8 +11,7 @@ jobs:
strategy:
matrix:
branches:
- - new_im_hooks
-# - apiX
+ - WORKFLOW_DISABLED_REMOVE_BEFORE_RUNNING
defaults:
run:
diff --git a/Dalamud.Boot/Dalamud.Boot.rc b/Dalamud.Boot/Dalamud.Boot.rc
index daa41a282..b46e81caf 100644
--- a/Dalamud.Boot/Dalamud.Boot.rc
+++ b/Dalamud.Boot/Dalamud.Boot.rc
@@ -12,6 +12,24 @@
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+#pragma code_page(1252)
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// RT_MANIFEST
+//
+
+RT_MANIFEST_THEMES RT_MANIFEST "themes.manifest"
+
+#endif // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
/////////////////////////////////////////////////////////////////////////////
// English (United Kingdom) resources
diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj b/Dalamud.Boot/Dalamud.Boot.vcxproj
index 298edbcbc..80435cd67 100644
--- a/Dalamud.Boot/Dalamud.Boot.vcxproj
+++ b/Dalamud.Boot/Dalamud.Boot.vcxproj
@@ -197,8 +197,11 @@
+
+
+
-
+
\ No newline at end of file
diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters
index 87eaf6fcc..7c26b28ff 100644
--- a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters
+++ b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters
@@ -163,4 +163,7 @@
Dalamud.Boot DLL
+
+
+
\ No newline at end of file
diff --git a/Dalamud.Boot/hooks.cpp b/Dalamud.Boot/hooks.cpp
index 1b1280cf0..295d427ae 100644
--- a/Dalamud.Boot/hooks.cpp
+++ b/Dalamud.Boot/hooks.cpp
@@ -5,6 +5,14 @@
#include "ntdll.h"
#include "logging.h"
+namespace {
+ int s_dllChanged = 0;
+}
+
+extern "C" __declspec(dllexport) int* GetDllChangedStorage() {
+ return &s_dllChanged;
+}
+
hooks::getprocaddress_singleton_import_hook::getprocaddress_singleton_import_hook()
: m_pfnGetProcAddress(GetProcAddress)
, m_thunk("kernel32!GetProcAddress(Singleton Import Hook)",
@@ -71,6 +79,7 @@ void hooks::getprocaddress_singleton_import_hook::initialize() {
m_getProcAddressHandler = set_handler(L"kernel32.dll", "GetProcAddress", m_thunk.get_thunk(), [this](void*) {});
LdrRegisterDllNotification(0, [](ULONG notiReason, const LDR_DLL_NOTIFICATION_DATA* pData, void* context) {
+ s_dllChanged = 1;
if (notiReason == LDR_DLL_NOTIFICATION_REASON_LOADED) {
const auto dllName = unicode::convert(pData->Loaded.FullDllName->Buffer);
diff --git a/Dalamud.Boot/themes.manifest b/Dalamud.Boot/themes.manifest
new file mode 100644
index 000000000..11c048abd
--- /dev/null
+++ b/Dalamud.Boot/themes.manifest
@@ -0,0 +1,9 @@
+
+
+ Windows Forms Common Control manifest
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs
index cce40da76..54025d5e2 100644
--- a/Dalamud.Injector/EntryPoint.cs
+++ b/Dalamud.Injector/EntryPoint.cs
@@ -199,9 +199,10 @@ namespace Dalamud.Injector
CullLogFile(logPath, 1 * 1024 * 1024);
+ const long maxLogSize = 100 * 1024 * 1024; // 100MB
Log.Logger = new LoggerConfiguration()
.WriteTo.Console(standardErrorFromLevel: LogEventLevel.Debug)
- .WriteTo.File(logPath, fileSizeLimitBytes: null)
+ .WriteTo.File(logPath, fileSizeLimitBytes: maxLogSize)
.MinimumLevel.ControlledBy(levelSwitch)
.CreateLogger();
diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
index 6bff5720f..5b49f5c72 100644
--- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs
+++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
@@ -8,6 +8,8 @@ using System.Runtime.InteropServices;
using Dalamud.Game.Text;
using Dalamud.Interface;
using Dalamud.Interface.FontIdentifier;
+using Dalamud.Interface.Internal;
+using Dalamud.Interface.Internal.ReShadeHandling;
using Dalamud.Interface.Style;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Internal.AutoUpdate;
@@ -441,6 +443,13 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
///
public bool WindowIsImmersive { get; set; } = false;
+ /// Gets or sets the mode specifying how to handle ReShade.
+ [JsonProperty("ReShadeHandlingModeV2")]
+ public ReShadeHandlingMode ReShadeHandlingMode { get; set; } = ReShadeHandlingMode.Default;
+
+ /// Gets or sets the swap chain hook mode.
+ public SwapChainHelper.HookMode SwapChainHookMode { get; set; } = SwapChainHelper.HookMode.ByteCode;
+
///
/// Gets or sets hitch threshold for game network up in milliseconds.
///
diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs
index 9ea96a45c..93de4c64d 100644
--- a/Dalamud/Dalamud.cs
+++ b/Dalamud/Dalamud.cs
@@ -65,7 +65,12 @@ internal sealed class Dalamud : IServiceType
true, new FileInfo(Path.Combine(cacheDir.FullName, $"{this.StartInfo.GameVersion}.json")));
}
- ServiceManager.InitializeProvidedServices(this, fs, configuration, scanner);
+ ServiceManager.InitializeProvidedServices(
+ this,
+ fs,
+ configuration,
+ scanner,
+ Localization.FromAssets(info.AssetDirectory!, configuration.LanguageOverride));
// Set up FFXIVClientStructs
this.SetupClientStructsResolver(cacheDir);
diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index 2eec5597f..d080a1622 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -9,7 +9,7 @@
- 10.0.0.4
+ 10.0.0.7XIV Launcher addon framework$(DalamudVersion)$(DalamudVersion)
@@ -136,57 +136,45 @@
$(OutputPath)TEMP_gitver.txt
+ $(OutputPath)commit_hash.txt$(OutputPath)revision.txt
-
+
-
-
-
- $([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitCommitCount), @"\t|\n|\r", ""))
-
-
-
-
-
-
-
-
-
+
+
-
-
+
+
-
-
+
- $([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitDescribeOutput), @"\t|\n|\r", ""))
- $([System.Text.RegularExpressions.Regex]::Replace($(ClientStructsGitDescribeOutput), @"\t|\n|\r", ""))
+ $([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitCommitCount), @"\t|\n|\r", ""))
+ $([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitCommitHash), @"\t|\n|\r", ""))
+ $([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitDescribeOutput), @"\t|\n|\r", ""))
+ $([System.Text.RegularExpressions.Regex]::Replace($(ClientStructsGitDescribeOutput), @"\t|\n|\r", ""))
-
-
-
-
-
-
-
- Local build at $([System.DateTime]::Now.ToString(yyyy-MM-dd HH:mm:ss))
- $(LocalBuildText)
- ???
-
-
-
-
+
+
+
-
+
+
+
+ Local build at $([System.DateTime]::Now.ToString(yyyy-MM-dd HH:mm:ss))
+ ???
+
+
+
+ $(IntermediateOutputPath)CustomAssemblyInfo.cs
@@ -197,21 +185,21 @@
-
- <_Parameter1>GitHash
- <_Parameter2>$(BuildHash)
+
+ <_Parameter1>SCMVersion
+ <_Parameter2>$(SCMVersion)
-
+
<_Parameter1>GitCommitCount
<_Parameter2>$(CommitCount)
-
+
<_Parameter1>GitHashClientStructs
- <_Parameter2>$(BuildHashClientStructs)
+ <_Parameter2>$(CommitHashClientStructs)
-
- <_Parameter1>FullGitHash
- <_Parameter2>$(DalamudFullGitCommitHash)
+
+ <_Parameter1>GitHash
+ <_Parameter2>$(CommitHash)
diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs
index 4b0adc9d8..512acd4cc 100644
--- a/Dalamud/EntryPoint.cs
+++ b/Dalamud/EntryPoint.cs
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Dalamud.Common;
using Dalamud.Configuration.Internal;
+using Dalamud.Interface.Internal.Windows;
using Dalamud.Logging.Internal;
using Dalamud.Logging.Retention;
using Dalamud.Plugin.Internal;
@@ -107,15 +108,16 @@ public sealed class EntryPoint
.WriteTo.Sink(SerilogEventSink.Instance)
.MinimumLevel.ControlledBy(LogLevelSwitch);
+ const long maxLogSize = 100 * 1024 * 1024; // 100MB
if (logSynchronously)
{
- config = config.WriteTo.File(logPath.FullName, fileSizeLimitBytes: null);
+ config = config.WriteTo.File(logPath.FullName, fileSizeLimitBytes: maxLogSize);
}
else
{
config = config.WriteTo.Async(a => a.File(
logPath.FullName,
- fileSizeLimitBytes: null,
+ fileSizeLimitBytes: maxLogSize,
buffered: false,
flushToDiskInterval: TimeSpan.FromSeconds(1)));
}
@@ -185,7 +187,7 @@ public sealed class EntryPoint
var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent);
Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]",
- Util.GetGitHash(),
+ Util.GetScmVersion(),
Util.GetGitHashClientStructs(),
FFXIVClientStructs.ThisAssembly.Git.Commits);
@@ -231,6 +233,10 @@ public sealed class EntryPoint
private static void SerilogOnLogLine(object? sender, (string Line, LogEvent LogEvent) ev)
{
+ if (!LoadingDialog.IsGloballyHidden)
+ LoadingDialog.NewLogEntries.Enqueue(ev);
+ ConsoleWindow.NewLogEntries.Enqueue(ev);
+
if (ev.LogEvent.Exception == null)
return;
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs
index 7cbc93eb2..91b9dd51f 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs
@@ -1,4 +1,8 @@
-namespace Dalamud.Game.Addon.Lifecycle;
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+namespace Dalamud.Game.Addon.Lifecycle;
///
/// Enumeration for available AddonLifecycle events.
@@ -6,67 +10,112 @@
public enum AddonEvent
{
///
- /// Event that is fired before an addon begins it's setup process.
+ /// An event that is fired prior to an addon being setup with its implementation of
+ /// . This event is useful for modifying the initial data contained within
+ /// prior to the addon being created.
///
+ ///
PreSetup,
-
+
///
- /// Event that is fired after an addon has completed it's setup process.
+ /// An event that is fired after an addon has finished its initial setup. This event is particularly useful for
+ /// developers seeking to add custom elements to now-initialized and populated node lists, as well as reading data
+ /// placed in the AtkValues by the game during the setup process.
+ /// See for more information.
///
PostSetup,
///
- /// Event that is fired before an addon begins update.
+ /// An event that is fired before an addon begins its update cycle via . This event
+ /// is fired every frame that an addon is loaded, regardless of visibility.
///
+ ///
PreUpdate,
///
- /// Event that is fired after an addon has completed update.
+ /// An event that is fired after an addon has finished its update.
+ /// See for more information.
///
PostUpdate,
///
- /// Event that is fired before an addon begins draw.
+ /// An event that is fired before an addon begins drawing to screen via . Unlike
+ /// , this event is only fired if an addon is visible or otherwise drawing to screen.
///
+ ///
PreDraw,
///
- /// Event that is fired after an addon has completed draw.
+ /// An event that is fired after an addon has finished its draw to screen.
+ /// See for more information.
///
PostDraw,
///
- /// Event that is fired before an addon is finalized.
+ /// An event that is fired immediately before an addon is finalized via and
+ /// destroyed. After this event, the addon will destruct its UI node data as well as free any allocated memory.
+ /// This event can be used for cleanup and tracking tasks.
///
+ ///
+ /// This event is NOT fired when the addon is being hidden, but tends to be fired when it's being properly
+ /// closed.
+ ///
+ /// As this is part of the destruction process for an addon, this event does not have an associated Post event.
+ ///
+ ///
PreFinalize,
///
- /// Event that is fired before an addon begins a requested update.
+ /// An event that is fired before a call to is made in response to a
+ /// change in the subscribed or
+ /// backing this addon. This generally occurs in response to
+ /// receiving data from the game server, but can happen in other cases as well. This event is useful for modifying
+ /// the data received before it's passed to the UI for display. Contrast to which tends to
+ /// be in response to client-driven interactions.
///
+ ///
+ ///
+ ///
+ /// A developer would use this event to intercept free company information after it's received from the server, but
+ /// before it's displayed to the user. This would allow the developer to add user-driven notes or other information
+ /// to the Free Company's overview.
+ ///
PreRequestedUpdate,
///
- /// Event that is fired after an addon finishes a requested update.
+ /// An event that is fired after an addon has finished processing an ArrayData update.
+ /// See for more information.
///
PostRequestedUpdate,
///
- /// Event that is fired before an addon begins a refresh.
- ///
+ /// An event that is fired before an addon calls its method. Refreshes are
+ /// generally triggered in response to certain user interactions such as changing tabs, and are primarily used to
+ /// update the AtkValues present in this addon. Contrast to which is called
+ /// in response to ArrayData updates.
+ ///
+ ///
PreRefresh,
///
- /// Event that is fired after an addon has finished a refresh.
+ /// An event that is fired after an addon has finished its refresh.
+ /// See for more information.
///
PostRefresh,
///
- /// Event that is fired before an addon begins processing an event.
+ /// An event that is fired before an addon begins processing a user-driven event via
+ /// , such as mousing over an element or clicking a button. This event
+ /// is only valid for addons that actually override the ReceiveEvent method of the underlying
+ /// AtkEventListener.
///
+ ///
+ ///
PreReceiveEvent,
///
- /// Event that is fired after an addon has processed an event.
+ /// An event that is fired after an addon finishes calling its method.
+ /// See for more information.
///
PostReceiveEvent,
}
diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs
index cd0cf9097..a41c6ff7a 100644
--- a/Dalamud/Game/ChatHandlers.cs
+++ b/Dalamud/Game/ChatHandlers.cs
@@ -179,7 +179,7 @@ internal class ChatHandlers : IServiceType
if (this.configuration.PrintDalamudWelcomeMsg)
{
- chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Util.GetGitHash())
+ chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Util.GetScmVersion())
+ string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), pluginManager.InstalledPlugins.Count(x => x.IsLoaded)));
}
diff --git a/Dalamud/Game/ClientState/JobGauge/Enums/SerpentCombo.cs b/Dalamud/Game/ClientState/JobGauge/Enums/SerpentCombo.cs
new file mode 100644
index 000000000..0fc50d87a
--- /dev/null
+++ b/Dalamud/Game/ClientState/JobGauge/Enums/SerpentCombo.cs
@@ -0,0 +1,42 @@
+namespace Dalamud.Game.ClientState.JobGauge.Enums;
+
+///
+/// Enum representing the SerpentCombo actions for the VPR job gauge.
+///
+public enum SerpentCombo : byte
+{
+ ///
+ /// No Serpent combo is active.
+ ///
+ NONE = 0,
+
+ ///
+ /// Death Rattle action.
+ ///
+ DEATHRATTLE = 1,
+
+ ///
+ /// Last Lash action.
+ ///
+ LASTLASH = 2,
+
+ ///
+ /// First Legacy action.
+ ///
+ FIRSTLEGACY = 3,
+
+ ///
+ /// Second Legacy action.
+ ///
+ SECONDLEGACY = 4,
+
+ ///
+ /// Third Legacy action.
+ ///
+ THIRDLEGACY = 5,
+
+ ///
+ /// Fourth Legacy action.
+ ///
+ FOURTHLEGACY = 6,
+}
diff --git a/Dalamud/Game/ClientState/JobGauge/Types/NINGauge.cs b/Dalamud/Game/ClientState/JobGauge/Types/NINGauge.cs
index 4d0b08556..aee8019e4 100644
--- a/Dalamud/Game/ClientState/JobGauge/Types/NINGauge.cs
+++ b/Dalamud/Game/ClientState/JobGauge/Types/NINGauge.cs
@@ -18,4 +18,9 @@ public unsafe class NINGauge : JobGaugeBase
public byte Ninki => this.Struct->Ninki;
+
+ ///
+ /// Gets the current charges for Kazematoi.
+ ///
+ public byte Kazematoi => this.Struct->Kazematoi;
}
diff --git a/Dalamud/Game/ClientState/JobGauge/Types/VPRGauge.cs b/Dalamud/Game/ClientState/JobGauge/Types/VPRGauge.cs
index a889c3482..3c822c7d7 100644
--- a/Dalamud/Game/ClientState/JobGauge/Types/VPRGauge.cs
+++ b/Dalamud/Game/ClientState/JobGauge/Types/VPRGauge.cs
@@ -1,8 +1,9 @@
-using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
+using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
using Reloaded.Memory;
using DreadCombo = Dalamud.Game.ClientState.JobGauge.Enums.DreadCombo;
+using SerpentCombo = Dalamud.Game.ClientState.JobGauge.Enums.SerpentCombo;
namespace Dalamud.Game.ClientState.JobGauge.Types;
@@ -39,4 +40,9 @@ public unsafe class VPRGauge : JobGaugeBase
/// Gets the last Weaponskill used in DreadWinder/Pit of Dread combo.
///
public DreadCombo DreadCombo => (DreadCombo)Struct->DreadCombo;
+
+ ///
+ /// Gets current ability for Serpent's Tail.
+ ///
+ public SerpentCombo SerpentCombo => (SerpentCombo)Struct->SerpentCombo;
}
diff --git a/Dalamud/Game/ClientState/Objects/Types/Character.cs b/Dalamud/Game/ClientState/Objects/Types/Character.cs
index 67f8c8b62..72f6a9950 100644
--- a/Dalamud/Game/ClientState/Objects/Types/Character.cs
+++ b/Dalamud/Game/ClientState/Objects/Types/Character.cs
@@ -93,6 +93,18 @@ public interface ICharacter : IGameObject
/// Gets the status flags.
///
public StatusFlags StatusFlags { get; }
+
+ ///
+ /// Gets the current mount for this character. Will be null if the character doesn't have a mount.
+ ///
+ public ExcelResolver? CurrentMount { get; }
+
+ ///
+ /// Gets the current minion summoned for this character. Will be null if the character doesn't have a minion.
+ /// This method *will* return information about a spawned (but invisible) minion, e.g. if the character is riding a
+ /// mount.
+ ///
+ public ExcelResolver? CurrentMinion { get; }
}
///
@@ -172,6 +184,32 @@ internal unsafe class Character : GameObject, ICharacter
(this.Struct->IsAllianceMember ? StatusFlags.AllianceMember : StatusFlags.None) |
(this.Struct->IsFriend ? StatusFlags.Friend : StatusFlags.None) |
(this.Struct->IsCasting ? StatusFlags.IsCasting : StatusFlags.None);
+
+ ///
+ public ExcelResolver? CurrentMount
+ {
+ get
+ {
+ if (this.Struct->IsNotMounted()) return null; // just for safety.
+
+ var mountId = this.Struct->Mount.MountId;
+ return mountId == 0 ? null : new ExcelResolver(mountId);
+ }
+ }
+
+ ///
+ public ExcelResolver? CurrentMinion
+ {
+ get
+ {
+ if (this.Struct->CompanionObject != null)
+ return new ExcelResolver(this.Struct->CompanionObject->BaseId);
+
+ // this is only present if a minion is summoned but hidden (e.g. the player's on a mount).
+ var hiddenCompanionId = this.Struct->CompanionData.CompanionId;
+ return hiddenCompanionId == 0 ? null : new ExcelResolver(hiddenCompanionId);
+ }
+ }
///
/// Gets the underlying structure.
diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs
index 1aeb42488..bfb58fd3c 100644
--- a/Dalamud/Game/Config/GameConfig.cs
+++ b/Dalamud/Game/Config/GameConfig.cs
@@ -16,10 +16,17 @@ namespace Dalamud.Game.Config;
[ServiceManager.EarlyLoadedService]
internal sealed class GameConfig : IInternalDisposableService, IGameConfig
{
- private readonly TaskCompletionSource tcsInitialization = new();
- private readonly TaskCompletionSource tcsSystem = new();
- private readonly TaskCompletionSource tcsUiConfig = new();
- private readonly TaskCompletionSource tcsUiControl = new();
+ private readonly TaskCompletionSource tcsInitialization =
+ new(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ private readonly TaskCompletionSource tcsSystem =
+ new(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ private readonly TaskCompletionSource tcsUiConfig =
+ new(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ private readonly TaskCompletionSource tcsUiControl =
+ new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly GameConfigAddressResolver address = new();
private Hook? configChangeHook;
diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs
index 4f9c8d6c6..07942f780 100644
--- a/Dalamud/Game/Framework.cs
+++ b/Dalamud/Game/Framework.cs
@@ -139,7 +139,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework
if (numTicks <= 0)
return Task.CompletedTask;
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
this.tickDelayedTaskCompletionSources[tcs] = (this.tickCounter + (ulong)numTicks, cancellationToken);
return tcs.Task;
}
diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs
index 2d8bb064b..55b2573f0 100644
--- a/Dalamud/Game/Gui/Dtr/DtrBar.cs
+++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs
@@ -76,6 +76,17 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
this.configuration.QueueSave();
}
+ ///
+ /// Event type fired each time a DtrEntry was removed.
+ ///
+ /// The title of the bar entry.
+ internal delegate void DtrEntryRemovedDelegate(string title);
+
+ ///
+ /// Event fired each time a DtrEntry was removed.
+ ///
+ internal event DtrEntryRemovedDelegate? DtrEntryRemoved;
+
///
public IReadOnlyList Entries => this.entries;
@@ -131,9 +142,13 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
///
internal void HandleRemovedNodes()
{
- foreach (var data in this.entries.Where(d => d.ShouldBeRemoved))
+ foreach (var data in this.entries)
{
- this.RemoveEntry(data);
+ if (data.ShouldBeRemoved)
+ {
+ this.RemoveEntry(data);
+ this.DtrEntryRemoved?.Invoke(data.Title);
+ }
}
this.entries.RemoveAll(d => d.ShouldBeRemoved);
@@ -210,7 +225,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
// If we have an unmodified DTR but still have entries, we need to
// work to reset our state.
- if (!this.CheckForDalamudNodes())
+ if (!this.CheckForDalamudNodes(dtr))
this.RecreateNodes();
var collisionNode = dtr->GetNodeById(17);
@@ -223,40 +238,36 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
foreach (var data in this.entries)
{
- var isHide = data.UserHidden || !data.Shown;
-
- if (data is { Dirty: true, Added: true, Text: not null, TextNode: not null })
+ if (!data.Added)
{
- var node = data.TextNode;
+ data.Added = this.AddNode(data.TextNode);
+ data.Dirty = true;
+ }
- if (data.Storage == null)
+ var isHide = !data.Shown || data.UserHidden;
+ var node = data.TextNode;
+ var nodeHidden = !node->AtkResNode.IsVisible();
+
+ if (!isHide)
+ {
+ if (nodeHidden)
+ node->AtkResNode.ToggleVisibility(true);
+
+ if (data is { Added: true, Text: not null, TextNode: not null } && (data.Dirty || nodeHidden))
{
- data.Storage = Utf8String.CreateEmpty();
- }
+ if (data.Storage == null)
+ {
+ data.Storage = Utf8String.CreateEmpty();
+ }
- data.Storage->SetString(data.Text.EncodeWithNullTerminator());
- node->SetText(data.Storage->StringPtr);
+ data.Storage->SetString(data.Text.EncodeWithNullTerminator());
+ node->SetText(data.Storage->StringPtr);
- ushort w = 0, h = 0;
-
- if (!isHide)
- {
+ ushort w = 0, h = 0;
node->GetTextDrawSize(&w, &h, node->NodeText.StringPtr);
node->AtkResNode.SetWidth(w);
}
- node->AtkResNode.ToggleVisibility(!isHide);
-
- data.Dirty = false;
- }
-
- if (!data.Added)
- {
- data.Added = this.AddNode(data.TextNode);
- }
-
- if (!isHide)
- {
var elementWidth = data.TextNode->AtkResNode.Width + this.configuration.DtrSpacing;
if (this.configuration.DtrSwapDirection)
@@ -270,17 +281,20 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2);
}
}
- else
+ else if (!nodeHidden)
{
// If we want the node hidden, shift it up, to prevent collision conflicts
- data.TextNode->AtkResNode.SetYFloat(-collisionNode->Height * dtr->RootNode->ScaleX);
+ node->AtkResNode.SetYFloat(-collisionNode->Height * dtr->RootNode->ScaleX);
+ node->AtkResNode.ToggleVisibility(false);
}
+
+ data.Dirty = false;
}
}
private void HandleAddedNodes()
{
- if (this.newEntries.Any())
+ if (!this.newEntries.IsEmpty)
{
foreach (var newEntry in this.newEntries)
{
@@ -354,11 +368,8 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
/// Checks if there are any Dalamud nodes in the DTR.
///
/// True if there are nodes with an ID > 1000.
- private bool CheckForDalamudNodes()
+ private bool CheckForDalamudNodes(AtkUnitBase* dtr)
{
- var dtr = this.GetDtr();
- if (dtr == null || dtr->RootNode == null) return false;
-
for (var i = 0; i < dtr->UldManager.NodeListCount; i++)
{
if (dtr->UldManager.NodeList[i]->NodeId > 1000)
@@ -526,7 +537,6 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
/// Plugin-scoped version of a AddonEventManager service.
///
[PluginInterface]
-
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia]
@@ -537,13 +547,23 @@ internal class DtrBarPluginScoped : IInternalDisposableService, IDtrBar
private readonly DtrBar dtrBarService = Service.Get();
private readonly Dictionary pluginEntries = new();
-
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal DtrBarPluginScoped()
+ {
+ this.dtrBarService.DtrEntryRemoved += this.OnDtrEntryRemoved;
+ }
+
///
public IReadOnlyList Entries => this.dtrBarService.Entries;
///
void IInternalDisposableService.DisposeService()
{
+ this.dtrBarService.DtrEntryRemoved -= this.OnDtrEntryRemoved;
+
foreach (var entry in this.pluginEntries)
{
entry.Value.Remove();
@@ -570,4 +590,9 @@ internal class DtrBarPluginScoped : IInternalDisposableService, IDtrBar
this.pluginEntries.Remove(title);
}
}
+
+ private void OnDtrEntryRemoved(string title)
+ {
+ this.pluginEntries.Remove(title);
+ }
}
diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs
index 33e9b26e3..fc5210fda 100644
--- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs
+++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs
@@ -137,14 +137,17 @@ public sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
get => this.shownBacking;
set
{
- this.shownBacking = value;
- this.Dirty = true;
+ if (value != this.shownBacking)
+ {
+ this.shownBacking = value;
+ this.Dirty = true;
+ }
}
}
///
[Api10ToDo("Maybe make this config scoped to internalname?")]
- public bool UserHidden => this.configuration.DtrIgnore?.Any(x => x == this.Title) ?? false;
+ public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false;
///
/// Gets or sets the internal text node of this entry.
diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs b/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs
new file mode 100644
index 000000000..28e2c36eb
--- /dev/null
+++ b/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs
@@ -0,0 +1,302 @@
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+using Dalamud.Game.Addon.Lifecycle;
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Game.ClientState.Objects;
+using Dalamud.IoC;
+using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Services;
+
+using FFXIVClientStructs.FFXIV.Client.UI;
+
+namespace Dalamud.Game.Gui.NamePlate;
+
+///
+/// Class used to modify the data used when rendering nameplates.
+///
+[ServiceManager.EarlyLoadedService]
+internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui
+{
+ ///
+ /// The index for the number array used by the NamePlate addon.
+ ///
+ public const int NumberArrayIndex = 5;
+
+ ///
+ /// The index for the string array used by the NamePlate addon.
+ ///
+ public const int StringArrayIndex = 4;
+
+ ///
+ /// The index for of the FullUpdate entry in the NamePlate number array.
+ ///
+ internal const int NumberArrayFullUpdateIndex = 4;
+
+ ///
+ /// An empty null-terminated string pointer allocated in unmanaged memory, used to tag removed fields.
+ ///
+ internal static readonly nint EmptyStringPointer = CreateEmptyStringPointer();
+
+ [ServiceManager.ServiceDependency]
+ private readonly AddonLifecycle addonLifecycle = Service.Get();
+
+ [ServiceManager.ServiceDependency]
+ private readonly GameGui gameGui = Service.Get();
+
+ [ServiceManager.ServiceDependency]
+ private readonly ObjectTable objectTable = Service.Get();
+
+ private readonly AddonLifecycleEventListener preRequestedUpdateListener;
+
+ private NamePlateUpdateContext? context;
+
+ private NamePlateUpdateHandler[] updateHandlers = [];
+
+ [ServiceManager.ServiceConstructor]
+ private NamePlateGui()
+ {
+ this.preRequestedUpdateListener = new AddonLifecycleEventListener(
+ AddonEvent.PreRequestedUpdate,
+ "NamePlate",
+ this.OnPreRequestedUpdate);
+
+ this.addonLifecycle.RegisterListener(this.preRequestedUpdateListener);
+ }
+
+ ///
+ public event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdate;
+
+ ///
+ public event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdate;
+
+ ///
+ public unsafe void RequestRedraw()
+ {
+ var addon = this.gameGui.GetAddonByName("NamePlate");
+ if (addon != 0)
+ {
+ var raptureAtkModule = RaptureAtkModule.Instance();
+ if (raptureAtkModule == null)
+ {
+ return;
+ }
+
+ ((AddonNamePlate*)addon)->DoFullUpdate = 1;
+ var namePlateNumberArrayData = raptureAtkModule->AtkArrayDataHolder.NumberArrays[NumberArrayIndex];
+ namePlateNumberArrayData->SetValue(NumberArrayFullUpdateIndex, 1);
+ }
+ }
+
+ ///
+ void IInternalDisposableService.DisposeService()
+ {
+ this.addonLifecycle.UnregisterListener(this.preRequestedUpdateListener);
+ }
+
+ ///
+ /// Strips the surrounding quotes from a free company tag. If the quotes are not present in the expected location,
+ /// no modifications will be made.
+ ///
+ /// A quoted free company tag.
+ /// A span containing the free company tag without its surrounding quote characters.
+ internal static ReadOnlySpan StripFreeCompanyTagQuotes(ReadOnlySpan text)
+ {
+ if (text.Length > 4 && text.StartsWith(" «"u8) && text.EndsWith("»"u8))
+ {
+ return text[3..^2];
+ }
+
+ return text;
+ }
+
+ ///
+ /// Strips the surrounding quotes from a title. If the quotes are not present in the expected location, no
+ /// modifications will be made.
+ ///
+ /// A quoted title.
+ /// A span containing the title without its surrounding quote characters.
+ internal static ReadOnlySpan StripTitleQuotes(ReadOnlySpan text)
+ {
+ if (text.Length > 5 && text.StartsWith("《"u8) && text.EndsWith("》"u8))
+ {
+ return text[3..^3];
+ }
+
+ return text;
+ }
+
+ private static nint CreateEmptyStringPointer()
+ {
+ var pointer = Marshal.AllocHGlobal(1);
+ Marshal.WriteByte(pointer, 0, 0);
+ return pointer;
+ }
+
+ private void CreateHandlers(NamePlateUpdateContext createdContext)
+ {
+ var handlers = new List();
+ for (var i = 0; i < AddonNamePlate.NumNamePlateObjects; i++)
+ {
+ handlers.Add(new NamePlateUpdateHandler(createdContext, i));
+ }
+
+ this.updateHandlers = handlers.ToArray();
+ }
+
+ private void OnPreRequestedUpdate(AddonEvent type, AddonArgs args)
+ {
+ if (this.OnDataUpdate == null && this.OnNamePlateUpdate == null)
+ {
+ return;
+ }
+
+ var reqArgs = (AddonRequestedUpdateArgs)args;
+ if (this.context == null)
+ {
+ this.context = new NamePlateUpdateContext(this.objectTable, reqArgs);
+ this.CreateHandlers(this.context);
+ }
+ else
+ {
+ this.context.ResetState(reqArgs);
+ }
+
+ var activeNamePlateCount = this.context.ActiveNamePlateCount;
+ if (activeNamePlateCount == 0)
+ return;
+
+ var activeHandlers = this.updateHandlers[..activeNamePlateCount];
+
+ if (this.context.IsFullUpdate)
+ {
+ foreach (var handler in activeHandlers)
+ {
+ handler.ResetState();
+ }
+
+ this.OnDataUpdate?.Invoke(this.context, activeHandlers);
+ this.OnNamePlateUpdate?.Invoke(this.context, activeHandlers);
+ if (this.context.HasParts)
+ this.ApplyBuilders(activeHandlers);
+ }
+ else
+ {
+ var udpatedHandlers = new List(activeNamePlateCount);
+ foreach (var handler in activeHandlers)
+ {
+ handler.ResetState();
+ if (handler.IsUpdating)
+ udpatedHandlers.Add(handler);
+ }
+
+ if (this.OnDataUpdate is not null)
+ {
+ this.OnDataUpdate?.Invoke(this.context, activeHandlers);
+ this.OnNamePlateUpdate?.Invoke(this.context, udpatedHandlers);
+ if (this.context.HasParts)
+ this.ApplyBuilders(activeHandlers);
+ }
+ else if (udpatedHandlers.Count != 0)
+ {
+ var changedHandlersSpan = udpatedHandlers.ToArray().AsSpan();
+ this.OnNamePlateUpdate?.Invoke(this.context, udpatedHandlers);
+ if (this.context.HasParts)
+ this.ApplyBuilders(changedHandlersSpan);
+ }
+ }
+ }
+
+ private void ApplyBuilders(Span handlers)
+ {
+ foreach (var handler in handlers)
+ {
+ if (handler.PartsContainer is { } container)
+ {
+ container.ApplyBuilders(handler);
+ }
+ }
+ }
+}
+
+///
+/// Plugin-scoped version of a AddonEventManager service.
+///
+[PluginInterface]
+[ServiceManager.ScopedService]
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlateGui
+{
+ [ServiceManager.ServiceDependency]
+ private readonly NamePlateGui parentService = Service.Get();
+
+ ///
+ public event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdate
+ {
+ add
+ {
+ if (this.OnNamePlateUpdateScoped == null)
+ this.parentService.OnNamePlateUpdate += this.OnNamePlateUpdateForward;
+ this.OnNamePlateUpdateScoped += value;
+ }
+
+ remove
+ {
+ this.OnNamePlateUpdateScoped -= value;
+ if (this.OnNamePlateUpdateScoped == null)
+ this.parentService.OnNamePlateUpdate -= this.OnNamePlateUpdateForward;
+ }
+ }
+
+ ///
+ public event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdate
+ {
+ add
+ {
+ if (this.OnDataUpdateScoped == null)
+ this.parentService.OnDataUpdate += this.OnDataUpdateForward;
+ this.OnDataUpdateScoped += value;
+ }
+
+ remove
+ {
+ this.OnDataUpdateScoped -= value;
+ if (this.OnDataUpdateScoped == null)
+ this.parentService.OnDataUpdate -= this.OnDataUpdateForward;
+ }
+ }
+
+ private event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdateScoped;
+
+ private event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdateScoped;
+
+ ///
+ public void RequestRedraw()
+ {
+ this.parentService.RequestRedraw();
+ }
+
+ ///
+ public void DisposeService()
+ {
+ this.parentService.OnNamePlateUpdate -= this.OnNamePlateUpdateForward;
+ this.OnNamePlateUpdateScoped = null;
+
+ this.parentService.OnDataUpdate -= this.OnDataUpdateForward;
+ this.OnDataUpdateScoped = null;
+ }
+
+ private void OnNamePlateUpdateForward(
+ INamePlateUpdateContext context, IReadOnlyList handlers)
+ {
+ this.OnNamePlateUpdateScoped?.Invoke(context, handlers);
+ }
+
+ private void OnDataUpdateForward(
+ INamePlateUpdateContext context, IReadOnlyList handlers)
+ {
+ this.OnDataUpdateScoped?.Invoke(context, handlers);
+ }
+}
diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateInfoView.cs b/Dalamud/Game/Gui/NamePlate/NamePlateInfoView.cs
new file mode 100644
index 000000000..020905422
--- /dev/null
+++ b/Dalamud/Game/Gui/NamePlate/NamePlateInfoView.cs
@@ -0,0 +1,105 @@
+using Dalamud.Game.Text.SeStringHandling;
+
+using FFXIVClientStructs.FFXIV.Client.UI;
+
+namespace Dalamud.Game.Gui.NamePlate;
+
+///
+/// Provides a read-only view of the nameplate info object data for a nameplate. Modifications to
+/// fields do not affect this data.
+///
+public interface INamePlateInfoView
+{
+ ///
+ /// Gets the displayed name for this nameplate according to the nameplate info object.
+ ///
+ SeString Name { get; }
+
+ ///
+ /// Gets the displayed free company tag for this nameplate according to the nameplate info object. For this field,
+ /// the quote characters which appear on either side of the title are NOT included.
+ ///
+ SeString FreeCompanyTag { get; }
+
+ ///
+ /// Gets the displayed free company tag for this nameplate according to the nameplate info object. For this field,
+ /// the quote characters which appear on either side of the title ARE included.
+ ///
+ SeString QuotedFreeCompanyTag { get; }
+
+ ///
+ /// Gets the displayed title for this nameplate according to the nameplate info object. For this field, the quote
+ /// characters which appear on either side of the title are NOT included.
+ ///
+ SeString Title { get; }
+
+ ///
+ /// Gets the displayed title for this nameplate according to the nameplate info object. For this field, the quote
+ /// characters which appear on either side of the title ARE included.
+ ///
+ SeString QuotedTitle { get; }
+
+ ///
+ /// Gets the displayed level text for this nameplate according to the nameplate info object.
+ ///
+ SeString LevelText { get; }
+
+ ///
+ /// Gets the flags for this nameplate according to the nameplate info object.
+ ///
+ int Flags { get; }
+
+ ///
+ /// Gets a value indicating whether this nameplate is considered 'dirty' or not according to the nameplate
+ /// info object.
+ ///
+ bool IsDirty { get; }
+
+ ///
+ /// Gets a value indicating whether the title for this nameplate is a prefix title or not according to the nameplate
+ /// info object. This value is derived from the field.
+ ///
+ bool IsPrefixTitle { get; }
+}
+
+///
+/// Provides a read-only view of the nameplate info object data for a nameplate. Modifications to
+/// fields do not affect this data.
+///
+internal unsafe class NamePlateInfoView(RaptureAtkModule.NamePlateInfo* info) : INamePlateInfoView
+{
+ private SeString? name;
+ private SeString? freeCompanyTag;
+ private SeString? quotedFreeCompanyTag;
+ private SeString? title;
+ private SeString? quotedTitle;
+ private SeString? levelText;
+
+ ///
+ public SeString Name => this.name ??= SeString.Parse(info->Name);
+
+ ///
+ public SeString FreeCompanyTag => this.freeCompanyTag ??=
+ SeString.Parse(NamePlateGui.StripFreeCompanyTagQuotes(info->FcName));
+
+ ///
+ public SeString QuotedFreeCompanyTag => this.quotedFreeCompanyTag ??= SeString.Parse(info->FcName);
+
+ ///
+ public SeString Title => this.title ??= SeString.Parse(info->Title);
+
+ ///
+ public SeString QuotedTitle => this.quotedTitle ??= SeString.Parse(info->DisplayTitle);
+
+ ///
+ public SeString LevelText => this.levelText ??= SeString.Parse(info->LevelText);
+
+ ///
+ public int Flags => info->Flags;
+
+ ///
+ public bool IsDirty => info->IsDirty;
+
+ ///
+ public bool IsPrefixTitle => ((info->Flags >> (8 * 3)) & 0xFF) == 1;
+}
diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateKind.cs b/Dalamud/Game/Gui/NamePlate/NamePlateKind.cs
new file mode 100644
index 000000000..af41ae199
--- /dev/null
+++ b/Dalamud/Game/Gui/NamePlate/NamePlateKind.cs
@@ -0,0 +1,57 @@
+namespace Dalamud.Game.Gui.NamePlate;
+
+///
+/// An enum describing what kind of game object this nameplate represents.
+///
+public enum NamePlateKind : byte
+{
+ ///
+ /// A player character.
+ ///
+ PlayerCharacter = 0,
+
+ ///
+ /// An event NPC or companion.
+ ///
+ EventNpcCompanion = 1,
+
+ ///
+ /// A retainer.
+ ///
+ Retainer = 2,
+
+ ///
+ /// An enemy battle NPC.
+ ///
+ BattleNpcEnemy = 3,
+
+ ///
+ /// A friendly battle NPC.
+ ///
+ BattleNpcFriendly = 4,
+
+ ///
+ /// An event object.
+ ///
+ EventObject = 5,
+
+ ///
+ /// Treasure.
+ ///
+ Treasure = 6,
+
+ ///
+ /// A gathering point.
+ ///
+ GatheringPoint = 7,
+
+ ///
+ /// A battle NPC with subkind 6.
+ ///
+ BattleNpcSubkind6 = 8,
+
+ ///
+ /// Something else.
+ ///
+ Other = 9,
+}
diff --git a/Dalamud/Game/Gui/NamePlate/NamePlatePartsContainer.cs b/Dalamud/Game/Gui/NamePlate/NamePlatePartsContainer.cs
new file mode 100644
index 000000000..c6f443c91
--- /dev/null
+++ b/Dalamud/Game/Gui/NamePlate/NamePlatePartsContainer.cs
@@ -0,0 +1,46 @@
+namespace Dalamud.Game.Gui.NamePlate;
+
+///
+/// A container for parts.
+///
+internal class NamePlatePartsContainer
+{
+ private NamePlateSimpleParts? nameParts;
+ private NamePlateQuotedParts? titleParts;
+ private NamePlateQuotedParts? freeCompanyTagParts;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The currently executing update context.
+ public NamePlatePartsContainer(NamePlateUpdateContext context)
+ {
+ context.HasParts = true;
+ }
+
+ ///
+ /// Gets a parts object for constructing a nameplate name.
+ ///
+ internal NamePlateSimpleParts Name => this.nameParts ??= new NamePlateSimpleParts(NamePlateStringField.Name);
+
+ ///
+ /// Gets a parts object for constructing a nameplate title.
+ ///
+ internal NamePlateQuotedParts Title => this.titleParts ??= new NamePlateQuotedParts(NamePlateStringField.Title, false);
+
+ ///
+ /// Gets a parts object for constructing a nameplate free company tag.
+ ///
+ internal NamePlateQuotedParts FreeCompanyTag => this.freeCompanyTagParts ??= new NamePlateQuotedParts(NamePlateStringField.FreeCompanyTag, true);
+
+ ///
+ /// Applies all container parts.
+ ///
+ /// The handler to apply the builders to.
+ internal void ApplyBuilders(NamePlateUpdateHandler handler)
+ {
+ this.nameParts?.Apply(handler);
+ this.freeCompanyTagParts?.Apply(handler);
+ this.titleParts?.Apply(handler);
+ }
+}
diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs b/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs
new file mode 100644
index 000000000..a398bdb82
--- /dev/null
+++ b/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs
@@ -0,0 +1,105 @@
+using Dalamud.Game.Text.SeStringHandling;
+
+namespace Dalamud.Game.Gui.NamePlate;
+
+///
+/// A part builder for constructing and setting quoted nameplate fields (i.e. free company tag and title).
+///
+/// The field type which should be set.
+/// Whether or not this is a Free Company part.
+///
+/// This class works as a lazy writer initialized with empty parts, where an empty part signifies no change should be
+/// performed. Only after all handler processing is complete does it write out any parts which were set to the
+/// associated field. Reading fields from this class is usually not what you want to do, as you'll only be reading the
+/// contents of parts which other plugins have written to. Prefer reading from the base handler's properties or using
+/// .
+///
+public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany)
+{
+ ///
+ /// Gets or sets the opening and closing SeStrings which will wrap the entire contents, which can be used to apply
+ /// colors or styling to the entire field.
+ ///
+ public (SeString, SeString)? OuterWrap { get; set; }
+
+ ///
+ /// Gets or sets the opening quote string which appears before the text and opening text-wrap.
+ ///
+ public SeString? LeftQuote { get; set; }
+
+ ///
+ /// Gets or sets the closing quote string which appears after the text and closing text-wrap.
+ ///
+ public SeString? RightQuote { get; set; }
+
+ ///
+ /// Gets or sets the opening and closing SeStrings which will wrap the text, which can be used to apply colors or
+ /// styling to the field's text.
+ ///
+ public (SeString, SeString)? TextWrap { get; set; }
+
+ ///
+ /// Gets or sets this field's text.
+ ///
+ public SeString? Text { get; set; }
+
+ ///
+ /// Applies the changes from this builder to the actual field.
+ ///
+ /// The handler to perform the changes on.
+ internal unsafe void Apply(NamePlateUpdateHandler handler)
+ {
+ if ((nint)handler.GetFieldAsPointer(field) == NamePlateGui.EmptyStringPointer)
+ return;
+
+ var sb = new SeStringBuilder();
+ if (this.OuterWrap is { Item1: var outerLeft })
+ {
+ sb.Append(outerLeft);
+ }
+
+ if (this.LeftQuote is not null)
+ {
+ sb.Append(this.LeftQuote);
+ }
+ else
+ {
+ sb.Append(isFreeCompany ? " «" : "《");
+ }
+
+ if (this.TextWrap is { Item1: var left, Item2: var right })
+ {
+ sb.Append(left);
+ sb.Append(this.Text ?? this.GetStrippedField(handler));
+ sb.Append(right);
+ }
+ else
+ {
+ sb.Append(this.Text ?? this.GetStrippedField(handler));
+ }
+
+ if (this.RightQuote is not null)
+ {
+ sb.Append(this.RightQuote);
+ }
+ else
+ {
+ sb.Append(isFreeCompany ? "»" : "》");
+ }
+
+ if (this.OuterWrap is { Item2: var outerRight })
+ {
+ sb.Append(outerRight);
+ }
+
+ handler.SetField(field, sb.Build());
+ }
+
+ private SeString GetStrippedField(NamePlateUpdateHandler handler)
+ {
+ return SeString.Parse(
+ isFreeCompany
+ ? NamePlateGui.StripFreeCompanyTagQuotes(handler.GetFieldAsSpan(field))
+ : NamePlateGui.StripTitleQuotes(handler.GetFieldAsSpan(field)));
+ }
+}
diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs b/Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs
new file mode 100644
index 000000000..2906005da
--- /dev/null
+++ b/Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs
@@ -0,0 +1,51 @@
+using Dalamud.Game.Text.SeStringHandling;
+
+namespace Dalamud.Game.Gui.NamePlate;
+
+///
+/// A part builder for constructing and setting a simple (unquoted) nameplate field.
+///
+/// The field type which should be set.
+///
+/// This class works as a lazy writer initialized with empty parts, where an empty part signifies no change should be
+/// performed. Only after all handler processing is complete does it write out any parts which were set to the
+/// associated field. Reading fields from this class is usually not what you want to do, as you'll only be reading the
+/// contents of parts which other plugins have written to. Prefer reading from the base handler's properties or using
+/// .
+///
+public class NamePlateSimpleParts(NamePlateStringField field)
+{
+ ///
+ /// Gets or sets the opening and closing SeStrings which will wrap the text, which can be used to apply colors or
+ /// styling to the field's text.
+ ///
+ public (SeString, SeString)? TextWrap { get; set; }
+
+ ///
+ /// Gets or sets this field's text.
+ ///
+ public SeString? Text { get; set; }
+
+ ///
+ /// Applies the changes from this builder to the actual field.
+ ///
+ /// The handler to perform the changes on.
+ internal unsafe void Apply(NamePlateUpdateHandler handler)
+ {
+ if ((nint)handler.GetFieldAsPointer(field) == NamePlateGui.EmptyStringPointer)
+ return;
+
+ if (this.TextWrap is { Item1: var left, Item2: var right })
+ {
+ var sb = new SeStringBuilder();
+ sb.Append(left);
+ sb.Append(this.Text ?? handler.GetFieldAsSeString(field));
+ sb.Append(right);
+ handler.SetField(field, sb.Build());
+ }
+ else if (this.Text is not null)
+ {
+ handler.SetField(field, this.Text);
+ }
+ }
+}
diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateStringField.cs b/Dalamud/Game/Gui/NamePlate/NamePlateStringField.cs
new file mode 100644
index 000000000..022935216
--- /dev/null
+++ b/Dalamud/Game/Gui/NamePlate/NamePlateStringField.cs
@@ -0,0 +1,38 @@
+namespace Dalamud.Game.Gui.NamePlate;
+
+///
+/// An enum describing the string fields available in nameplate data. The and various flags
+/// determine which fields will actually be rendered.
+///
+public enum NamePlateStringField
+{
+ ///
+ /// The object's name.
+ ///
+ Name = 0,
+
+ ///
+ /// The object's title.
+ ///
+ Title = 50,
+
+ ///
+ /// The object's free company tag.
+ ///
+ FreeCompanyTag = 100,
+
+ ///
+ /// The object's status prefix.
+ ///
+ StatusPrefix = 150,
+
+ ///
+ /// The object's target suffix.
+ ///
+ TargetSuffix = 200,
+
+ ///
+ /// The object's level prefix.
+ ///
+ LevelPrefix = 250,
+}
diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateUpdateContext.cs b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateContext.cs
new file mode 100644
index 000000000..b8a4a9bd8
--- /dev/null
+++ b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateContext.cs
@@ -0,0 +1,152 @@
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Game.ClientState.Objects;
+
+using FFXIVClientStructs.FFXIV.Client.UI;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+namespace Dalamud.Game.Gui.NamePlate;
+
+///
+/// Contains information related to the pending nameplate data update. This is only valid for a single frame and should
+/// not be kept across frames.
+///
+public interface INamePlateUpdateContext
+{
+ ///
+ /// Gets the number of active nameplates. The actual number visible may be lower than this in cases where some
+ /// nameplates are hidden by default (based on in-game "Display Name Settings" and so on).
+ ///
+ int ActiveNamePlateCount { get; }
+
+ ///
+ /// Gets a value indicating whether the game is currently performing a full update of all active nameplates.
+ ///
+ bool IsFullUpdate { get; }
+
+ ///
+ /// Gets the address of the NamePlate addon.
+ ///
+ nint AddonAddress { get; }
+
+ ///
+ /// Gets the address of the NamePlate addon's number array data container.
+ ///
+ nint NumberArrayDataAddress { get; }
+
+ ///
+ /// Gets the address of the NamePlate addon's string array data container.
+ ///
+ nint StringArrayDataAddress { get; }
+
+ ///
+ /// Gets the address of the first entry in the NamePlate addon's int array.
+ ///
+ nint NumberArrayDataEntryAddress { get; }
+}
+
+///
+/// Contains information related to the pending nameplate data update. This is only valid for a single frame and should
+/// not be kept across frames.
+///
+internal unsafe class NamePlateUpdateContext : INamePlateUpdateContext
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// An object table.
+ /// The addon lifecycle arguments for the update request.
+ internal NamePlateUpdateContext(ObjectTable objectTable, AddonRequestedUpdateArgs args)
+ {
+ this.ObjectTable = objectTable;
+ this.RaptureAtkModule = FFXIVClientStructs.FFXIV.Client.UI.RaptureAtkModule.Instance();
+ this.Ui3DModule = UIModule.Instance()->GetUI3DModule();
+ this.ResetState(args);
+ }
+
+ ///
+ /// Gets the number of active nameplates. The actual number visible may be lower than this in cases where some
+ /// nameplates are hidden by default (based on in-game "Display Name Settings" and so on).
+ ///
+ public int ActiveNamePlateCount { get; private set; }
+
+ ///
+ /// Gets a value indicating whether the game is currently performing a full update of all active nameplates.
+ ///
+ public bool IsFullUpdate { get; private set; }
+
+ ///
+ /// Gets the address of the NamePlate addon.
+ ///
+ public nint AddonAddress => (nint)this.Addon;
+
+ ///
+ /// Gets the address of the NamePlate addon's number array data container.
+ ///
+ public nint NumberArrayDataAddress => (nint)this.NumberData;
+
+ ///
+ /// Gets the address of the NamePlate addon's string array data container.
+ ///
+ public nint StringArrayDataAddress => (nint)this.StringData;
+
+ ///
+ /// Gets the address of the first entry in the NamePlate addon's int array.
+ ///
+ public nint NumberArrayDataEntryAddress => (nint)this.NumberStruct;
+
+ ///
+ /// Gets the RaptureAtkModule.
+ ///
+ internal RaptureAtkModule* RaptureAtkModule { get; }
+
+ ///
+ /// Gets the Ui3DModule.
+ ///
+ internal UI3DModule* Ui3DModule { get; }
+
+ ///
+ /// Gets the ObjectTable.
+ ///
+ internal ObjectTable ObjectTable { get; }
+
+ ///
+ /// Gets a pointer to the NamePlate addon.
+ ///
+ internal AddonNamePlate* Addon { get; private set; }
+
+ ///
+ /// Gets a pointer to the NamePlate addon's number array data container.
+ ///
+ internal NumberArrayData* NumberData { get; private set; }
+
+ ///
+ /// Gets a pointer to the NamePlate addon's string array data container.
+ ///
+ internal StringArrayData* StringData { get; private set; }
+
+ ///
+ /// Gets a pointer to the NamePlate addon's number array entries as a struct.
+ ///
+ internal AddonNamePlate.NamePlateIntArrayData* NumberStruct { get; private set; }
+
+ ///
+ /// Gets or sets a value indicating whether any handler in the current context has instantiated a part builder.
+ ///
+ internal bool HasParts { get; set; }
+
+ ///
+ /// Resets the state of the context based on the provided addon lifecycle arguments.
+ ///
+ /// The addon lifecycle arguments for the update request.
+ internal void ResetState(AddonRequestedUpdateArgs args)
+ {
+ this.Addon = (AddonNamePlate*)args.Addon;
+ this.NumberData = ((NumberArrayData**)args.NumberArrayData)![NamePlateGui.NumberArrayIndex];
+ this.NumberStruct = (AddonNamePlate.NamePlateIntArrayData*)this.NumberData->IntArray;
+ this.StringData = ((StringArrayData**)args.StringArrayData)![NamePlateGui.StringArrayIndex];
+ this.HasParts = false;
+
+ this.ActiveNamePlateCount = this.NumberStruct->ActiveNamePlateCount;
+ this.IsFullUpdate = this.Addon->DoFullUpdate != 0;
+ }
+}
diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs
new file mode 100644
index 000000000..99429d932
--- /dev/null
+++ b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs
@@ -0,0 +1,616 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+
+using Dalamud.Game.ClientState.Objects.SubKinds;
+using Dalamud.Game.ClientState.Objects.Types;
+using Dalamud.Game.Text.SeStringHandling;
+
+using FFXIVClientStructs.FFXIV.Client.UI;
+using FFXIVClientStructs.Interop;
+
+namespace Dalamud.Game.Gui.NamePlate;
+
+///
+/// A class representing a single nameplate. Provides mechanisms to look up the game object associated with the
+/// nameplate and allows for modification of various backing fields in number and string array data, which in turn
+/// affect aspects of the nameplate's appearance when drawn. Instances of this class are only valid for a single frame
+/// and should not be kept across frames.
+///
+public interface INamePlateUpdateHandler
+{
+ ///
+ /// Gets the GameObjectId of the game object associated with this nameplate.
+ ///
+ ulong GameObjectId { get; }
+
+ ///
+ /// Gets the associated with this nameplate, if possible. Performs an object table scan
+ /// and caches the result if successful.
+ ///
+ IGameObject? GameObject { get; }
+
+ ///
+ /// Gets a read-only view of the nameplate info object data for a nameplate. Modifications to
+ /// fields do not affect fields in the returned view.
+ ///
+ INamePlateInfoView InfoView { get; }
+
+ ///
+ /// Gets the index for this nameplate data in the backing number and string array data. This is not the same as the
+ /// rendered or object index, which can be retrieved from .
+ ///
+ int ArrayIndex { get; }
+
+ ///
+ /// Gets the associated with this nameplate, if possible. Returns null if the nameplate
+ /// has an associated , but that object cannot be assigned to .
+ ///
+ IBattleChara? BattleChara { get; }
+
+ ///
+ /// Gets the associated with this nameplate, if possible. Returns null if the
+ /// nameplate has an associated , but that object cannot be assigned to
+ /// .
+ ///
+ IPlayerCharacter? PlayerCharacter { get; }
+
+ ///
+ /// Gets the address of the nameplate info struct.
+ ///
+ nint NamePlateInfoAddress { get; }
+
+ ///
+ /// Gets the address of the first entry associated with this nameplate in the NamePlate addon's int array.
+ ///
+ nint NamePlateObjectAddress { get; }
+
+ ///
+ /// Gets a value indicating what kind of nameplate this is, based on the kind of object it is associated with.
+ ///
+ NamePlateKind NamePlateKind { get; }
+
+ ///
+ /// Gets the update flags for this nameplate.
+ ///
+ int UpdateFlags { get; }
+
+ ///
+ /// Gets or sets the overall text color for this nameplate. If this value is changed, the appropriate update flag
+ /// will be set so that the game will reflect this change immediately.
+ ///
+ uint TextColor { get; set; }
+
+ ///
+ /// Gets or sets the overall text edge color for this nameplate. If this value is changed, the appropriate update
+ /// flag will be set so that the game will reflect this change immediately.
+ ///
+ uint EdgeColor { get; set; }
+
+ ///
+ /// Gets or sets the icon ID for the nameplate's marker icon, which is the large icon used to indicate quest
+ /// availability and so on. This value is read from and reset by the game every frame, not just when a nameplate
+ /// changes. Setting this to 0 disables the icon.
+ ///
+ int MarkerIconId { get; set; }
+
+ ///
+ /// Gets or sets the icon ID for the nameplate's name icon, which is the small icon shown to the left of the name.
+ /// Setting this to -1 disables the icon.
+ ///
+ int NameIconId { get; set; }
+
+ ///
+ /// Gets the nameplate index, which is the index used for rendering and looking up entries in the object array. For
+ /// number and string array data, is used.
+ ///
+ int NamePlateIndex { get; }
+
+ ///
+ /// Gets the draw flags for this nameplate.
+ ///
+ int DrawFlags { get; }
+
+ ///
+ /// Gets or sets the visibility flags for this nameplate.
+ ///
+ int VisibilityFlags { get; set; }
+
+ ///
+ /// Gets a value indicating whether this nameplate is undergoing a major update or not. This is usually true when a
+ /// nameplate has just appeared or something meaningful about the entity has changed (e.g. its job or status). This
+ /// flag is reset by the game during the update process (during requested update and before draw).
+ ///
+ bool IsUpdating { get; }
+
+ ///
+ /// Gets or sets a value indicating whether the title (when visible) will be displayed above the object's name (a
+ /// prefix title) instead of below the object's name (a suffix title).
+ ///
+ bool IsPrefixTitle { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the title should be displayed at all.
+ ///
+ bool DisplayTitle { get; set; }
+
+ ///
+ /// Gets or sets the name for this nameplate.
+ ///
+ SeString Name { get; set; }
+
+ ///
+ /// Gets a builder which can be used to help cooperatively build a new name for this nameplate even when other
+ /// plugins modifying the name are present. Specifically, this builder allows setting text and text-wrapping
+ /// payloads (e.g. for setting text color) separately.
+ ///
+ NamePlateSimpleParts NameParts { get; }
+
+ ///
+ /// Gets or sets the title for this nameplate.
+ ///
+ SeString Title { get; set; }
+
+ ///
+ /// Gets a builder which can be used to help cooperatively build a new title for this nameplate even when other
+ /// plugins modifying the title are present. Specifically, this builder allows setting text, text-wrapping
+ /// payloads (e.g. for setting text color), and opening and closing quote sequences separately.
+ ///
+ NamePlateQuotedParts TitleParts { get; }
+
+ ///
+ /// Gets or sets the free company tag for this nameplate.
+ ///
+ SeString FreeCompanyTag { get; set; }
+
+ ///
+ /// Gets a builder which can be used to help cooperatively build a new FC tag for this nameplate even when other
+ /// plugins modifying the FC tag are present. Specifically, this builder allows setting text, text-wrapping
+ /// payloads (e.g. for setting text color), and opening and closing quote sequences separately.
+ ///
+ NamePlateQuotedParts FreeCompanyTagParts { get; }
+
+ ///
+ /// Gets or sets the status prefix for this nameplate. This prefix is used by the game to add BitmapFontIcon-based
+ /// online status icons to player nameplates.
+ ///
+ SeString StatusPrefix { get; set; }
+
+ ///
+ /// Gets or sets the target suffix for this nameplate. This suffix is used by the game to add the squared-letter
+ /// target tags to the end of combat target nameplates.
+ ///
+ SeString TargetSuffix { get; set; }
+
+ ///
+ /// Gets or sets the level prefix for this nameplate. This "Lv60" style prefix is added to enemy and friendly battle
+ /// NPC nameplates to indicate the NPC level.
+ ///
+ SeString LevelPrefix { get; set; }
+
+ ///
+ /// Removes the contents of the name field for this nameplate. This differs from simply setting the field
+ /// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
+ /// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
+ ///
+ void RemoveName();
+
+ ///
+ /// Removes the contents of the title field for this nameplate. This differs from simply setting the field
+ /// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
+ /// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
+ ///
+ void RemoveTitle();
+
+ ///
+ /// Removes the contents of the FC tag field for this nameplate. This differs from simply setting the field
+ /// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
+ /// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
+ ///
+ void RemoveFreeCompanyTag();
+
+ ///
+ /// Removes the contents of the status prefix field for this nameplate. This differs from simply setting the field
+ /// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
+ /// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
+ ///
+ void RemoveStatusPrefix();
+
+ ///
+ /// Removes the contents of the target suffix field for this nameplate. This differs from simply setting the field
+ /// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
+ /// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
+ ///
+ void RemoveTargetSuffix();
+
+ ///
+ /// Removes the contents of the level prefix field for this nameplate. This differs from simply setting the field
+ /// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
+ /// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
+ ///
+ void RemoveLevelPrefix();
+
+ ///
+ /// Gets a pointer to the string array value in the provided field.
+ ///
+ /// The field to read from.
+ /// A pointer to a sequence of non-null bytes.
+ unsafe byte* GetFieldAsPointer(NamePlateStringField field);
+
+ ///
+ /// Gets a byte span containing the string array value in the provided field.
+ ///
+ /// The field to read from.
+ /// A ReadOnlySpan containing a sequence of non-null bytes.
+ ReadOnlySpan GetFieldAsSpan(NamePlateStringField field);
+
+ ///
+ /// Gets a UTF8 string copy of the string array value in the provided field.
+ ///
+ /// The field to read from.
+ /// A copy of the string array value as a string.
+ string GetFieldAsString(NamePlateStringField field);
+
+ ///
+ /// Gets a parsed SeString copy of the string array value in the provided field.
+ ///
+ /// The field to read from.
+ /// A copy of the string array value as a parsed SeString.
+ SeString GetFieldAsSeString(NamePlateStringField field);
+
+ ///
+ /// Sets the string array value for the provided field.
+ ///
+ /// The field to write to.
+ /// The string to write.
+ void SetField(NamePlateStringField field, string value);
+
+ ///
+ /// Sets the string array value for the provided field.
+ ///
+ /// The field to write to.
+ /// The SeString to write.
+ void SetField(NamePlateStringField field, SeString value);
+
+ ///
+ /// Sets the string array value for the provided field. The provided byte sequence must be null-terminated.
+ ///
+ /// The field to write to.
+ /// The ReadOnlySpan of bytes to write.
+ void SetField(NamePlateStringField field, ReadOnlySpan value);
+
+ ///
+ /// Sets the string array value for the provided field. The provided byte sequence must be null-terminated.
+ ///
+ /// The field to write to.
+ /// The pointer to a null-terminated sequence of bytes to write.
+ unsafe void SetField(NamePlateStringField field, byte* value);
+
+ ///
+ /// Sets the string array value for the provided field to a fixed pointer to an empty string in unmanaged memory.
+ /// Other methods may notice this fixed pointer and refuse to overwrite it, preserving the emptiness of the field.
+ ///
+ /// The field to write to.
+ void RemoveField(NamePlateStringField field);
+}
+
+///
+/// A class representing a single nameplate. Provides mechanisms to look up the game object associated with the
+/// nameplate and allows for modification of various backing fields in number and string array data, which in turn
+/// affect aspects of the nameplate's appearance when drawn. Instances of this class are only valid for a single frame
+/// and should not be kept across frames.
+///
+internal unsafe class NamePlateUpdateHandler : INamePlateUpdateHandler
+{
+ private readonly NamePlateUpdateContext context;
+
+ private ulong? gameObjectId;
+ private IGameObject? gameObject;
+ private NamePlateInfoView? infoView;
+ private NamePlatePartsContainer? partsContainer;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The current update context.
+ /// The index for this nameplate data in the backing number and string array data. This is
+ /// not the same as the rendered index, which can be retrieved from .
+ internal NamePlateUpdateHandler(NamePlateUpdateContext context, int arrayIndex)
+ {
+ this.context = context;
+ this.ArrayIndex = arrayIndex;
+ }
+
+ ///
+ public int ArrayIndex { get; }
+
+ ///
+ public ulong GameObjectId => this.gameObjectId ??= this.NamePlateInfo->ObjectId;
+
+ ///
+ public IGameObject? GameObject => this.gameObject ??= this.context.ObjectTable.CreateObjectReference(
+ (nint)this.context.Ui3DModule->NamePlateObjectInfoPointers[
+ this.ArrayIndex].Value->GameObject);
+
+ ///
+ public IBattleChara? BattleChara => this.GameObject as IBattleChara;
+
+ ///
+ public IPlayerCharacter? PlayerCharacter => this.GameObject as IPlayerCharacter;
+
+ ///
+ public INamePlateInfoView InfoView => this.infoView ??= new NamePlateInfoView(this.NamePlateInfo);
+
+ ///
+ public nint NamePlateInfoAddress => (nint)this.NamePlateInfo;
+
+ ///
+ public nint NamePlateObjectAddress => (nint)this.NamePlateObject;
+
+ ///
+ public NamePlateKind NamePlateKind => (NamePlateKind)this.ObjectData->NamePlateKind;
+
+ ///
+ public int UpdateFlags
+ {
+ get => this.ObjectData->UpdateFlags;
+ private set => this.ObjectData->UpdateFlags = value;
+ }
+
+ ///
+ public uint TextColor
+ {
+ get => this.ObjectData->NameTextColor;
+ set
+ {
+ if (value != this.TextColor) this.UpdateFlags |= 2;
+ this.ObjectData->NameTextColor = value;
+ }
+ }
+
+ ///
+ public uint EdgeColor
+ {
+ get => this.ObjectData->NameEdgeColor;
+ set
+ {
+ if (value != this.EdgeColor) this.UpdateFlags |= 2;
+ this.ObjectData->NameEdgeColor = value;
+ }
+ }
+
+ ///
+ public int MarkerIconId
+ {
+ get => this.ObjectData->MarkerIconId;
+ set => this.ObjectData->MarkerIconId = value;
+ }
+
+ ///
+ public int NameIconId
+ {
+ get => this.ObjectData->NameIconId;
+ set => this.ObjectData->NameIconId = value;
+ }
+
+ ///
+ public int NamePlateIndex => this.ObjectData->NamePlateObjectIndex;
+
+ ///
+ public int DrawFlags
+ {
+ get => this.ObjectData->DrawFlags;
+ private set => this.ObjectData->DrawFlags = value;
+ }
+
+ ///
+ public int VisibilityFlags
+ {
+ get => ObjectData->VisibilityFlags;
+ set => ObjectData->VisibilityFlags = value;
+ }
+
+ ///
+ public bool IsUpdating => (this.UpdateFlags & 1) != 0;
+
+ ///
+ public bool IsPrefixTitle
+ {
+ get => (this.DrawFlags & 1) != 0;
+ set => this.DrawFlags = value ? this.DrawFlags | 1 : this.DrawFlags & ~1;
+ }
+
+ ///
+ public bool DisplayTitle
+ {
+ get => (this.DrawFlags & 0x80) == 0;
+ set => this.DrawFlags = value ? this.DrawFlags & ~0x80 : this.DrawFlags | 0x80;
+ }
+
+ ///
+ public SeString Name
+ {
+ get => this.GetFieldAsSeString(NamePlateStringField.Name);
+ set => this.WeakSetField(NamePlateStringField.Name, value);
+ }
+
+ ///
+ public NamePlateSimpleParts NameParts => this.PartsContainer.Name;
+
+ ///
+ public SeString Title
+ {
+ get => this.GetFieldAsSeString(NamePlateStringField.Title);
+ set => this.WeakSetField(NamePlateStringField.Title, value);
+ }
+
+ ///
+ public NamePlateQuotedParts TitleParts => this.PartsContainer.Title;
+
+ ///
+ public SeString FreeCompanyTag
+ {
+ get => this.GetFieldAsSeString(NamePlateStringField.FreeCompanyTag);
+ set => this.WeakSetField(NamePlateStringField.FreeCompanyTag, value);
+ }
+
+ ///
+ public NamePlateQuotedParts FreeCompanyTagParts => this.PartsContainer.FreeCompanyTag;
+
+ ///
+ public SeString StatusPrefix
+ {
+ get => this.GetFieldAsSeString(NamePlateStringField.StatusPrefix);
+ set => this.WeakSetField(NamePlateStringField.StatusPrefix, value);
+ }
+
+ ///
+ public SeString TargetSuffix
+ {
+ get => this.GetFieldAsSeString(NamePlateStringField.TargetSuffix);
+ set => this.WeakSetField(NamePlateStringField.TargetSuffix, value);
+ }
+
+ ///
+ public SeString LevelPrefix
+ {
+ get => this.GetFieldAsSeString(NamePlateStringField.LevelPrefix);
+ set => this.WeakSetField(NamePlateStringField.LevelPrefix, value);
+ }
+
+ ///
+ /// Gets or (lazily) creates a part builder container for this nameplate.
+ ///
+ internal NamePlatePartsContainer PartsContainer =>
+ this.partsContainer ??= new NamePlatePartsContainer(this.context);
+
+ private RaptureAtkModule.NamePlateInfo* NamePlateInfo =>
+ this.context.RaptureAtkModule->NamePlateInfoEntries.GetPointer(this.NamePlateIndex);
+
+ private AddonNamePlate.NamePlateObject* NamePlateObject =>
+ &this.context.Addon->NamePlateObjectArray[this.NamePlateIndex];
+
+ private AddonNamePlate.NamePlateIntArrayData.NamePlateObjectIntArrayData* ObjectData =>
+ this.context.NumberStruct->ObjectData.GetPointer(this.ArrayIndex);
+
+ ///
+ public void RemoveName() => this.RemoveField(NamePlateStringField.Name);
+
+ ///
+ public void RemoveTitle() => this.RemoveField(NamePlateStringField.Title);
+
+ ///
+ public void RemoveFreeCompanyTag() => this.RemoveField(NamePlateStringField.FreeCompanyTag);
+
+ ///
+ public void RemoveStatusPrefix() => this.RemoveField(NamePlateStringField.StatusPrefix);
+
+ ///
+ public void RemoveTargetSuffix() => this.RemoveField(NamePlateStringField.TargetSuffix);
+
+ ///
+ public void RemoveLevelPrefix() => this.RemoveField(NamePlateStringField.LevelPrefix);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public byte* GetFieldAsPointer(NamePlateStringField field)
+ {
+ return this.context.StringData->StringArray[this.ArrayIndex + (int)field];
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ReadOnlySpan GetFieldAsSpan(NamePlateStringField field)
+ {
+ return MemoryMarshal.CreateReadOnlySpanFromNullTerminated(this.GetFieldAsPointer(field));
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public string GetFieldAsString(NamePlateStringField field)
+ {
+ return Encoding.UTF8.GetString(this.GetFieldAsSpan(field));
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public SeString GetFieldAsSeString(NamePlateStringField field)
+ {
+ return SeString.Parse(this.GetFieldAsSpan(field));
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetField(NamePlateStringField field, string value)
+ {
+ this.context.StringData->SetValue(this.ArrayIndex + (int)field, value, true, true, true);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetField(NamePlateStringField field, SeString value)
+ {
+ this.context.StringData->SetValue(
+ this.ArrayIndex + (int)field,
+ value.EncodeWithNullTerminator(),
+ true,
+ true,
+ true);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetField(NamePlateStringField field, ReadOnlySpan value)
+ {
+ this.context.StringData->SetValue(this.ArrayIndex + (int)field, value, true, true, true);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetField(NamePlateStringField field, byte* value)
+ {
+ this.context.StringData->SetValue(this.ArrayIndex + (int)field, value, true, true, true);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void RemoveField(NamePlateStringField field)
+ {
+ this.context.StringData->SetValue(
+ this.ArrayIndex + (int)field,
+ (byte*)NamePlateGui.EmptyStringPointer,
+ true,
+ false,
+ true);
+ }
+
+ ///
+ /// Resets the state of this handler for re-use in a new update.
+ ///
+ internal void ResetState()
+ {
+ this.gameObjectId = null;
+ this.gameObject = null;
+ this.infoView = null;
+ this.partsContainer = null;
+ }
+
+ ///
+ /// Sets the string array value for the provided field, unless it was already set to the special empty string
+ /// pointer used by the Remove methods.
+ ///
+ /// The field to write to.
+ /// The SeString to write.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void WeakSetField(NamePlateStringField field, SeString value)
+ {
+ if ((nint)this.GetFieldAsPointer(field) == NamePlateGui.EmptyStringPointer)
+ return;
+ this.context.StringData->SetValue(
+ this.ArrayIndex + (int)field,
+ value.EncodeWithNullTerminator(),
+ true,
+ true,
+ true);
+ }
+}
diff --git a/Dalamud/Hooking/Internal/ObjectVTableHook.cs b/Dalamud/Hooking/Internal/ObjectVTableHook.cs
new file mode 100644
index 000000000..8b2f24de2
--- /dev/null
+++ b/Dalamud/Hooking/Internal/ObjectVTableHook.cs
@@ -0,0 +1,295 @@
+using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+using Dalamud.Utility;
+
+using Serilog;
+
+namespace Dalamud.Hooking.Internal;
+
+/// Manages a hook that works by replacing the vtable of target object.
+internal unsafe class ObjectVTableHook : IDisposable
+{
+ private readonly nint** ppVtbl;
+ private readonly int numMethods;
+
+ private readonly nint* pVtblOriginal;
+ private readonly nint[] vtblOverriden;
+
+ /// Extra data for overriden vtable entries, primarily for keeping references to delegates that are used
+ /// with .
+ private readonly object?[] vtblOverridenTag;
+
+ private bool released;
+
+ /// Initializes a new instance of the class.
+ /// Address to vtable. Usually the address of the object itself.
+ /// Number of methods in this vtable.
+ public ObjectVTableHook(nint ppVtbl, int numMethods)
+ {
+ this.ppVtbl = (nint**)ppVtbl;
+ this.numMethods = numMethods;
+ this.vtblOverridenTag = new object?[numMethods];
+ this.pVtblOriginal = *this.ppVtbl;
+ this.vtblOverriden = GC.AllocateArray(numMethods, true);
+ this.OriginalVTableSpan.CopyTo(this.vtblOverriden);
+ }
+
+ /// Initializes a new instance of the class.
+ /// Address to vtable. Usually the address of the object itself.
+ /// Number of methods in this vtable.
+ public ObjectVTableHook(void* ppVtbl, int numMethods)
+ : this((nint)ppVtbl, numMethods)
+ {
+ }
+
+ /// Finalizes an instance of the class.
+ ~ObjectVTableHook() => this.ReleaseUnmanagedResources();
+
+ /// Gets the span view of original vtable.
+ public ReadOnlySpan OriginalVTableSpan => new(this.pVtblOriginal, this.numMethods);
+
+ /// Gets the span view of overriden vtable.
+ public ReadOnlySpan OverridenVTableSpan => this.vtblOverriden.AsSpan();
+
+ /// Gets the address of the pointer to the vtable.
+ public nint Address => (nint)this.ppVtbl;
+
+ /// Gets the address of the original vtable.
+ public nint OriginalVTableAddress => (nint)this.pVtblOriginal;
+
+ /// Gets the address of the overriden vtable.
+ public nint OverridenVTableAddress => (nint)Unsafe.AsPointer(ref this.vtblOverriden[0]);
+
+ /// Disables the hook.
+ public void Disable()
+ {
+ // already disabled
+ if (*this.ppVtbl == this.pVtblOriginal)
+ return;
+
+ if (*this.ppVtbl != Unsafe.AsPointer(ref this.vtblOverriden[0]))
+ {
+ Log.Warning(
+ "[{who}]: the object was hooked by something else; disabling may result in a crash.",
+ this.GetType().Name);
+ }
+
+ *this.ppVtbl = this.pVtblOriginal;
+ }
+
+ ///
+ public void Dispose()
+ {
+ this.ReleaseUnmanagedResources();
+ GC.SuppressFinalize(this);
+ }
+
+ /// Enables the hook.
+ public void Enable()
+ {
+ // already enabled
+ if (*this.ppVtbl == Unsafe.AsPointer(ref this.vtblOverriden[0]))
+ return;
+
+ if (*this.ppVtbl != this.pVtblOriginal)
+ {
+ Log.Warning(
+ "[{who}]: the object was hooked by something else; enabling may result in a crash.",
+ this.GetType().Name);
+ }
+
+ *this.ppVtbl = (nint*)Unsafe.AsPointer(ref this.vtblOverriden[0]);
+ }
+
+ /// Gets the original method address of the given method index.
+ /// Index of the method.
+ /// Address of the original method.
+ public nint GetOriginalMethodAddress(int methodIndex)
+ {
+ this.EnsureMethodIndex(methodIndex);
+ return this.pVtblOriginal[methodIndex];
+ }
+
+ /// Gets the original method of the given method index, as a delegate of given type.
+ /// Index of the method.
+ /// Type of delegate.
+ /// Delegate to the original method.
+ public T GetOriginalMethodDelegate(int methodIndex)
+ where T : Delegate
+ {
+ this.EnsureMethodIndex(methodIndex);
+ return Marshal.GetDelegateForFunctionPointer(this.pVtblOriginal[methodIndex]);
+ }
+
+ /// Resets a method to the original function.
+ /// Index of the method.
+ public void ResetVtableEntry(int methodIndex)
+ {
+ this.EnsureMethodIndex(methodIndex);
+ this.vtblOverriden[methodIndex] = this.pVtblOriginal[methodIndex];
+ this.vtblOverridenTag[methodIndex] = null;
+ }
+
+ /// Sets a method in vtable to the given address of function.
+ /// Index of the method.
+ /// Address of the detour function.
+ /// Additional reference to keep in memory.
+ public void SetVtableEntry(int methodIndex, nint pfn, object? refkeep)
+ {
+ this.EnsureMethodIndex(methodIndex);
+ this.vtblOverriden[methodIndex] = pfn;
+ this.vtblOverridenTag[methodIndex] = refkeep;
+ }
+
+ /// Sets a method in vtable to the given delegate.
+ /// Index of the method.
+ /// Detour delegate.
+ /// Type of delegate.
+ public void SetVtableEntry(int methodIndex, T detourDelegate)
+ where T : Delegate =>
+ this.SetVtableEntry(methodIndex, Marshal.GetFunctionPointerForDelegate(detourDelegate), detourDelegate);
+
+ /// Sets a method in vtable to the given delegate.
+ /// Index of the method.
+ /// Detour delegate.
+ /// Original method delegate.
+ /// Type of delegate.
+ public void SetVtableEntry(int methodIndex, T detourDelegate, out T originalMethodDelegate)
+ where T : Delegate
+ {
+ originalMethodDelegate = this.GetOriginalMethodDelegate(methodIndex);
+ this.SetVtableEntry(methodIndex, Marshal.GetFunctionPointerForDelegate(detourDelegate), detourDelegate);
+ }
+
+ /// Creates a new instance of that manages one entry in the vtable hook.
+ /// Index of the method.
+ /// Detour delegate.
+ /// Type of delegate.
+ /// A new instance of .
+ /// Even if a single hook is enabled, without , the hook will remain disabled.
+ ///
+ public Hook CreateHook(int methodIndex, T detourDelegate) where T : Delegate =>
+ new SingleHook(this, methodIndex, detourDelegate);
+
+ private void EnsureMethodIndex(int methodIndex)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(methodIndex);
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(methodIndex, this.numMethods);
+ }
+
+ private void ReleaseUnmanagedResources()
+ {
+ if (!this.released)
+ {
+ this.Disable();
+ this.released = true;
+ }
+ }
+
+ private sealed class SingleHook(ObjectVTableHook hook, int methodIndex, T detourDelegate)
+ : Hook((nint)hook.ppVtbl)
+ where T : Delegate
+ {
+ ///
+ public override T Original { get; } = hook.GetOriginalMethodDelegate(methodIndex);
+
+ ///
+ public override bool IsEnabled =>
+ hook.OriginalVTableSpan[methodIndex] != hook.OverridenVTableSpan[methodIndex];
+
+ ///
+ public override string BackendName => nameof(ObjectVTableHook);
+
+ ///
+ public override void Enable() => hook.SetVtableEntry(methodIndex, detourDelegate);
+
+ ///
+ public override void Disable() => hook.ResetVtableEntry(methodIndex);
+ }
+}
+
+/// Typed version of .
+/// VTable struct.
+internal unsafe class ObjectVTableHook : ObjectVTableHook
+ where TVTable : unmanaged
+{
+ private static readonly string[] Fields =
+ typeof(TVTable).GetFields(BindingFlags.Instance | BindingFlags.Public).Select(x => x.Name).ToArray();
+
+ /// Initializes a new instance of the class.
+ /// Address to vtable. Usually the address of the object itself.
+ public ObjectVTableHook(void* ppVtbl)
+ : base(ppVtbl, Fields.Length)
+ {
+ }
+
+ /// Gets the original vtable.
+ public ref readonly TVTable OriginalVTable => ref MemoryMarshal.Cast(this.OriginalVTableSpan)[0];
+
+ /// Gets the overriden vtable.
+ public ref readonly TVTable OverridenVTable => ref MemoryMarshal.Cast(this.OverridenVTableSpan)[0];
+
+ /// Gets the index of the method by method name.
+ /// Name of the method.
+ /// Index of the method.
+ public int GetMethodIndex(string methodName) => Fields.IndexOf(methodName);
+
+ /// Gets the original method address of the given method index.
+ /// Name of the method.
+ /// Address of the original method.
+ public nint GetOriginalMethodAddress(string methodName) =>
+ this.GetOriginalMethodAddress(this.GetMethodIndex(methodName));
+
+ /// Gets the original method of the given method index, as a delegate of given type.
+ /// Name of the method.
+ /// Type of delegate.
+ /// Delegate to the original method.
+ public T GetOriginalMethodDelegate(string methodName)
+ where T : Delegate
+ => this.GetOriginalMethodDelegate(this.GetMethodIndex(methodName));
+
+ /// Resets a method to the original function.
+ /// Name of the method.
+ public void ResetVtableEntry(string methodName)
+ => this.ResetVtableEntry(this.GetMethodIndex(methodName));
+
+ /// Sets a method in vtable to the given address of function.
+ /// Name of the method.
+ /// Address of the detour function.
+ /// Additional reference to keep in memory.
+ public void SetVtableEntry(string methodName, nint pfn, object? refkeep)
+ => this.SetVtableEntry(this.GetMethodIndex(methodName), pfn, refkeep);
+
+ /// Sets a method in vtable to the given delegate.
+ /// Name of the method.
+ /// Detour delegate.
+ /// Type of delegate.
+ public void SetVtableEntry(string methodName, T detourDelegate)
+ where T : Delegate =>
+ this.SetVtableEntry(
+ this.GetMethodIndex(methodName),
+ Marshal.GetFunctionPointerForDelegate(detourDelegate),
+ detourDelegate);
+
+ /// Sets a method in vtable to the given delegate.
+ /// Name of the method.
+ /// Detour delegate.
+ /// Original method delegate.
+ /// Type of delegate.
+ public void SetVtableEntry(string methodName, T detourDelegate, out T originalMethodDelegate)
+ where T : Delegate
+ => this.SetVtableEntry(this.GetMethodIndex(methodName), detourDelegate, out originalMethodDelegate);
+
+ /// Creates a new instance of that manages one entry in the vtable hook.
+ /// Name of the method.
+ /// Detour delegate.
+ /// Type of delegate.
+ /// A new instance of .
+ /// Even if a single hook is enabled, without , the hook will remain
+ /// disabled.
+ public Hook CreateHook(string methodName, T detourDelegate) where T : Delegate =>
+ this.CreateHook(this.GetMethodIndex(methodName), detourDelegate);
+}
diff --git a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs
index c5c4581e7..f03518ada 100644
--- a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs
+++ b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs
@@ -59,7 +59,7 @@ public sealed class SingleFontChooserDialog : IDisposable
private readonly int counter;
private readonly byte[] fontPreviewText = new byte[2048];
- private readonly TaskCompletionSource tcs = new();
+ private readonly TaskCompletionSource tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly IFontAtlas atlas;
private string popupImGuiName;
diff --git a/Dalamud/Interface/Internal/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs
index cc63d887f..fb64ad979 100644
--- a/Dalamud/Interface/Internal/DalamudCommands.cs
+++ b/Dalamud/Interface/Internal/DalamudCommands.cs
@@ -329,7 +329,7 @@ internal class DalamudCommands : IServiceType
chatGui.Print(new SeStringBuilder()
.AddItalics("Dalamud:")
- .AddText($" D{Util.AssemblyVersion}({Util.GetGitHash()}")
+ .AddText($" {Util.GetScmVersion()}")
.Build());
chatGui.Print(new SeStringBuilder()
diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs
index 71ba2b071..793dfddd9 100644
--- a/Dalamud/Interface/Internal/DalamudInterface.cs
+++ b/Dalamud/Interface/Internal/DalamudInterface.cs
@@ -190,6 +190,29 @@ internal class DalamudInterface : IInternalDisposableService
this.creditsDarkeningAnimation.Point1 = Vector2.Zero;
this.creditsDarkeningAnimation.Point2 = new Vector2(CreditsDarkeningMaxAlpha);
+
+ // This is temporary, until we know the repercussions of vtable hooking mode
+ consoleManager.AddCommand(
+ "dalamud.interface.swapchain_mode",
+ "Set swapchain hooking mode",
+ (string mode) =>
+ {
+ switch (mode)
+ {
+ case "vtable":
+ this.configuration.SwapChainHookMode = SwapChainHelper.HookMode.VTable;
+ break;
+ case "bytecode":
+ this.configuration.SwapChainHookMode = SwapChainHelper.HookMode.ByteCode;
+ break;
+ default:
+ Log.Error("Unknown swapchain mode: {Mode}", mode);
+ return false;
+ }
+
+ this.configuration.QueueSave();
+ return true;
+ });
}
private delegate nint CrashDebugDelegate(nint self);
@@ -818,10 +841,9 @@ internal class DalamudInterface : IInternalDisposableService
{
this.OpenBranchSwitcher();
}
-
- ImGui.MenuItem(Util.AssemblyVersion, false);
+
ImGui.MenuItem(this.dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown version", false);
- ImGui.MenuItem($"D: {Util.GetGitHash()}[{Util.GetGitCommitCount()}] CS: {Util.GetGitHashClientStructs()}[{FFXIVClientStructs.ThisAssembly.Git.Commits}]", false);
+ ImGui.MenuItem($"D: {Util.GetScmVersion()} CS: {Util.GetGitHashClientStructs()}[{FFXIVClientStructs.ThisAssembly.Git.Commits}]", false);
ImGui.MenuItem($"CLR: {Environment.Version}", false);
ImGui.EndMenu();
@@ -1020,7 +1042,7 @@ internal class DalamudInterface : IInternalDisposableService
{
ImGui.PushFont(InterfaceManager.MonoFont);
- ImGui.BeginMenu($"{Util.GetGitHash()}({Util.GetGitCommitCount()})", false);
+ ImGui.BeginMenu(Util.GetScmVersion(), false);
ImGui.BeginMenu(this.FrameCount.ToString("000000"), false);
ImGui.BeginMenu(ImGui.GetIO().Framerate.ToString("000"), false);
ImGui.BeginMenu($"W:{Util.FormatBytes(GC.GetTotalMemory(false))}", false);
diff --git a/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs b/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs
new file mode 100644
index 000000000..05af78949
--- /dev/null
+++ b/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs
@@ -0,0 +1,141 @@
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+using Dalamud.Utility;
+
+using TerraFX.Interop.DirectX;
+using TerraFX.Interop.Windows;
+
+namespace Dalamud.Interface.Internal;
+
+///
+/// This class manages interaction with the ImGui interface.
+///
+internal unsafe partial class InterfaceManager
+{
+ // NOTE: Do not use HRESULT as return value type. It appears that .NET marshaller thinks HRESULT needs to be still
+ // treated as a type that does not fit into RAX.
+
+ /// Delegate for DXGISwapChain::on_present(UINT flags, const DXGI_PRESENT_PARAMETERS *params) in
+ /// dxgi_swapchain.cpp.
+ /// Pointer to an instance of DXGISwapChain, which happens to be an
+ /// .
+ /// An integer value that contains swap-chain presentation options. These options are defined by
+ /// the DXGI_PRESENT constants.
+ /// Optional; DXGI present parameters.
+ [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
+ private delegate void ReShadeDxgiSwapChainPresentDelegate(
+ ReShadeDxgiSwapChain* swapChain,
+ uint flags,
+ DXGI_PRESENT_PARAMETERS* presentParams);
+
+ /// Delegate for .
+ /// Microsoft
+ /// Learn.
+ /// Pointer to an instance of .
+ /// An integer that specifies how to synchronize presentation of a frame with the
+ /// vertical blank.
+ /// An integer value that contains swap-chain presentation options. These options are defined by
+ /// the DXGI_PRESENT constants.
+ /// A representing the result of the operation.
+ [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
+ private delegate int DxgiSwapChainPresentDelegate(IDXGISwapChain* swapChain, uint syncInterval, uint flags);
+
+ /// Detour function for .
+ ///
+ /// Microsoft Learn.
+ /// Pointer to an instance of .
+ /// The number of buffers in the swap chain (including all back and front buffers).
+ /// This number can be different from the number of buffers with which you created the swap chain. This number
+ /// can't be greater than . Set this number to zero to preserve the
+ /// existing number of buffers in the swap chain. You can't specify less than two buffers for the flip presentation
+ /// model.
+ /// The new width of the back buffer. If you specify zero, DXGI will use the width of the client
+ /// area of the target window. You can't specify the width as zero if you called the
+ /// method to create the swap chain for a composition
+ /// surface.
+ /// The new height of the back buffer. If you specify zero, DXGI will use the height of the
+ /// client area of the target window. You can't specify the height as zero if you called the
+ /// method to create the swap chain for a composition
+ /// surface.
+ /// A DXGI_FORMAT-typed value for the new format of the back buffer. Set this value to
+ /// to preserve the existing format of the back buffer. The flip
+ /// presentation model supports a more restricted set of formats than the bit-block transfer (bitblt) model.
+ /// A combination of -typed values that are combined
+ /// by using a bitwise OR operation. The resulting value specifies options for swap-chain behavior.
+ /// A representing the result of the operation.
+ [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
+ private delegate int ResizeBuffersDelegate(
+ IDXGISwapChain* swapChain,
+ uint bufferCount,
+ uint width,
+ uint height,
+ DXGI_FORMAT newFormat,
+ uint swapChainFlags);
+
+ private void ReShadeDxgiSwapChainOnPresentDetour(
+ ReShadeDxgiSwapChain* swapChain,
+ uint flags,
+ DXGI_PRESENT_PARAMETERS* presentParams)
+ {
+ Debug.Assert(
+ this.reShadeDxgiSwapChainPresentHook is not null,
+ "this.reShadeDxgiSwapChainPresentHook is not null");
+
+ if (this.RenderDalamudCheckAndInitialize(swapChain->AsIDxgiSwapChain(), flags) is { } activeScene)
+ this.RenderDalamudDraw(activeScene);
+
+ this.reShadeDxgiSwapChainPresentHook!.Original(swapChain, flags, presentParams);
+
+ // Upstream call to system IDXGISwapChain::Present will be called by ReShade.
+ }
+
+ private int DxgiSwapChainPresentDetour(IDXGISwapChain* swapChain, uint syncInterval, uint flags)
+ {
+ Debug.Assert(this.dxgiSwapChainPresentHook is not null, "this.dxgiSwapChainPresentHook is not null");
+
+ if (this.RenderDalamudCheckAndInitialize(swapChain, flags) is { } activeScene)
+ this.RenderDalamudDraw(activeScene);
+
+ return this.dxgiSwapChainPresentHook!.Original(swapChain, syncInterval, flags);
+ }
+
+ private int AsHookDxgiSwapChainResizeBuffersDetour(
+ IDXGISwapChain* swapChain,
+ uint bufferCount,
+ uint width,
+ uint height,
+ DXGI_FORMAT newFormat,
+ uint swapChainFlags)
+ {
+ if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain))
+ return this.dxgiSwapChainResizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags);
+
+#if DEBUG
+ Log.Verbose(
+ $"Calling resizebuffers swap@{(nint)swapChain:X}{bufferCount} {width} {height} {newFormat} {swapChainFlags}");
+#endif
+
+ this.ResizeBuffers?.InvokeSafely();
+
+ this.backend?.OnPreResize();
+
+ var ret = this.dxgiSwapChainResizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags);
+ if (ret == DXGI.DXGI_ERROR_INVALID_CALL)
+ Log.Error("invalid call to resizeBuffers");
+
+ this.backend?.OnPostResize((int)width, (int)height);
+
+ return ret;
+ }
+
+ /// Represents DXGISwapChain in ReShade.
+ [StructLayout(LayoutKind.Sequential)]
+ private struct ReShadeDxgiSwapChain
+ {
+ // DXGISwapChain only implements IDXGISwapChain4. The only vtable should be that.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public IDXGISwapChain* AsIDxgiSwapChain() => (IDXGISwapChain*)Unsafe.AsPointer(ref this);
+ }
+}
diff --git a/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs b/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs
new file mode 100644
index 000000000..cd4b0a418
--- /dev/null
+++ b/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs
@@ -0,0 +1,72 @@
+using Dalamud.Interface.Internal.ReShadeHandling;
+using Dalamud.Utility;
+
+using TerraFX.Interop.DirectX;
+using TerraFX.Interop.Windows;
+
+namespace Dalamud.Interface.Internal;
+
+///
+/// This class manages interaction with the ImGui interface.
+///
+internal unsafe partial class InterfaceManager
+{
+ private void ReShadeAddonInterfaceOnDestroySwapChain(ref ReShadeAddonInterface.ApiObject swapChain)
+ {
+ var swapChainNative = swapChain.GetNative();
+ if (this.backend?.IsAttachedToPresentationTarget((nint)swapChainNative) is not true)
+ return;
+
+ this.backend?.OnPreResize();
+ }
+
+ private void ReShadeAddonInterfaceOnInitSwapChain(ref ReShadeAddonInterface.ApiObject swapChain)
+ {
+ var swapChainNative = swapChain.GetNative();
+ if (this.backend?.IsAttachedToPresentationTarget((nint)swapChainNative) is not true)
+ return;
+
+ DXGI_SWAP_CHAIN_DESC desc;
+ if (swapChainNative->GetDesc(&desc).FAILED)
+ return;
+
+ this.backend?.OnPostResize((int)desc.BufferDesc.Width, (int)desc.BufferDesc.Height);
+ }
+
+ private void ReShadeAddonInterfaceOnPresent(
+ ref ReShadeAddonInterface.ApiObject runtime,
+ ref ReShadeAddonInterface.ApiObject swapChain,
+ ReadOnlySpan sourceRect,
+ ReadOnlySpan destRect,
+ ReadOnlySpan dirtyRects)
+ {
+ var swapChainNative = swapChain.GetNative();
+
+ if (this.RenderDalamudCheckAndInitialize(swapChainNative, 0) is { } activebackend)
+ this.RenderDalamudDraw(activebackend);
+ }
+
+ private void ReShadeAddonInterfaceOnReShadeOverlay(ref ReShadeAddonInterface.ApiObject runtime)
+ {
+ var swapChainNative = runtime.GetNative();
+
+ if (this.RenderDalamudCheckAndInitialize(swapChainNative, 0) is { } activebackend)
+ this.RenderDalamudDraw(activebackend);
+ }
+
+ private int AsReShadeAddonDxgiSwapChainResizeBuffersDetour(
+ IDXGISwapChain* swapChain,
+ uint bufferCount,
+ uint width,
+ uint height,
+ DXGI_FORMAT newFormat,
+ uint swapChainFlags)
+ {
+ // Hooked vtbl instead of registering ReShade event. This check is correct.
+ if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain))
+ return this.dxgiSwapChainResizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags);
+
+ this.ResizeBuffers?.InvokeSafely();
+ return this.dxgiSwapChainResizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags);
+ }
+}
diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs
index f7087d3ac..65cce15e2 100644
--- a/Dalamud/Interface/Internal/InterfaceManager.cs
+++ b/Dalamud/Interface/Internal/InterfaceManager.cs
@@ -3,20 +3,26 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
-using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using CheapLoc;
+
using Dalamud.Configuration.Internal;
using Dalamud.Game;
using Dalamud.Game.ClientState.GamePad;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Hooking;
+using Dalamud.Hooking.Internal;
using Dalamud.Hooking.WndProcHook;
using Dalamud.Interface.ImGuiBackend;
+using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification.Internal;
+using Dalamud.Interface.Internal.DesignSystem;
using Dalamud.Interface.Internal.ManagedAsserts;
+using Dalamud.Interface.Internal.ReShadeHandling;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Style;
@@ -28,7 +34,7 @@ using Dalamud.Utility.Timing;
using ImGuiNET;
-using PInvoke;
+using JetBrains.Annotations;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
@@ -53,7 +59,7 @@ namespace Dalamud.Interface.Internal;
/// This class manages interaction with the ImGui interface.
///
[ServiceManager.EarlyLoadedService]
-internal class InterfaceManager : IInternalDisposableService
+internal partial class InterfaceManager : IInternalDisposableService
{
///
/// The default font size, in points.
@@ -71,19 +77,30 @@ internal class InterfaceManager : IInternalDisposableService
private readonly ConcurrentBag deferredDisposeDisposables = new();
[ServiceManager.ServiceDependency]
- private readonly WndProcHookManager wndProcHookManager = Service.Get();
+ private readonly DalamudConfiguration dalamudConfiguration = Service.Get();
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service.Get();
+ // ReShadeAddonInterface requires hooks to be alive to unregister itself.
+ [ServiceManager.ServiceDependency]
+ [UsedImplicitly]
+ private readonly HookManager hookManager = Service.Get();
+
+ [ServiceManager.ServiceDependency]
+ private readonly WndProcHookManager wndProcHookManager = Service.Get();
+
private readonly ConcurrentQueue runBeforeImGuiRender = new();
private readonly ConcurrentQueue runAfterImGuiRender = new();
private IWin32Backend? backend;
private Hook? setCursorHook;
- private Hook? presentHook;
- private Hook? resizeBuffersHook;
+ private Hook? reShadeDxgiSwapChainPresentHook;
+ private Hook? dxgiSwapChainPresentHook;
+ private Hook? dxgiSwapChainResizeBuffersHook;
+ private ObjectVTableHook>? dxgiSwapChainHook;
+ private ReShadeAddonInterface? reShadeAddonInterface;
private IFontAtlas? dalamudAtlas;
private ILockedImFont? defaultFontResourceLock;
@@ -99,14 +116,7 @@ internal class InterfaceManager : IInternalDisposableService
}
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
- private unsafe delegate HRESULT PresentDelegate(IDXGISwapChain* swapChain, uint syncInterval, uint presentFlags);
-
- [UnmanagedFunctionPointer(CallingConvention.StdCall)]
- private unsafe delegate HRESULT ResizeBuffersDelegate(
- IDXGISwapChain* swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags);
-
- [UnmanagedFunctionPointer(CallingConvention.StdCall)]
- private delegate HCURSOR SetCursorDelegate(HCURSOR hCursor);
+ private delegate nint SetCursorDelegate(nint hCursor);
///
/// This event gets called each frame to facilitate ImGui drawing.
@@ -205,7 +215,7 @@ internal class InterfaceManager : IInternalDisposableService
///
public bool IsDispatchingEvents { get; set; } = true;
- /// Gets a value indicating whether the main thread is executing .
+ /// Gets a value indicating whether the main thread is executing .
/// This still will be true even when queried off the main thread.
public bool IsMainThreadInPresent { get; private set; }
@@ -243,7 +253,7 @@ internal class InterfaceManager : IInternalDisposableService
///
public Task FontBuildTask => WhenFontsReady().dalamudAtlas!.BuildTask;
- /// Gets the number of calls to so far.
+ /// Gets the number of calls to so far.
///
/// The value increases even when Dalamud is hidden via "/xlui hide".
/// does not.
@@ -290,8 +300,11 @@ internal class InterfaceManager : IInternalDisposableService
{
this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc;
Interlocked.Exchange(ref this.setCursorHook, null)?.Dispose();
- Interlocked.Exchange(ref this.presentHook, null)?.Dispose();
- Interlocked.Exchange(ref this.resizeBuffersHook, null)?.Dispose();
+ Interlocked.Exchange(ref this.dxgiSwapChainPresentHook, null)?.Dispose();
+ Interlocked.Exchange(ref this.reShadeDxgiSwapChainPresentHook, null)?.Dispose();
+ Interlocked.Exchange(ref this.dxgiSwapChainResizeBuffersHook, null)?.Dispose();
+ Interlocked.Exchange(ref this.dxgiSwapChainHook, null)?.Dispose();
+ Interlocked.Exchange(ref this.reShadeAddonInterface, null)?.Dispose();
}
}
@@ -327,7 +340,7 @@ internal class InterfaceManager : IInternalDisposableService
/// A that resolves once is run.
public Task RunBeforeImGuiRender(Action action)
{
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
this.runBeforeImGuiRender.Enqueue(
() =>
{
@@ -350,7 +363,7 @@ internal class InterfaceManager : IInternalDisposableService
/// A that resolves once is run.
public Task RunBeforeImGuiRender(Func func)
{
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
this.runBeforeImGuiRender.Enqueue(
() =>
{
@@ -371,7 +384,7 @@ internal class InterfaceManager : IInternalDisposableService
/// A that resolves once is run.
public Task RunAfterImGuiRender(Action action)
{
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
this.runAfterImGuiRender.Enqueue(
() =>
{
@@ -394,7 +407,7 @@ internal class InterfaceManager : IInternalDisposableService
/// A that resolves once is run.
public Task RunAfterImGuiRender(Func func)
{
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
this.runAfterImGuiRender.Enqueue(
() =>
{
@@ -473,24 +486,72 @@ internal class InterfaceManager : IInternalDisposableService
return im;
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void RenderImGui(IImGuiBackend backend)
+ /// Checks if the provided swap chain is the target that Dalamud should draw its interface onto,
+ /// and initializes ImGui for drawing.
+ /// The swap chain to test and initialize ImGui with if conditions are met.
+ /// Flags passed to .
+ /// An initialized instance of , or null if
+ /// is not the main swap chain.
+ private unsafe IImGuiBackend? RenderDalamudCheckAndInitialize(IDXGISwapChain* swapChain, uint flags)
{
- var conf = Service.Get();
+ // Quoting ReShade dxgi_swapchain.cpp DXGISwapChain::on_present:
+ // > Some D3D11 games test presentation for timing and composition purposes
+ // > These calls are not rendering related, but rather a status request for the D3D runtime and as such should be ignored
+ if ((flags & DXGI.DXGI_PRESENT_TEST) != 0)
+ return null;
+
+ if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain))
+ return null;
+
+ Debug.Assert(this.dalamudAtlas is not null, "dalamudAtlas should have been set already");
+
+ var activeBackend = this.backend ?? this.InitBackend(swapChain);
+
+ if (!this.dalamudAtlas!.HasBuiltAtlas)
+ {
+ if (this.dalamudAtlas.BuildTask.Exception != null)
+ {
+ // TODO: Can we do something more user-friendly here? Unload instead?
+ Log.Error(this.dalamudAtlas.BuildTask.Exception, "Failed to initialize Dalamud base fonts");
+ Util.Fatal("Failed to initialize Dalamud base fonts.\nPlease report this error.", "Dalamud");
+ }
+
+ return null;
+ }
+
+ return activeBackend;
+ }
+
+ /// Draws Dalamud to the given scene representing the ImGui context.
+ /// The scene to draw to.
+ private void RenderDalamudDraw(IImGuiBackend activeBackend)
+ {
+ this.CumulativePresentCalls++;
+ this.IsMainThreadInPresent = true;
+
+ while (this.runBeforeImGuiRender.TryDequeue(out var action))
+ action.InvokeSafely();
// Process information needed by ImGuiHelpers each frame.
ImGuiHelpers.NewFrame();
// Enable viewports if there are no issues.
- if (conf.IsDisableViewport || backend.IsMainViewportFullScreen() || ImGui.GetPlatformIO().Monitors.Size == 1)
+ var viewportsEnable = this.dalamudConfiguration.IsDisableViewport ||
+ activeBackend.IsMainViewportFullScreen() ||
+ ImGui.GetPlatformIO().Monitors.Size == 1;
+ if (viewportsEnable)
ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.ViewportsEnable;
else
ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable;
- backend.Render();
+ // Call drawing functions, which in turn will call Draw event.
+ activeBackend.Render();
+
+ this.PostImGuiRender();
+ this.IsMainThreadInPresent = false;
}
- private unsafe void InitScene(IDXGISwapChain* swapChain)
+ private unsafe IImGuiBackend InitBackend(IDXGISwapChain* swapChain)
{
IWin32Backend newBackend;
using (Timings.Start("IM Scene Init"))
@@ -504,27 +565,33 @@ internal class InterfaceManager : IInternalDisposableService
Service.ProvideException(ex);
Log.Error(ex, "Could not load ImGui dependencies.");
- var res = User32.MessageBox(
- IntPtr.Zero,
- "Dalamud plugins require the Microsoft Visual C++ Redistributable to be installed.\nPlease install the runtime from the official Microsoft website or disable Dalamud.\n\nDo you want to download the redistributable now?",
- "Dalamud Error",
- User32.MessageBoxOptions.MB_YESNO | User32.MessageBoxOptions.MB_TOPMOST |
- User32.MessageBoxOptions.MB_ICONERROR);
-
- if (res == User32.MessageBoxResult.IDYES)
+ fixed (void* lpText =
+ "Dalamud plugins require the Microsoft Visual C++ Redistributable to be installed.\nPlease install the runtime from the official Microsoft website or disable Dalamud.\n\nDo you want to download the redistributable now?")
{
- var psi = new ProcessStartInfo
+ fixed (void* lpCaption = "Dalamud Error")
{
- FileName = "https://aka.ms/vs/16/release/vc_redist.x64.exe",
- UseShellExecute = true,
- };
- Process.Start(psi);
+ var res = MessageBoxW(
+ default,
+ (ushort*)lpText,
+ (ushort*)lpCaption,
+ MB.MB_YESNO | MB.MB_TOPMOST | MB.MB_ICONERROR);
+
+ if (res == IDYES)
+ {
+ var psi = new ProcessStartInfo
+ {
+ FileName = "https://aka.ms/vs/16/release/vc_redist.x64.exe",
+ UseShellExecute = true,
+ };
+ Process.Start(psi);
+ }
+ }
}
Environment.Exit(-1);
// Doesn't reach here, but to make the compiler not complain
- return;
+ throw new InvalidOperationException();
}
var startInfo = Service.Get().StartInfo;
@@ -621,6 +688,7 @@ internal class InterfaceManager : IInternalDisposableService
Service.Provide(new(this));
this.wndProcHookManager.PreWndProc += this.WndProcHookManagerOnPreWndProc;
+ return newBackend;
}
private void WndProcHookManagerOnPreWndProc(WndProcEventArgs args)
@@ -630,53 +698,6 @@ internal class InterfaceManager : IInternalDisposableService
args.SuppressWithValue(r.Value);
}
- /*
- * NOTE(goat): When hooking ReShade DXGISwapChain::runtime_present, this is missing the syncInterval arg.
- * Seems to work fine regardless, I guess, so whatever.
- */
- private unsafe HRESULT PresentDetour(IDXGISwapChain* swapChain, uint syncInterval, uint presentFlags)
- {
- Debug.Assert(this.presentHook is not null, "How did PresentDetour get called when presentHook is null?");
-
- if (this.backend is null)
- {
- this.InitScene(swapChain);
- if (this.backend is null)
- throw new InvalidOperationException("InitScene did not set this.scene?");
- }
-
- if (!this.backend.IsAttachedToPresentationTarget((nint)swapChain))
- return this.presentHook!.Original(swapChain, syncInterval, presentFlags);
-
- // Do not do anything yet if no font atlas has been built yet.
- if (this.dalamudAtlas?.HasBuiltAtlas is not true)
- {
- if (this.dalamudAtlas?.BuildTask.Exception != null)
- {
- // TODO: Can we do something more user-friendly here? Unload instead?
- Log.Error(this.dalamudAtlas.BuildTask.Exception, "Failed to initialize Dalamud base fonts");
- Util.Fatal("Failed to initialize Dalamud base fonts.\nPlease report this error.", "Dalamud");
- }
-
- return this.presentHook!.Original(swapChain, syncInterval, presentFlags);
- }
-
- this.IsMainThreadInPresent = true;
- this.CumulativePresentCalls++;
- this.PreImGuiRender();
- RenderImGui(this.backend!);
- this.PostImGuiRender();
- this.IsMainThreadInPresent = false;
-
- return this.presentHook!.Original(swapChain, syncInterval, presentFlags);
- }
-
- private void PreImGuiRender()
- {
- while (this.runBeforeImGuiRender.TryDequeue(out var action))
- action.InvokeSafely();
- }
-
private void PostImGuiRender()
{
while (this.runAfterImGuiRender.TryDequeue(out var action))
@@ -726,14 +747,13 @@ internal class InterfaceManager : IInternalDisposableService
GlyphMaxAdvanceX = DefaultFontSizePx,
})));
this.IconFontFixedWidthHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
- e => e.OnPreBuild(
- tk => tk.AddDalamudAssetFont(
- DalamudAsset.FontAwesomeFreeSolid,
- new()
- {
- SizePx = Service.Get().DefaultFontSpec.SizePx,
- GlyphRanges = [0x20, 0x20, 0x00],
- })));
+ e => e.OnPreBuild(tk => tk.AddDalamudAssetFont(
+ DalamudAsset.FontAwesomeFreeSolid,
+ new()
+ {
+ SizePx = Service.Get().DefaultFontSpec.SizePx,
+ GlyphRanges = [0x20, 0x20, 0x00],
+ })));
this.MonoFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
e => e.OnPreBuild(
tk => tk.AddDalamudAssetFont(
@@ -781,9 +801,13 @@ internal class InterfaceManager : IInternalDisposableService
_ = this.dalamudAtlas.BuildFontsAsync();
SwapChainHelper.BusyWaitForGameDeviceSwapChain();
+ var swapChainDesc = default(DXGI_SWAP_CHAIN_DESC);
+ if (SwapChainHelper.GameDeviceSwapChain->GetDesc(&swapChainDesc).SUCCEEDED)
+ this.gameWindowHandle = swapChainDesc.OutputWindow;
try
{
+ // Requires that game window to be there, which will be the case once game swap chain is initialized.
if (Service.Get().WindowIsImmersive)
this.SetImmersiveMode(true);
}
@@ -799,60 +823,206 @@ internal class InterfaceManager : IInternalDisposableService
0,
this.SetCursorDetour);
- Log.Verbose("===== S W A P C H A I N =====");
- this.resizeBuffersHook = Hook.FromAddress(
- (nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers,
- this.ResizeBuffersDetour);
- Log.Verbose($"ResizeBuffers address {Util.DescribeAddress(this.resizeBuffersHook!.Address)}");
+ if (ReShadeAddonInterface.ReShadeIsSignedByReShade)
+ {
+ Log.Warning("Signed ReShade binary detected");
+ Service
+ .GetAsync()
+ .ContinueWith(
+ nmt => nmt.Result.AddNotification(
+ new()
+ {
+ MinimizedText = Loc.Localize(
+ "ReShadeNoAddonSupportNotificationMinimizedText",
+ "Wrong ReShade installation"),
+ Content = Loc.Localize(
+ "ReShadeNoAddonSupportNotificationContent",
+ "Your installation of ReShade does not have full addon support, and may not work with Dalamud and/or the game.\n" +
+ "Download and install ReShade with full addon-support."),
+ Type = NotificationType.Warning,
+ InitialDuration = TimeSpan.MaxValue,
+ ShowIndeterminateIfNoExpiry = false,
+ })).ContinueWith(
+ t =>
+ {
+ t.Result.DrawActions += _ =>
+ {
+ ImGuiHelpers.ScaledDummy(2);
+ if (DalamudComponents.PrimaryButton(Loc.Localize("LearnMore", "Learn more...")))
+ {
+ Util.OpenLink("https://dalamud.dev/news/2024/07/23/reshade/");
+ }
+ };
+ });
+ }
- this.presentHook = Hook.FromAddress(
- (nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present,
- this.PresentDetour);
- Log.Verbose(
- $"IDXGISwapChain::Present address {Util.DescribeAddress(SwapChainHelper.GameDeviceSwapChainVtbl->Present)}");
+ Log.Information("===== S W A P C H A I N =====");
+ var sb = new StringBuilder();
+ foreach (var m in ReShadeAddonInterface.AllReShadeModules)
+ {
+ sb.Clear();
+ sb.Append("ReShade detected: ");
+ sb.Append(m.FileName).Append('(');
+ sb.Append(m.FileVersionInfo.OriginalFilename);
+ sb.Append("; ").Append(m.FileVersionInfo.ProductName);
+ sb.Append("; ").Append(m.FileVersionInfo.ProductVersion);
+ sb.Append("; ").Append(m.FileVersionInfo.FileDescription);
+ sb.Append("; ").Append(m.FileVersionInfo.FileVersion);
+ sb.Append($"@ 0x{m.BaseAddress:X}");
+ if (!ReferenceEquals(m, ReShadeAddonInterface.ReShadeModule))
+ sb.Append(" [ignored by Dalamud]");
+ Log.Information(sb.ToString());
+ }
+
+ if (ReShadeAddonInterface.AllReShadeModules.Length > 1)
+ Log.Warning("Multiple ReShade dlls are detected.");
+
+ ResizeBuffersDelegate dxgiSwapChainResizeBuffersDelegate;
+ ReShadeDxgiSwapChainPresentDelegate? reShadeDxgiSwapChainPresentDelegate = null;
+ DxgiSwapChainPresentDelegate? dxgiSwapChainPresentDelegate = null;
+ nint pfnReShadeDxgiSwapChainPresent = 0;
+ switch (this.dalamudConfiguration.ReShadeHandlingMode)
+ {
+ // If ReShade is not found, do no special handling.
+ case var _ when ReShadeAddonInterface.ReShadeModule is null:
+ goto default;
+
+ // This is the only mode honored when SwapChainHookMode is set to VTable.
+ case ReShadeHandlingMode.Default:
+ case ReShadeHandlingMode.UnwrapReShade:
+ if (SwapChainHelper.UnwrapReShade())
+ Log.Information("Unwrapped ReShade");
+ else
+ Log.Warning("Could not unwrap ReShade");
+ goto default;
+
+ // Do no special ReShade handling.
+ // If SwapChainHookMode is set to VTable, do no special handling.
+ case ReShadeHandlingMode.None:
+ case var _ when this.dalamudConfiguration.SwapChainHookMode == SwapChainHelper.HookMode.VTable:
+ default:
+ dxgiSwapChainResizeBuffersDelegate = this.AsHookDxgiSwapChainResizeBuffersDetour;
+ dxgiSwapChainPresentDelegate = this.DxgiSwapChainPresentDetour;
+ break;
+
+ // Register Dalamud as a ReShade addon.
+ case ReShadeHandlingMode.ReShadeAddonPresent:
+ case ReShadeHandlingMode.ReShadeAddonReShadeOverlay:
+ if (!ReShadeAddonInterface.TryRegisterAddon(out this.reShadeAddonInterface))
+ {
+ Log.Warning("Could not register as ReShade addon");
+ goto default;
+ }
+
+ Log.Information("Registered as a ReShade addon");
+ this.reShadeAddonInterface.InitSwapChain += this.ReShadeAddonInterfaceOnInitSwapChain;
+ this.reShadeAddonInterface.DestroySwapChain += this.ReShadeAddonInterfaceOnDestroySwapChain;
+ if (this.dalamudConfiguration.ReShadeHandlingMode == ReShadeHandlingMode.ReShadeAddonPresent)
+ this.reShadeAddonInterface.Present += this.ReShadeAddonInterfaceOnPresent;
+ else
+ this.reShadeAddonInterface.ReShadeOverlay += this.ReShadeAddonInterfaceOnReShadeOverlay;
+
+ dxgiSwapChainResizeBuffersDelegate = this.AsReShadeAddonDxgiSwapChainResizeBuffersDetour;
+ break;
+
+ // Hook ReShade's DXGISwapChain::on_present. This is the legacy and the default option.
+ case ReShadeHandlingMode.HookReShadeDxgiSwapChainOnPresent:
+ pfnReShadeDxgiSwapChainPresent = ReShadeAddonInterface.FindReShadeDxgiSwapChainOnPresent();
+
+ if (pfnReShadeDxgiSwapChainPresent == 0)
+ {
+ Log.Warning("ReShade::DXGISwapChain::on_present could not be found");
+ goto default;
+ }
+
+ Log.Information(
+ "Found ReShade::DXGISwapChain::on_present at {addr}",
+ Util.DescribeAddress(pfnReShadeDxgiSwapChainPresent));
+ reShadeDxgiSwapChainPresentDelegate = this.ReShadeDxgiSwapChainOnPresentDetour;
+ dxgiSwapChainResizeBuffersDelegate = this.AsHookDxgiSwapChainResizeBuffersDetour;
+ break;
+ }
+
+ switch (this.dalamudConfiguration.SwapChainHookMode)
+ {
+ case SwapChainHelper.HookMode.ByteCode:
+ default:
+ {
+ Log.Information("Hooking using bytecode...");
+ this.dxgiSwapChainResizeBuffersHook = Hook.FromAddress(
+ (nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers,
+ dxgiSwapChainResizeBuffersDelegate);
+ Log.Information(
+ "Hooked IDXGISwapChain::ResizeBuffers using bytecode: {addr}",
+ Util.DescribeAddress(this.dxgiSwapChainResizeBuffersHook.Address));
+
+ if (dxgiSwapChainPresentDelegate is not null)
+ {
+ this.dxgiSwapChainPresentHook = Hook.FromAddress(
+ (nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present,
+ dxgiSwapChainPresentDelegate);
+ Log.Information(
+ "Hooked IDXGISwapChain::Present using bytecode: {addr}",
+ Util.DescribeAddress(this.dxgiSwapChainPresentHook.Address));
+ }
+
+ if (reShadeDxgiSwapChainPresentDelegate is not null && pfnReShadeDxgiSwapChainPresent != 0)
+ {
+ this.reShadeDxgiSwapChainPresentHook = Hook.FromAddress(
+ pfnReShadeDxgiSwapChainPresent,
+ reShadeDxgiSwapChainPresentDelegate);
+ Log.Information(
+ "Hooked ReShade::DXGISwapChain::on_present using bytecode: {addr}",
+ Util.DescribeAddress(this.reShadeDxgiSwapChainPresentHook.Address));
+ }
+
+ break;
+ }
+
+ case SwapChainHelper.HookMode.VTable:
+ {
+ Log.Information("Hooking using VTable...");
+ this.dxgiSwapChainHook = new(SwapChainHelper.GameDeviceSwapChain);
+ this.dxgiSwapChainResizeBuffersHook = this.dxgiSwapChainHook.CreateHook(
+ nameof(IDXGISwapChain.ResizeBuffers),
+ dxgiSwapChainResizeBuffersDelegate);
+ Log.Information(
+ "Hooked IDXGISwapChain::ResizeBuffers using VTable: {addr}",
+ Util.DescribeAddress(this.dxgiSwapChainResizeBuffersHook.Address));
+
+ if (dxgiSwapChainPresentDelegate is not null)
+ {
+ this.dxgiSwapChainPresentHook = this.dxgiSwapChainHook.CreateHook(
+ nameof(IDXGISwapChain.Present),
+ dxgiSwapChainPresentDelegate);
+ Log.Information(
+ "Hooked IDXGISwapChain::Present using VTable: {addr}",
+ Util.DescribeAddress(this.dxgiSwapChainPresentHook.Address));
+ }
+
+ Log.Information(
+ "Detouring vtable at {addr}: {prev} to {new}",
+ Util.DescribeAddress(this.dxgiSwapChainHook.Address),
+ Util.DescribeAddress(this.dxgiSwapChainHook.OriginalVTableAddress),
+ Util.DescribeAddress(this.dxgiSwapChainHook.OverridenVTableAddress));
+ break;
+ }
+ }
this.setCursorHook.Enable();
- this.presentHook.Enable();
- this.resizeBuffersHook.Enable();
+ this.reShadeDxgiSwapChainPresentHook?.Enable();
+ this.dxgiSwapChainResizeBuffersHook.Enable();
+ this.dxgiSwapChainPresentHook?.Enable();
+ this.dxgiSwapChainHook?.Enable();
}
- private unsafe HRESULT ResizeBuffersDetour(
- IDXGISwapChain* swapChain,
- uint bufferCount,
- uint width,
- uint height,
- uint newFormat,
- uint swapChainFlags)
- {
-#if DEBUG
- Log.Verbose(
- $"Calling resizebuffers swap@{(nint)swapChain:X}{bufferCount} {width} {height} {newFormat} {swapChainFlags}");
-#endif
-
- this.ResizeBuffers?.InvokeSafely();
-
- // We have to ensure we're working with the main swapchain, as other viewports might be resizing as well.
- if (this.backend?.IsAttachedToPresentationTarget((nint)swapChain) is not true)
- return this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags);
-
- this.backend?.OnPreResize();
-
- var ret = this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags);
- if (ret == DXGI.DXGI_ERROR_INVALID_CALL)
- Log.Error("invalid call to resizeBuffers");
-
- this.backend?.OnPostResize((int)width, (int)height);
-
- return ret;
- }
-
- private HCURSOR SetCursorDetour(HCURSOR hCursor)
+ private nint SetCursorDetour(nint hCursor)
{
if (this.lastWantCapture && (!this.backend?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor)
return default;
return this.setCursorHook?.IsDisposed is not false
- ? SetCursor(hCursor)
+ ? SetCursor((HCURSOR)hCursor)
: this.setCursorHook.Original(hCursor);
}
diff --git a/Dalamud/Interface/Internal/PluginCategoryManager.cs b/Dalamud/Interface/Internal/PluginCategoryManager.cs
index ec2a1c15b..71c869ede 100644
--- a/Dalamud/Interface/Internal/PluginCategoryManager.cs
+++ b/Dalamud/Interface/Internal/PluginCategoryManager.cs
@@ -23,6 +23,7 @@ internal class PluginCategoryManager
new(CategoryKind.All, "special.all", () => Locs.Category_All),
new(CategoryKind.IsTesting, "special.isTesting", () => Locs.Category_IsTesting, CategoryInfo.AppearCondition.DoPluginTest),
new(CategoryKind.AvailableForTesting, "special.availableForTesting", () => Locs.Category_AvailableForTesting, CategoryInfo.AppearCondition.DoPluginTest),
+ new(CategoryKind.Hidden, "special.hidden", () => Locs.Category_Hidden, CategoryInfo.AppearCondition.AnyHiddenPlugins),
new(CategoryKind.DevInstalled, "special.devInstalled", () => Locs.Category_DevInstalled),
new(CategoryKind.IconTester, "special.devIconTester", () => Locs.Category_IconTester),
new(CategoryKind.DalamudChangelogs, "special.dalamud", () => Locs.Category_Dalamud),
@@ -106,6 +107,11 @@ internal class PluginCategoryManager
///
AvailableForTesting = 2,
+ ///
+ /// Plugins that were hidden.
+ ///
+ Hidden = 3,
+
///
/// Installed dev plugins.
///
@@ -309,6 +315,9 @@ internal class PluginCategoryManager
{
groupAvail.Categories.Add(this.CategoryList[categoryIdx].CategoryKind);
}
+
+ // Hidden at the end
+ groupAvail.Categories.Add(CategoryKind.Hidden);
// compare with prev state and mark as dirty if needed
var noCategoryChanges = prevCategoryIds.SequenceEqual(groupAvail.Categories);
@@ -332,7 +341,10 @@ internal class PluginCategoryManager
{
var groupInfo = this.groupList[this.currentGroupIdx];
- var includeAll = (this.currentCategoryKind == CategoryKind.All) || (groupInfo.GroupKind != GroupKind.Available);
+ var includeAll = this.currentCategoryKind == CategoryKind.All ||
+ this.currentCategoryKind == CategoryKind.Hidden ||
+ groupInfo.GroupKind != GroupKind.Available;
+
if (includeAll)
{
result.AddRange(plugins);
@@ -455,6 +467,11 @@ internal class PluginCategoryManager
/// Check if plugin testing is enabled.
///
DoPluginTest,
+
+ ///
+ /// Check if there are any hidden plugins.
+ ///
+ AnyHiddenPlugins,
}
///
@@ -529,6 +546,8 @@ internal class PluginCategoryManager
public static string Category_AvailableForTesting => Loc.Localize("InstallerCategoryAvailableForTesting", "Testing Available");
+ public static string Category_Hidden => Loc.Localize("InstallerCategoryHidden", "Hidden");
+
public static string Category_DevInstalled => Loc.Localize("InstallerInstalledDevPlugins", "Installed Dev Plugins");
public static string Category_IconTester => "Image/Icon Tester";
diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.AddonEvent.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.AddonEvent.cs
new file mode 100644
index 000000000..c68cf4fb6
--- /dev/null
+++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.AddonEvent.cs
@@ -0,0 +1,1706 @@
+namespace Dalamud.Interface.Internal.ReShadeHandling;
+
+/// ReShade interface.
+internal sealed partial class ReShadeAddonInterface
+{
+ /// Supported events emitted by ReShade.
+ private enum AddonEvent : uint
+ {
+#pragma warning disable
+
+ ///
+ /// Called after successful device creation, from:
+ ///
+ /// IDirect3D9::CreateDevice
+ /// IDirect3D9Ex::CreateDeviceEx
+ /// IDirect3DDevice9::Reset
+ /// IDirect3DDevice9Ex::ResetEx
+ /// D3D10CreateDevice
+ /// D3D10CreateDevice1
+ /// D3D10CreateDeviceAndSwapChain
+ /// D3D10CreateDeviceAndSwapChain1
+ /// D3D11CreateDevice
+ /// D3D11CreateDeviceAndSwapChain
+ /// D3D12CreateDevice
+ /// glMakeCurrent
+ /// vkCreateDevice
+ ///
+ /// Callback function signature: void (api::device *device)
+ ///
+ InitDevice,
+
+ ///
+ /// Called on device destruction, before:
+ ///
+ /// IDirect3DDevice9::Reset
+ /// IDirect3DDevice9Ex::ResetEx
+ /// IDirect3DDevice9::Release
+ /// ID3D10Device::Release
+ /// ID3D11Device::Release
+ /// ID3D12Device::Release
+ /// wglDeleteContext
+ /// vkDestroyDevice
+ ///
+ /// Callback function signature: void (api::device *device)
+ ///
+ DestroyDevice,
+
+ ///
+ /// Called after successful command list creation, from:
+ ///
+ /// ID3D11Device::CreateDeferredContext
+ /// ID3D11Device1::CreateDeferredContext1
+ /// ID3D11Device2::CreateDeferredContext2
+ /// ID3D11Device3::CreateDeferredContext3
+ /// ID3D12Device::CreateCommandList
+ /// ID3D12Device4::CreateCommandList1
+ /// vkAllocateCommandBuffers
+ ///
+ /// Callback function signature: void (api::command_list *cmd_list)
+ ///
+ ///
+ /// In case of D3D9, D3D10, D3D11 and OpenGL this is called during device initialization as well and behaves as if an implicit immediate command list was created.
+ ///
+ InitCommandList,
+
+ ///
+ /// Called on command list destruction, before:
+ ///
+ /// ID3D11CommandList::Release
+ /// ID3D12CommandList::Release
+ /// vkFreeCommandBuffers
+ ///
+ /// Callback function signature: void (api::command_list *cmd_list)
+ ///
+ DestroyCommandList,
+
+ ///
+ /// Called after successful command queue creation, from:
+ ///
+ /// ID3D12Device::CreateCommandQueue
+ /// vkCreateDevice (for every queue associated with the device)
+ ///
+ /// Callback function signature: void (api::command_queue *queue)
+ ///
+ ///
+ /// In case of D3D9, D3D10, D3D11 and OpenGL this is called during device initialization as well and behaves as if an implicit command queue was created.
+ ///
+ InitCommandQueue,
+
+ ///
+ /// Called on command queue destruction, before:
+ ///
+ /// ID3D12CommandQueue::Release
+ /// vkDestroyDevice (for every queue associated with the device)
+ ///
+ /// Callback function signature: void (api::command_queue *queue)
+ ///
+ DestroyCommandQueue,
+
+ ///
+ /// Called after successful swap chain creation, from:
+ ///
+ /// IDirect3D9::CreateDevice (for the implicit swap chain)
+ /// IDirect3D9Ex::CreateDeviceEx (for the implicit swap chain)
+ /// IDirect3D9Device::CreateAdditionalSwapChain
+ /// IDXGIFactory::CreateSwapChain
+ /// IDXGIFactory2::CreateSwapChain(...)
+ /// wglMakeCurrent
+ /// wglSwapBuffers (after window was resized)
+ /// vkCreateSwapchainKHR
+ /// xrCreateSession
+ ///
+ /// In addition, called when swap chain is resized, after:
+ ///
+ /// IDirect3DDevice9::Reset (for the implicit swap chain)
+ /// IDirect3DDevice9Ex::ResetEx (for the implicit swap chain)
+ /// IDXGISwapChain::ResizeBuffers
+ /// IDXGISwapChain3::ResizeBuffers1
+ ///
+ /// Callback function signature: void (api::swapchain *swapchain)
+ ///
+ InitSwapChain,
+
+ ///
+ /// Called on swap chain creation, before:
+ ///
+ /// IDirect3D9::CreateDevice (for the implicit swap chain)
+ /// IDirect3D9Ex::CreateDeviceEx (for the implicit swap chain)
+ /// IDirect3D9Device::CreateAdditionalSwapChain
+ /// IDirect3D9Device::Reset (for the implicit swap chain)
+ /// IDirect3D9DeviceEx::ResetEx (for the implicit swap chain)
+ /// IDXGIFactory::CreateSwapChain
+ /// IDXGIFactory2::CreateSwapChain(...)
+ /// IDXGISwapChain::ResizeBuffers
+ /// IDXGISwapChain3::ResizeBuffers1
+ /// wglSetPixelFormat
+ /// vkCreateSwapchainKHR
+ ///
+ /// Callback function signature: bool (api::swapchain_desc &desc, void *hwnd)
+ ///
+ ///
+ /// To overwrite the swap chain description, modify desc in the callback and return , otherwise return .
+ ///
+ CreateSwapChain,
+
+ ///
+ /// Called on swap chain destruction, before:
+ ///
+ /// IDirect3DDevice9::Release (for the implicit swap chain)
+ /// IDirect3DSwapChain9::Release
+ /// IDXGISwapChain::Release
+ /// wglDeleteContext
+ /// wglSwapBuffers (after window was resized)
+ /// vkDestroySwapchainKHR
+ /// xrDestroySession
+ ///
+ /// In addition, called when swap chain is resized, before:
+ ///
+ /// IDirect3DDevice9::Reset (for the implicit swap chain)
+ /// IDirect3DDevice9Ex::ResetEx (for the implicit swap chain)
+ /// IDXGISwapChain::ResizeBuffers
+ /// IDXGISwapChain1::ResizeBuffers1
+ ///
+ /// Callback function signature: void (api::swapchain *swapchain)
+ ///
+ DestroySwapChain,
+
+ ///
+ /// Called after effect runtime initialization (which happens after swap chain creation or a swap chain buffer resize).
+ /// Callback function signature: void (api::effect_runtime *runtime)
+ ///
+ InitEffectRuntime,
+
+ ///
+ /// Called when an effect runtime is reset or destroyed.
+ /// Callback function signature: void (api::effect_runtime *runtime)
+ ///
+ DestroyEffectRuntime,
+
+ ///
+ /// Called after successful sampler creation from:
+ ///
+ /// ID3D10Device::CreateSamplerState
+ /// ID3D11Device::CreateSamplerState
+ /// ID3D12Device::CreateSampler
+ /// vkCreateSampler
+ ///
+ /// Callback function signature: void (api::device *device, const api::sampler_desc &desc, api::sampler sampler)
+ ///
+ ///
+ /// Is not called in D3D9 (since samplers are loose state there) or OpenGL.
+ ///
+ InitSampler,
+
+ ///
+ /// Called on sampler creation, before:
+ ///
+ /// ID3D10Device::CreateSamplerState
+ /// ID3D11Device::CreateSamplerState
+ /// ID3D12Device::CreateSampler
+ /// ID3D12Device::CreateRootSignature
+ /// vkCreateSampler
+ ///
+ /// Callback function signature: bool (api::device *device, api::sampler_desc &desc)
+ ///
+ ///
+ /// To overwrite the sampler description, modify desc in the callback and return , otherwise return .
+ /// Is not called in D3D9 (since samplers are loose state there) or OpenGL.
+ ///
+ CreateSampler,
+
+ ///
+ /// Called on sampler destruction, before:
+ ///
+ /// ID3D10SamplerState::Release
+ /// ID3D11SamplerState::Release
+ /// glDeleteSamplers
+ /// vkDestroySampler
+ ///
+ /// Callback function signature: void (api::device *device, api::sampler sampler)
+ ///
+ ///
+ /// Is not called in D3D9 (since samplers are loose state there), D3D12 (since samplers are descriptor handles instead of objects there) or OpenGL.
+ ///
+ DestroySampler,
+
+ ///
+ /// Called after successful resource creation from:
+ ///
+ /// IDirect3DDevice9::CreateVertexBuffer
+ /// IDirect3DDevice9::CreateIndexBuffer
+ /// IDirect3DDevice9::CreateTexture
+ /// IDirect3DDevice9::CreateCubeTexture
+ /// IDirect3DDevice9::CreateVolumeTexture
+ /// IDirect3DDevice9::CreateRenderTargetSurface
+ /// IDirect3DDevice9::CreateDepthStencilSurface
+ /// IDirect3DDevice9::CreateOffscreenPlainSurface
+ /// IDirect3DDevice9Ex::CreateRenderTargetSurfaceEx
+ /// IDirect3DDevice9Ex::CreateDepthStencilSurfaceEx
+ /// IDirect3DDevice9Ex::CreateOffscreenPlainSurfaceEx
+ /// ID3D10Device::CreateBuffer
+ /// ID3D10Device::CreateTexture1D
+ /// ID3D10Device::CreateTexture2D
+ /// ID3D10Device::CreateTexture2D
+ /// ID3D11Device::CreateBuffer
+ /// ID3D11Device::CreateTexture1D
+ /// ID3D11Device::CreateTexture2D
+ /// ID3D11Device::CreateTexture3D
+ /// ID3D11Device3::CreateTexture2D
+ /// ID3D11Device3::CreateTexture3D
+ /// ID3D12Device::CreateCommittedResource
+ /// ID3D12Device::CreatePlacedResource
+ /// ID3D12Device::CreateReservedResource
+ /// ID3D12Device4::CreateCommittedResource1
+ /// ID3D12Device4::CreateReservedResource1
+ /// glBufferData
+ /// glBufferStorage
+ /// glNamedBufferData
+ /// glNamedBufferStorage
+ /// glTexImage1D
+ /// glTexImage2D
+ /// glTexImage2DMultisample
+ /// glTexImage3D
+ /// glTexImage3DMultisample
+ /// glCompressedTexImage1D
+ /// glCompressedTexImage2D
+ /// glCompressedTexImage3D
+ /// glTexStorage1D
+ /// glTexStorage2D
+ /// glTexStorage2DMultisample
+ /// glTexStorage3D
+ /// glTexStorage3DMultisample
+ /// glTextureStorage1D
+ /// glTextureStorage2D
+ /// glTextureStorage2DMultisample
+ /// glTextureStorage3D
+ /// glTextureStorage3DMultisample
+ /// glRenderbufferStorage
+ /// glRenderbufferStorageMultisample
+ /// glNamedRenderbufferStorage
+ /// glNamedRenderbufferStorageMultisample
+ /// vkBindBufferMemory
+ /// vkBindBufferMemory2
+ /// vkBindImageMemory
+ /// vkBindImageMemory2
+ ///
+ /// Callback function signature: void (api::device *device, const api::resource_desc &desc, const api::subresource_data *initial_data, api::resource_usage initial_state, api::resource resource)
+ ///
+ ///
+ /// May be called multiple times with the same resource handle (whenever the resource is updated or its reference count is incremented).
+ ///
+ InitResource,
+
+ ///
+ /// Called on resource creation, before:
+ ///
+ /// IDirect3DDevice9::CreateVertexBuffer
+ /// IDirect3DDevice9::CreateIndexBuffer
+ /// IDirect3DDevice9::CreateTexture
+ /// IDirect3DDevice9::CreateCubeTexture
+ /// IDirect3DDevice9::CreateVolumeTexture
+ /// IDirect3DDevice9::CreateRenderTargetSurface
+ /// IDirect3DDevice9::CreateDepthStencilSurface
+ /// IDirect3DDevice9::CreateOffscreenPlainSurface
+ /// IDirect3DDevice9Ex::CreateRenderTargetSurfaceEx
+ /// IDirect3DDevice9Ex::CreateDepthStencilSurfaceEx
+ /// IDirect3DDevice9Ex::CreateOffscreenPlainSurfaceEx
+ /// ID3D10Device::CreateBuffer
+ /// ID3D10Device::CreateTexture1D
+ /// ID3D10Device::CreateTexture2D
+ /// ID3D10Device::CreateTexture2D
+ /// ID3D11Device::CreateBuffer
+ /// ID3D11Device::CreateTexture1D
+ /// ID3D11Device::CreateTexture2D
+ /// ID3D11Device::CreateTexture3D
+ /// ID3D11Device3::CreateTexture2D
+ /// ID3D11Device3::CreateTexture3D
+ /// ID3D12Device::CreateCommittedResource
+ /// ID3D12Device::CreatePlacedResource
+ /// ID3D12Device::CreateReservedResource
+ /// ID3D12Device4::CreateCommittedResource1
+ /// ID3D12Device4::CreateReservedResource1
+ /// glBufferData
+ /// glBufferStorage
+ /// glNamedBufferData
+ /// glNamedBufferStorage
+ /// glTexImage1D
+ /// glTexImage2D
+ /// glTexImage2DMultisample
+ /// glTexImage3D
+ /// glTexImage3DMultisample
+ /// glCompressedTexImage1D
+ /// glCompressedTexImage2D
+ /// glCompressedTexImage3D
+ /// glTexStorage1D
+ /// glTexStorage2D
+ /// glTexStorage2DMultisample
+ /// glTexStorage3D
+ /// glTexStorage3DMultisample
+ /// glTextureStorage1D
+ /// glTextureStorage2D
+ /// glTextureStorage2DMultisample
+ /// glTextureStorage3D
+ /// glTextureStorage3DMultisample
+ /// glRenderbufferStorage
+ /// glRenderbufferStorageMultisample
+ /// glNamedRenderbufferStorage
+ /// glNamedRenderbufferStorageMultisample
+ /// vkCreateBuffer
+ /// vkCreateImage
+ ///
+ /// Callback function signature: bool (api::device *device, api::resource_desc &desc, api::subresource_data *initial_data, api::resource_usage initial_state)
+ ///
+ ///
+ /// To overwrite the resource description, modify desc in the callback and return , otherwise return .
+ ///
+ CreateResource,
+
+ ///
+ /// Called on resource destruction, before:
+ ///
+ /// IDirect3DResource9::Release
+ /// ID3D10Resource::Release
+ /// ID3D11Resource::Release
+ /// ID3D12Resource::Release
+ /// glDeleteBuffers
+ /// glDeleteTextures
+ /// glDeleteRenderbuffers
+ /// vkDestroyBuffer
+ /// vkDestroyImage
+ ///
+ /// Callback function signature: void (api::device *device, api::resource resource)
+ ///
+ DestroyResource,
+
+ ///
+ /// Called after successful resource view creation from:
+ ///
+ /// IDirect3DDevice9::CreateTexture
+ /// IDirect3DDevice9::CreateCubeTexture
+ /// IDirect3DDevice9::CreateVolumeTexture
+ /// ID3D10Device::CreateShaderResourceView
+ /// ID3D10Device::CreateRenderTargetView
+ /// ID3D10Device::CreateDepthStencilView
+ /// ID3D10Device1::CreateShaderResourceView1
+ /// ID3D11Device::CreateShaderResourceView
+ /// ID3D11Device::CreateUnorderedAccessView
+ /// ID3D11Device::CreateRenderTargetView
+ /// ID3D11Device::CreateDepthStencilView
+ /// ID3D11Device3::CreateShaderResourceView1
+ /// ID3D11Device3::CreateUnorderedAccessView1
+ /// ID3D11Device3::CreateRenderTargetView1
+ /// ID3D12Device::CreateShaderResourceView
+ /// ID3D12Device::CreateUnorderedAccessView
+ /// ID3D12Device::CreateRenderTargetView
+ /// ID3D12Device::CreateDepthStencilView
+ /// glTexBuffer
+ /// glTextureBuffer
+ /// glTextureView
+ /// vkCreateBufferView
+ /// vkCreateImageView
+ /// vkCreateAccelerationStructureKHR
+ ///
+ /// Callback function signature: void (api::device *device, api::resource resource, api::resource_usage usage_type, const api::resource_view_desc &desc, api::resource_view view)
+ ///
+ ///
+ /// May be called multiple times with the same resource view handle (whenever the resource view is updated).
+ ///
+ InitResourceView,
+
+ ///
+ /// Called on resource view creation, before:
+ ///
+ /// ID3D10Device::CreateShaderResourceView
+ /// ID3D10Device::CreateRenderTargetView
+ /// ID3D10Device::CreateDepthStencilView
+ /// ID3D10Device1::CreateShaderResourceView1
+ /// ID3D11Device::CreateShaderResourceView
+ /// ID3D11Device::CreateUnorderedAccessView
+ /// ID3D11Device::CreateRenderTargetView
+ /// ID3D11Device::CreateDepthStencilView
+ /// ID3D11Device3::CreateShaderResourceView1
+ /// ID3D11Device3::CreateUnorderedAccessView1
+ /// ID3D11Device3::CreateRenderTargetView1
+ /// ID3D12Device::CreateShaderResourceView
+ /// ID3D12Device::CreateUnorderedAccessView
+ /// ID3D12Device::CreateRenderTargetView
+ /// ID3D12Device::CreateDepthStencilView
+ /// glTexBuffer
+ /// glTextureBuffer
+ /// glTextureView
+ /// vkCreateBufferView
+ /// vkCreateImageView
+ /// vkCreateAccelerationStructureKHR
+ ///
+ /// Callback function signature: bool (api::device *device, api::resource resource, api::resource_usage usage_type, api::resource_view_desc &desc)
+ ///
+ ///
+ /// To overwrite the resource view description, modify desc in the callback and return , otherwise return .
+ /// Is not called in D3D9 (since resource views are tied to resources there).
+ ///
+ CreateResourceView,
+
+ ///
+ /// Called on resource view destruction, before:
+ ///
+ /// IDirect3DResource9::Release
+ /// ID3D10View::Release
+ /// ID3D11View::Release
+ /// glDeleteTextures
+ /// vkDestroyBufferView
+ /// vkDestroyImageView
+ /// vkDestroyAccelerationStructureKHR
+ ///
+ /// Callback function signature: void (api::device *device, api::resource_view view)
+ ///
+ ///
+ /// Is not called in D3D12 (since resource views are descriptor handles instead of objects there).
+ ///
+ DestroyResourceView,
+
+ ///
+ /// Called after:
+ ///
+ /// IDirect3DVertexBuffer9::Lock
+ /// IDirect3DIndexBuffer9::Lock
+ /// ID3D10Resource::Map
+ /// ID3D11DeviceContext::Map
+ /// ID3D12Resource::Map
+ /// glMapBuffer
+ /// glMapBufferRange
+ /// glMapNamedBuffer
+ /// glMapNamedBufferRange
+ ///
+ /// Callback function signature: void (api::device *device, api::resource resource, uint64_t offset, uint64_t size, api::map_access access, void **data)
+ ///
+ MapBufferRegion,
+
+ ///
+ /// Called before:
+ ///
+ /// IDirect3DVertexBuffer9::Unlock
+ /// IDirect3DIndexBuffer9::Unlock
+ /// ID3D10Resource::Unmap
+ /// ID3D11DeviceContext::Unmap
+ /// ID3D12Resource::Unmap
+ /// glUnmapBuffer
+ /// glUnmapNamedBuffer
+ ///
+ /// Callback function signature: void (api::device *device, api::resource resource)
+ ///
+ UnmapBufferRegion,
+
+ ///
+ /// Called after:
+ ///
+ /// IDirect3DSurface9::LockRect
+ /// IDirect3DVolume9::LockBox
+ /// IDirect3DTexture9::LockRect
+ /// IDirect3DVolumeTexture9::LockBox
+ /// IDirect3DCubeTexture9::LockRect
+ /// ID3D10Resource::Map
+ /// ID3D11DeviceContext::Map
+ /// ID3D12Resource::Map
+ ///
+ /// Callback function signature: void (api::device *device, api::resource resource, uint32_t subresource, const api::subresource_box *box, api::map_access access, api::subresource_data *data)
+ ///
+ MapTextureRegion,
+
+ ///
+ /// Called before:
+ ///
+ /// IDirect3DSurface9::UnlockRect
+ /// IDirect3DVolume9::UnlockBox
+ /// IDirect3DTexture9::UnlockRect
+ /// IDirect3DVolumeTexture9::UnlockBox
+ /// IDirect3DCubeTexture9::UnlockRect
+ /// ID3D10Resource::Unmap
+ /// ID3D11DeviceContext::Unmap
+ /// ID3D12Resource::Unmap
+ ///
+ /// Callback function signature: void (api::device *device, api::resource resource, uint32_t subresource)
+ ///
+ UnmapTextureRegion,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D10Device::UpdateSubresource
+ /// ID3D11DeviceContext::UpdateSubresource
+ /// glBufferSubData
+ /// glNamedBufferSubData
+ ///
+ /// Callback function signature: bool (api::device *device, const void *data, api::resource resource, uint64_t offset, uint64_t size)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ /// Destination resource will be in the state.
+ ///
+ UpdateBufferRegion,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D10Device::UpdateSubresource
+ /// ID3D11DeviceContext::UpdateSubresource
+ /// glTexSubData1D
+ /// glTexSubData2D
+ /// glTexSubData3D
+ /// glTextureSubData1D
+ /// glTextureSubData2D
+ /// glTextureSubData3D
+ /// glCompressedTexSubData1D
+ /// glCompressedTexSubData2D
+ /// glCompressedTexSubData3D
+ /// glCompressedTextureSubData1D
+ /// glCompressedTextureSubData2D
+ /// glCompressedTextureSubData3D
+ ///
+ /// Callback function signature: bool (api::device *device, const api::subresource_data &data, api::resource resource, uint32_t subresource, const api::subresource_box *box)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ /// Destination resource will be in the state.
+ ///
+ UpdateTextureRegion,
+
+ ///
+ /// Called after successful pipeline creation from:
+ ///
+ /// IDirect3DDevice9::CreateVertexShader
+ /// IDirect3DDevice9::CreatePixelShader
+ /// IDirect3DDevice9::CreateVertexDeclaration
+ /// ID3D10Device::CreateVertexShader
+ /// ID3D10Device::CreateGeometryShader
+ /// ID3D10Device::CreateGeometryShaderWithStreamOutput
+ /// ID3D10Device::CreatePixelShader
+ /// ID3D10Device::CreateInputLayout
+ /// ID3D10Device::CreateBlendState
+ /// ID3D10Device::CreateDepthStencilState
+ /// ID3D10Device::CreateRasterizerState
+ /// ID3D10Device1::CreateBlendState1
+ /// ID3D11Device::CreateVertexShader
+ /// ID3D11Device::CreateHullShader
+ /// ID3D11Device::CreateDomainShader
+ /// ID3D11Device::CreateGeometryShader
+ /// ID3D11Device::CreateGeometryShaderWithStreamOutput
+ /// ID3D11Device::CreatePixelShader
+ /// ID3D11Device::CreateComputeShader
+ /// ID3D11Device::CreateInputLayout
+ /// ID3D11Device::CreateBlendState
+ /// ID3D11Device::CreateDepthStencilState
+ /// ID3D11Device::CreateRasterizerState
+ /// ID3D11Device1::CreateBlendState1
+ /// ID3D11Device1::CreateRasterizerState1
+ /// ID3D11Device3::CreateRasterizerState2
+ /// ID3D12Device::CreateComputePipelineState
+ /// ID3D12Device::CreateGraphicsPipelineState
+ /// ID3D12Device2::CreatePipelineState
+ /// ID3D12Device5::CreateStateObject
+ /// ID3D12Device7::AddToStateObject
+ /// ID3D12PipelineLibrary::LoadComputePipeline
+ /// ID3D12PipelineLibrary::LoadGraphicsPipeline
+ /// ID3D12PipelineLibrary1::LoadPipeline
+ /// glLinkProgram
+ /// vkCreateComputePipelines
+ /// vkCreateGraphicsPipelines
+ ///
+ /// Callback function signature: void (api::device *device, api::pipeline_layout layout, uint32_t subobject_count, const api::pipeline_subobject *subobjects, api::pipeline pipeline)
+ ///
+ ///
+ /// May be called multiple times with the same pipeline handle (whenever the pipeline is updated or its reference count is incremented).
+ ///
+ InitPipeline,
+
+ ///
+ /// Called on pipeline creation, before:
+ ///
+ /// IDirect3DDevice9::CreateVertexShader
+ /// IDirect3DDevice9::CreatePixelShader
+ /// IDirect3DDevice9::CreateVertexDeclaration
+ /// ID3D10Device::CreateVertexShader
+ /// ID3D10Device::CreateGeometryShader
+ /// ID3D10Device::CreateGeometryShaderWithStreamOutput
+ /// ID3D10Device::CreatePixelShader
+ /// ID3D10Device::CreateInputLayout
+ /// ID3D10Device::CreateBlendState
+ /// ID3D10Device::CreateDepthStencilState
+ /// ID3D10Device::CreateRasterizerState
+ /// ID3D10Device1::CreateBlendState1
+ /// ID3D11Device::CreateVertexShader
+ /// ID3D11Device::CreateHullShader
+ /// ID3D11Device::CreateDomainShader
+ /// ID3D11Device::CreateGeometryShader
+ /// ID3D11Device::CreateGeometryShaderWithStreamOutput
+ /// ID3D11Device::CreatePixelShader
+ /// ID3D11Device::CreateComputeShader
+ /// ID3D11Device::CreateInputLayout
+ /// ID3D11Device::CreateBlendState
+ /// ID3D11Device::CreateDepthStencilState
+ /// ID3D11Device::CreateRasterizerState
+ /// ID3D11Device1::CreateBlendState1
+ /// ID3D11Device1::CreateRasterizerState1
+ /// ID3D11Device3::CreateRasterizerState2
+ /// ID3D12Device::CreateComputePipelineState
+ /// ID3D12Device::CreateGraphicsPipelineState
+ /// ID3D12Device2::CreatePipelineState
+ /// ID3D12Device5::CreateStateObject
+ /// glShaderSource
+ /// vkCreateComputePipelines
+ /// vkCreateGraphicsPipelines
+ ///
+ /// Callback function signature: bool (api::device *device, api::pipeline_layout layout, uint32_t subobject_count, const api::pipeline_subobject *subobjects)
+ ///
+ ///
+ /// To overwrite the pipeline description, modify desc in the callback and return , otherwise return .
+ ///
+ CreatePipeline,
+
+ ///
+ /// Called on pipeline destruction, before:
+ ///
+ /// ID3D10VertexShader::Release
+ /// ID3D10GeometryShader::Release
+ /// ID3D10PixelShader::Release
+ /// ID3D10InputLayout::Release
+ /// ID3D10BlendState::Release
+ /// ID3D10DepthStencilState::Release
+ /// ID3D10RasterizerState::Release
+ /// ID3D11VertexShader::Release
+ /// ID3D11HullShader::Release
+ /// ID3D11DomainShader::Release
+ /// ID3D11GeometryShader::Release
+ /// ID3D11PixelShader::Release
+ /// ID3D11ComputeShader::Release
+ /// ID3D11InputLayout::Release
+ /// ID3D11BlendState::Release
+ /// ID3D11DepthStencilState::Release
+ /// ID3D11RasterizerState::Release
+ /// ID3D12PipelineState::Release
+ /// ID3D12StateObject::Release
+ /// glDeleteProgram
+ /// vkDestroyPipeline
+ ///
+ /// Callback function signature: void (api::device *device, api::pipeline pipeline)
+ ///
+ ///
+ /// Is not called in D3D9.
+ ///
+ DestroyPipeline,
+
+ ///
+ /// Called after successful pipeline layout creation from:
+ ///
+ /// ID3D12Device::CreateRootSignature
+ /// vkCreatePipelineLayout
+ ///
+ /// Callback function signature: void (api::device *device, uint32_t param_count, const api::pipeline_layout_param *params, api::pipeline_layout layout)
+ ///
+ ///
+ /// In case of D3D9, D3D10, D3D11 and OpenGL this is called during device initialization as well and behaves as if an implicit global pipeline layout was created.
+ ///
+ InitPipelineLayout,
+
+ ///
+ /// Called on pipeline layout creation, before:
+ ///
+ /// ID3D12Device::CreateRootSignature
+ /// vkCreatePipelineLayout
+ ///
+ /// Callback function signature: bool (api::device *device, uint32_t ¶m_count, api::pipeline_layout_param *¶ms)
+ ///
+ ///
+ /// Is not called in D3D9, D3D10, D3D11 or OpenGL.
+ ///
+ CreatePipelineLayout,
+
+ ///
+ /// Called on pipeline layout destruction, before:
+ ///
+ /// ID3D12RootSignature::Release
+ /// VkDestroyPipelineLayout
+ ///
+ /// Callback function signature: void (api::device *device, api::pipeline_layout layout)
+ ///
+ DestroyPipelineLayout,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D12Device::CopyDescriptors
+ /// ID3D12Device::CopyDescriptorsSimple
+ /// vkUpdateDescriptorSets
+ ///
+ /// Callback function signature: bool (api::device *device, uint32_t count, const api::descriptor_table_copy *copies)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ ///
+ CopyDescriptorTables,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D12Device::CreateConstantBufferView
+ /// ID3D12Device::CreateShaderResourceView
+ /// ID3D12Device::CreateUnorderedAccessView
+ /// ID3D12Device::CreateSampler
+ /// vkUpdateDescriptorSets
+ ///
+ /// Callback function signature: bool (api::device *device, uint32_t count, const api::descriptor_table_update *updates)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ ///
+ UpdateDescriptorTables,
+
+ ///
+ /// Called after successful query heap creation from:
+ ///
+ /// ID3D12Device::CreateQueryHeap
+ /// vkCreateQueryPool
+ ///
+ /// Callback function signature: void (api::device *device, api::query_type type, uint32_t size, api::query_heap heap)
+ ///
+ InitQueryHeap,
+
+ ///
+ /// Called on query heap creation, before:
+ ///
+ /// ID3D12Device::CreateQueryHeap
+ /// vkCreateQueryPool
+ ///
+ /// Callback function signature: bool (api::device *device, api::query_type type, uint32_t &size)
+ ///
+ CreateQueryHeap,
+
+ ///
+ /// Called on query heap destruction, before:
+ ///
+ /// ID3D12QueryHeap::Release
+ /// vkDestroyQueryPool
+ ///
+ /// Callback function signature: void (api::device *device, api::query_heap heap)
+ ///
+ DestroyQueryHeap,
+
+ ///
+ /// Called before:
+ ///
+ /// vkGetQueryPoolResults
+ ///
+ /// Callback function signature: bool (api::device *device, api::query_heap heap, uint32_t first, uint32_t count, void *results, uint32_t stride)
+ ///
+ GetQueryHeapResults,
+
+ ///
+ /// Called after:
+ ///
+ /// ID3D12GraphicsCommandList::ResourceBarrier
+ /// ID3D12GraphicsCommandList7::Barrier
+ /// vkCmdPipelineBarrier
+ /// vkCmdPipelineBarrier2
+ ///
+ /// Callback function signature: void (api::command_list *cmd_list, uint32_t count, const api::resource *resources, const api::resource_usage *old_states, const api::resource_usage *new_states)
+ ///
+ Barrier,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D12GraphicsCommandList4::BeginRenderPass
+ /// vkCmdBeginRenderPass
+ /// vkCmdBeginRenderPass2
+ /// vkCmdNextSubpass
+ /// vkCmdNextSubpass2
+ /// vkCmdBeginRendering
+ ///
+ /// Callback function signature: void (api::command_list *cmd_list, uint32_t count, const api::render_pass_render_target_desc *rts, const api::render_pass_depth_stencil_desc *ds)
+ ///
+ ///
+ /// The depth-stencil description argument is optional and may be (which indicates that no depth-stencil is used).
+ ///
+ BeginRenderPass,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D12GraphicsCommandList4::EndRenderPass
+ /// vkCmdEndRenderPass
+ /// vkCmdEndRenderPass2
+ /// vkCmdNextSubpass
+ /// vkCmdNextSubpass2
+ /// vkCmdEndRendering
+ ///
+ /// Callback function signature: void (api::command_list *cmd_list)
+ ///
+ EndRenderPass,
+
+ ///
+ /// Called after:
+ ///
+ /// IDirect3DDevice9::SetRenderTarget
+ /// IDirect3DDevice9::SetDepthStencilSurface
+ /// ID3D10Device::OMSetRenderTargets
+ /// ID3D11DeviceContext::OMSetRenderTargets
+ /// ID3D11DeviceContext::OMSetRenderTargetsAndUnorderedAccessViews
+ /// ID3D12GraphicsCommandList::OMSetRenderTargets
+ /// glBindFramebuffer
+ ///
+ /// Callback function signature: void (api::command_list *cmd_list, uint32_t count, const api::resource_view *rtvs, api::resource_view dsv)
+ ///
+ BindRenderTargetsAndDepthStencil,
+
+ ///
+ /// Called after:
+ ///
+ /// IDirect3DDevice9::SetVertexShader
+ /// IDirect3DDevice9::SetPixelShader
+ /// IDirect3DDevice9::SetVertexDeclaration
+ /// IDirect3DDevice9::ProcessVertices
+ /// ID3D10Device::VSSetShader
+ /// ID3D10Device::GSSetShader
+ /// ID3D10Device::PSSetShader
+ /// ID3D10Device::IASetInputLayout
+ /// ID3D10Device::OMSetBlendState
+ /// ID3D10Device::OMSetDepthStencilState
+ /// ID3D10Device::RSSetState
+ /// ID3D11DeviceContext::VSSetShader
+ /// ID3D11DeviceContext::HSSetShader
+ /// ID3D11DeviceContext::DSSetShader
+ /// ID3D11DeviceContext::GSSetShader
+ /// ID3D11DeviceContext::PSSetShader
+ /// ID3D11DeviceContext::CSSetShader
+ /// ID3D11DeviceContext::IASetInputLayout
+ /// ID3D11DeviceContext::OMSetBlendState
+ /// ID3D11DeviceContext::OMSetDepthStencilState
+ /// ID3D11DeviceContext::RSSetState
+ /// ID3D12GraphicsCommandList::Reset
+ /// ID3D12GraphicsCommandList::SetPipelineState
+ /// ID3D12GraphicsCommandList4::SetPipelineState1
+ /// glUseProgram
+ /// glBindVertexArray
+ /// vkCmdBindPipeline
+ ///
+ /// Callback function signature: void (api::command_list *cmd_list, api::pipeline_stage stages, api::pipeline pipeline)
+ ///
+ BindPipeline,
+
+ ///
+ /// Called after:
+ ///
+ /// IDirect3DDevice9::SetRenderState
+ /// ID3D10Device::IASetPrimitiveTopology
+ /// ID3D10Device::OMSetBlendState
+ /// ID3D10Device::OMSetDepthStencilState
+ /// ID3D11DeviceContext::IASetPrimitiveTopology
+ /// ID3D11DeviceContext::OMSetBlendState
+ /// ID3D11DeviceContext::OMSetDepthStencilState
+ /// ID3D12GraphicsCommandList::IASetPrimitiveTopology
+ /// ID3D12GraphicsCommandList::OMSetBlendFactor
+ /// ID3D12GraphicsCommandList::OMSetStencilRef
+ /// gl(...)
+ /// vkCmdSetDepthBias
+ /// vkCmdSetBlendConstants
+ /// vkCmdSetStencilCompareMask
+ /// vkCmdSetStencilWriteMask
+ /// vkCmdSetStencilReference
+ ///
+ /// Callback function signature: void (api::command_list *cmd_list, uint32_t count, const api::dynamic_state *states, const uint32_t *values)
+ ///
+ BindPipelineStates,
+
+ ///
+ /// Called after:
+ ///
+ /// IDirect3DDevice9::SetViewport
+ /// IDirect3DDevice9::SetRenderTarget (implicitly updates the viewport)
+ /// ID3D10Device::RSSetViewports
+ /// ID3D11DeviceContext::RSSetViewports
+ /// ID3D12GraphicsCommandList::RSSetViewports
+ /// glViewport
+ /// glViewportArrayv
+ /// glViewportIndexedf
+ /// glViewportIndexedfv
+ /// vkCmdSetViewport
+ ///
+ /// Callback function signature: void (api::command_list *cmd_list, uint32_t first, uint32_t count, const api::viewport *viewports)
+ ///
+ BindViewports,
+
+ ///
+ /// Called after:
+ ///
+ /// IDirect3DDevice9::SetScissorRect
+ /// ID3D10Device::RSSetScissorRects
+ /// ID3D11DeviceContext::RSSetScissorRects
+ /// ID3D12GraphicsCommandList::RSSetScissorRects
+ /// glScissor
+ /// glScissorArrayv
+ /// glScissorIndexed
+ /// glScissorIndexedv
+ /// vkCmdSetScissor
+ ///
+ /// Callback function signature: void (api::command_list *cmd_list, uint32_t first, uint32_t count, const api::rect *rects)
+ ///
+ BindScissorRects,
+
+ ///
+ /// Called after:
+ ///
+ /// IDirect3DDevice9::SetVertexShaderConstantF
+ /// IDirect3DDevice9::SetPixelShaderConstantF
+ /// ID3D12GraphicsCommandList::SetComputeRoot32BitConstant
+ /// ID3D12GraphicsCommandList::SetComputeRoot32BitConstants
+ /// ID3D12GraphicsCommandList::SetGraphicsRoot32BitConstant
+ /// ID3D12GraphicsCommandList::SetGraphicsRoot32BitConstants
+ /// glUniform(...)
+ /// vkCmdPushConstants
+ ///
+ /// Callback function signature: void (api::command_list *cmd_list, api::shader_stage stages, api::pipeline_layout layout, uint32_t layout_param, uint32_t first, uint32_t count, const void *values)
+ ///
+ PushConstants,
+
+ ///
+ /// Called after:
+ ///
+ /// IDirect3DDevice9::SetTexture
+ /// ID3D10Device::VSSetSamplers
+ /// ID3D10Device::VSSetShaderResources
+ /// ID3D10Device::VSSetConstantBuffers
+ /// ID3D10Device::GSSetSamplers
+ /// ID3D10Device::GSSetShaderResources
+ /// ID3D10Device::GSSetConstantBuffers
+ /// ID3D10Device::PSSetSamplers
+ /// ID3D10Device::PSSetShaderResources
+ /// ID3D10Device::PSSetConstantBuffers
+ /// ID3D11DeviceContext::VSSetSamplers
+ /// ID3D11DeviceContext::VSSetShaderResources
+ /// ID3D11DeviceContext::VSSetConstantBuffers
+ /// ID3D11DeviceContext::HSSetSamplers
+ /// ID3D11DeviceContext::HSSetShaderResources
+ /// ID3D11DeviceContext::HSSetConstantBuffers
+ /// ID3D11DeviceContext::DSSetSamplers
+ /// ID3D11DeviceContext::DSSetShaderResources
+ /// ID3D11DeviceContext::DSSetConstantBuffers
+ /// ID3D11DeviceContext::GSSetSamplers
+ /// ID3D11DeviceContext::GSSetShaderResources
+ /// ID3D11DeviceContext::GSSetConstantBuffers
+ /// ID3D11DeviceContext::PSSetSamplers
+ /// ID3D11DeviceContext::PSSetShaderResources
+ /// ID3D11DeviceContext::PSSetConstantBuffers
+ /// ID3D11DeviceContext::CSSetSamplers
+ /// ID3D11DeviceContext::CSSetShaderResources
+ /// ID3D11DeviceContext::CSSetUnorderedAccessViews
+ /// ID3D11DeviceContext::CSSetConstantBuffers
+ /// ID3D12GraphicsCommandList::SetComputeRootConstantBufferView
+ /// ID3D12GraphicsCommandList::SetGraphicsRootConstantBufferView
+ /// ID3D12GraphicsCommandList::SetComputeRootShaderResourceView
+ /// ID3D12GraphicsCommandList::SetGraphicsRootShaderResourceView
+ /// ID3D12GraphicsCommandList::SetComputeRootUnorderedAccessView
+ /// ID3D12GraphicsCommandList::SetGraphicsRootUnorderedAccessView
+ /// glBindBufferBase
+ /// glBindBufferRange
+ /// glBindBuffersBase
+ /// glBindBuffersRange
+ /// glBindTexture
+ /// glBindImageTexture
+ /// glBindTextures
+ /// glBindImageTextures
+ /// glBindTextureUnit
+ /// glBindMultiTextureEXT
+ /// vkCmdPushDescriptorSetKHR
+ ///
+ /// Callback function signature: void (api::command_list *cmd_list, api::shader_stage stages, api::pipeline_layout layout, uint32_t layout_param, const api::descriptor_table_update &update)
+ ///
+ PushDescriptors,
+
+ ///
+ /// Called after:
+ ///
+ /// ID3D12GraphicsCommandList::SetComputeRootSignature
+ /// ID3D12GraphicsCommandList::SetGraphicsRootSignature
+ /// ID3D12GraphicsCommandList::SetComputeRootDescriptorTable
+ /// ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable
+ /// vkCmdBindDescriptorSets
+ ///
+ /// Callback function signature: void (api::command_list *cmd_list, api::shader_stage stages, api::pipeline_layout layout, uint32_t first, uint32_t count, const api::descriptor_table *tables)
+ ///
+ BindDescriptorTables,
+
+ ///
+ /// Called after:
+ ///
+ /// IDirect3DDevice9::SetIndices
+ /// ID3D10Device::IASetIndexBuffer
+ /// ID3D11DeviceContext::IASetIndexBuffer
+ /// ID3D12GraphicsCommandList::IASetIndexBuffer
+ /// glBindBuffer
+ /// vkCmdBindIndexBuffer
+ ///
+ /// Callback function signature: void (api::command_list *cmd_list, api::resource buffer, uint64_t offset, uint32_t index_size)
+ ///
+ BindIndexBuffer,
+
+ ///
+ /// Called after:
+ ///
+ /// IDirect3DDevice9::SetStreamSource
+ /// ID3D10Device::IASetVertexBuffers
+ /// ID3D11DeviceContext::IASetVertexBuffers
+ /// ID3D12GraphicsCommandList::IASetVertexBuffers
+ /// glBindBuffer
+ /// glBindVertexBuffer
+ /// glBindVertexBuffers
+ /// vkCmdBindVertexBuffers
+ /// vkCmdBindVertexBuffers2
+ ///
+ /// Callback function signature: void (api::command_list *cmd_list, uint32_t first, uint32_t count, const api::resource *buffers, const uint64_t *offsets, const uint32_t *strides)
+ ///
+ ///
+ /// The strides argument is optional and may be .
+ ///
+ BindVertexBuffers,
+
+ ///
+ /// Called after:
+ ///
+ /// IDirect3DDevice9::ProcessVertices
+ /// ID3D10Device::SOSetTargets
+ /// ID3D11DeviceContext::SOSetTargets
+ /// ID3D12GraphicsCommandList::SOSetTargets
+ /// glBindBufferBase
+ /// glBindBufferRange
+ /// glBindBuffersBase
+ /// glBindBuffersRange
+ /// vkCmdBindTransformFeedbackBuffersEXT
+ ///
+ /// Callback function signature: void (api::command_list *cmd_list, uint32_t first, uint32_t count, const api::resource *buffers, const uint64_t *offsets, const uint64_t *max_sizes, const api::resource *counter_buffers, const uint64_t *counter_offsets)
+ ///
+ ///
+ /// The counter arguments are optional and may be .
+ ///
+ BindStreamOutputBuffers,
+
+ ///
+ /// Called before:
+ ///
+ /// IDirect3DDevice9::DrawPrimitive
+ /// IDirect3DDevice9::DrawPrimitiveUP
+ /// IDirect3DDevice9::ProcessVertices
+ /// ID3D10Device::Draw
+ /// ID3D10Device::DrawInstanced
+ /// ID3D11DeviceContext::Draw
+ /// ID3D11DeviceContext::DrawInstanced
+ /// ID3D12GraphicsCommandList::DrawInstanced
+ /// glDrawArrays
+ /// glDrawArraysInstanced
+ /// glDrawArraysInstancedBaseInstance
+ /// glMultiDrawArrays
+ /// vkCmdDraw
+ ///
+ /// Callback function signature: bool (api::command_list *cmd_list, uint32_t vertex_count, uint32_t instance_count, uint32_t first_vertex, uint32_t first_instance)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ ///
+ Draw,
+
+ ///
+ /// Called before:
+ ///
+ /// IDirect3DDevice9::DrawIndexedPrimitive
+ /// IDirect3DDevice9::DrawIndexedPrimitiveUP
+ /// ID3D10Device::DrawIndexed
+ /// ID3D10Device::DrawIndexedInstanced
+ /// ID3D11DeviceContext::DrawIndexed
+ /// ID3D11DeviceContext::DrawIndexedInstanced
+ /// ID3D12GraphicsCommandList::DrawIndexedInstanced
+ /// glDrawElements
+ /// glDrawElementsBaseVertex
+ /// glDrawElementsInstanced
+ /// glDrawElementsInstancedBaseVertex
+ /// glDrawElementsInstancedBaseInstance
+ /// glDrawElementsInstancedBaseVertexBaseInstance
+ /// glMultiDrawElements
+ /// glMultiDrawElementsBaseVertex
+ /// vkCmdDrawIndexed
+ ///
+ /// Callback function signature: bool (api::command_list *cmd_list, uint32_t index_count, uint32_t instance_count, uint32_t first_index, int32_t vertex_offset, uint32_t first_instance)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ ///
+ DrawIndexed,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D11DeviceContext::Dispatch
+ /// ID3D12GraphicsCommandList::Dispatch
+ /// glDispatchCompute
+ /// vkCmdDispatch
+ ///
+ /// Callback function signature: bool (api::command_list *cmd_list, uint32_t group_count_x, uint32_t group_count_y, uint32_t group_count_z)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ ///
+ Dispatch = 54,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D12GraphicsCommandList::DispatchMesh
+ /// vkCmdDrawMeshTasksEXT
+ ///
+ /// Callback function signature: bool (api::command_list *cmd_list, uint32_t group_count_x, uint32_t group_count_y, uint32_t group_count_z)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ ///
+ DispatchMesh = 89,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D12GraphicsCommandList::DispatchRays
+ /// vkCmdTraceRaysKHR
+ ///
+ /// Callback function signature: bool (api::command_list *cmd_list, api::resource raygen, uint64_t raygen_offset, uint64_t raygen_size, api::resource miss, uint64_t miss_offset, uint64_t miss_size, uint64_t miss_stride, api::resource hit_group, uint64_t hit_group_offset, uint64_t hit_group_size, uint64_t hit_group_stride, api::resource callable, uint64_t callable_offset, uint64_t callable_size, uint64_t callable_stride, uint32_t width, uint32_t height, uint32_t depth)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ /// In case of D3D12 and Vulkan, the shader handle buffer handles may be zero with the buffers instead referred to via a device address passed in the related offset argument.
+ ///
+ DispatchRays = 90,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D11DeviceContext::DrawInstancedIndirect
+ /// ID3D11DeviceContext::DrawIndexedInstancedIndirect
+ /// ID3D11DeviceContext::DispatchIndirect
+ /// ID3D12GraphicsCommandList::ExecuteIndirect
+ /// glDrawArraysIndirect
+ /// glDrawElementsIndirect
+ /// glMultiDrawArraysIndirect
+ /// glMultiDrawElementsIndirect
+ /// glDispatchComputeIndirect
+ /// vkCmdDrawIndirect
+ /// vkCmdDrawIndexedIndirect
+ /// vkCmdDispatchIndirect
+ /// vkCmdTraceRaysIndirect2KHR
+ /// vkCmdDrawMeshTasksIndirectEXT
+ /// vkCmdDrawMeshTasksIndirectCountEXT
+ ///
+ /// Callback function signature: bool (api::command_list *cmd_list, api::indirect_command type, api::resource buffer, uint64_t offset, uint32_t draw_count, uint32_t stride)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ ///
+ DrawOrDispatchIndirect = 55,
+
+ ///
+ /// Called before:
+ ///
+ /// IDirect3DDevice9::UpdateTexture
+ /// IDirect3DDevice9::GetRenderTargetData
+ /// ID3D10Device::CopyResource
+ /// ID3D11DeviceContext::CopyResource
+ /// ID3D12GraphicsCommandList::CopyResource
+ ///
+ /// Callback function signature: bool (api::command_list *cmd_list, api::resource source, api::resource dest)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ /// Source resource will be in the state.
+ /// Destination resource will be in the state.
+ ///
+ CopyResource,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D12GraphicsCommandList::CopyBufferRegion
+ /// glCopyBufferSubData
+ /// glCopyNamedBufferSubData
+ /// vkCmdCopyBuffer
+ /// vkCmdCopyBuffer2
+ ///
+ /// Callback function signature: bool (api::command_list *cmd_list, api::resource source, uint64_t source_offset, api::resource dest, uint64_t dest_offset, uint64_t size)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ /// Source resource will be in the state.
+ /// Destination resource will be in the state.
+ ///
+ CopyBufferRegion,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D12GraphicsCommandList::CopyTextureRegion
+ /// vkCmdCopyBufferToImage
+ /// vkCmdCopyBufferToImage2
+ ///
+ /// Callback function signature: bool (api::command_list *cmd_list, api::resource source, uint64_t source_offset, uint32_t row_length, uint32_t slice_height, api::resource dest, uint32_t dest_subresource, const api::subresource_box *dest_box)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ /// Source resource will be in the state.
+ /// Destination resource will be in the state.
+ /// The subresource box argument is optional and may be (which indicates the entire subresource is referenced).
+ ///
+ CopyBufferToTexture,
+
+ ///
+ /// Called before:
+ ///
+ /// IDirect3DDevice9::UpdateSurface
+ /// IDirect3DDevice9::StretchRect
+ /// ID3D10Device::CopySubresourceRegion
+ /// ID3D11DeviceContext::CopySubresourceRegion
+ /// ID3D12GraphicsCommandList::CopyTextureRegion
+ /// glBlitFramebuffer
+ /// glBlitNamedFramebuffer
+ /// glCopyImageSubData
+ /// glCopyTexSubImage1D
+ /// glCopyTexSubImage2D
+ /// glCopyTexSubImage3D
+ /// glCopyTextureSubImage1D
+ /// glCopyTextureSubImage2D
+ /// glCopyTextureSubImage3D
+ /// vkCmdBlitImage
+ /// vkCmdBlitImage2
+ /// vkCmdCopyImage
+ /// vkCmdCopyImage2
+ ///
+ /// Callback function signature: bool (api::command_list *cmd_list, api::resource source, uint32_t source_subresource, const api::subresource_box *source_box, api::resource dest, uint32_t dest_subresource, const api::subresource_box *dest_box, api::filter_mode filter)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ /// Source resource will be in the state.
+ /// Destination resource will be in the state.
+ /// The subresource box arguments are optional and may be (which indicates the entire subresource is used).
+ ///
+ CopyTextureRegion,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D12GraphicsCommandList::CopyTextureRegion
+ /// vkCmdCopyImageToBuffer
+ /// vkCmdCopyImageToBuffer2
+ ///
+ /// Callback function signature: bool (api::command_list *cmd_list, api::resource source, uint32_t source_subresource, const api::subresource_box *source_box, api::resource dest, uint64_t dest_offset, uint32_t row_length, uint32_t slice_height)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ /// Source resource will be in the state.
+ /// Destination resource will be in the state.
+ /// The subresource box argument is optional and may be (which indicates the entire subresource is used).
+ ///
+ CopyTextureToBuffer,
+
+ ///
+ /// Called before:
+ ///
+ /// IDirect3DDevice9::StretchRect
+ /// ID3D10Device::ResolveSubresource
+ /// ID3D11DeviceContext::ResolveSubresource
+ /// ID3D12GraphicsCommandList::ResolveSubresource
+ /// ID3D12GraphicsCommandList1::ResolveSubresourceRegion
+ /// glBlitFramebuffer
+ /// glBlitNamedFramebuffer
+ /// vkCmdResolveImage
+ /// vkCmdResolveImage2
+ ///
+ /// Callback function signature: bool (api::command_list *cmd_list, api::resource source, uint32_t source_subresource, const api::subresource_box *source_box, api::resource dest, uint32_t dest_subresource, int32_t dest_x, int32_t dest_y, int32_t dest_z, api::format format)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ /// Source resource will be in the state.
+ /// Destination resource will be in the state.
+ /// The subresource box argument is optional and may be (which indicates the entire subresource is used).
+ ///
+ ResolveTextureRegion,
+
+ ///
+ /// Called before:
+ ///
+ /// IDirect3DDevice9::Clear
+ /// ID3D10Device::ClearDepthStencilView
+ /// ID3D11DeviceContext::ClearDepthStencilView
+ /// ID3D11DeviceContext1::ClearView (for depth-stencil views)
+ /// ID3D12GraphicsCommandList::ClearDepthStencilView
+ /// glClear
+ /// glClearBufferfi
+ /// glClearBufferfv
+ /// glClearNamedFramebufferfi
+ /// glClearNamedFramebufferfv
+ /// vkCmdClearDepthStencilImage
+ /// vkCmdClearAttachments
+ ///
+ /// Callback function signature: bool (api::command_list *cmd_list, api::resource_view dsv, const float *depth, const uint8_t *stencil, uint32_t rect_count, const api::rect *rects)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ /// Resource will be in the state.
+ /// One of the depth or stencil clear value arguments may be when the respective component is not cleared.
+ ///
+ ClearDepthStencilView,
+
+ ///
+ /// Called before:
+ ///
+ /// IDirect3DDevice9::Clear
+ /// IDirect3DDevice9::ColorFill
+ /// ID3D10Device::ClearRenderTargetView
+ /// ID3D11DeviceContext::ClearRenderTargetView
+ /// ID3D11DeviceContext1::ClearView (for render target views)
+ /// ID3D12GraphicsCommandList::ClearRenderTargetView
+ /// glClear
+ /// glClearBufferfv
+ /// glClearNamedFramebufferfv
+ /// vkCmdClearColorImage
+ /// vkCmdClearAttachments
+ ///
+ /// Callback function signature: bool (api::command_list *cmd_list, api::resource_view rtv, const float color[4], uint32_t rect_count, const api::rect *rects)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ /// Resources will be in the state.
+ ///
+ ClearRenderTargetView,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D11DeviceContext::ClearUnorderedAccessViewUint
+ /// ID3D12GraphicsCommandList::ClearUnorderedAccessViewUint
+ ///
+ /// Callback function signature: bool (api::command_list *cmd_list, api::resource_view uav, const uint32_t values[4], uint32_t rect_count, const api::rect *rects)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ /// Resource will be in the state.
+ ///
+ ClearUnorderedAccessViewUint,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D11DeviceContext::ClearUnorderedAccessViewFloat
+ /// ID3D11DeviceContext1::ClearView (for unordered access views)
+ /// ID3D12GraphicsCommandList::ClearUnorderedAccessViewFloat
+ ///
+ /// Callback function signature: bool (api::command_list *cmd_list, api::resource_view uav, const float values[4], uint32_t rect_count, const api::rect *rects)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ /// Resource will be in the state.
+ ///
+ ClearUnorderedAccessViewFloat,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D10Device::GenerateMips
+ /// ID3D11DeviceContext::GenerateMips
+ /// glGenerateMipmap
+ /// glGenerateTextureMipmap
+ ///
+ /// Callback function signature: bool (api::command_list *cmd_list, api::resource_view srv)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ ///
+ GenerateMipmaps,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D12GraphicsCommandList::BeginQuery
+ /// vkCmdBeginQuery
+ /// vkCmdBeginQueryIndexedEXT
+ ///
+ /// Callback function signature: bool (api::command_list *cmd_list, api::query_heap heap, api::query_type type, uint32_t index)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ ///
+ BeginQuery,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D12GraphicsCommandList::EndQuery
+ /// vkCmdEndQuery
+ /// vkCmdEndQueryIndexedEXT
+ /// vkCmdWriteTimestamp
+ /// vkCmdWriteTimestamp2
+ ///
+ /// Callback function signature: bool (api::command_list *cmd_list, api::query_heap heap, api::query_type type, uint32_t index)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ ///
+ EndQuery,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D12GraphicsCommandList::ResolveQueryData
+ /// vkCmdCopyQueryPoolResults
+ ///
+ /// Callback function signature: bool (api::command_list *cmd_list, api::query_heap heap, api::query_type type, uint32_t first, uint32_t count, api::resource dest, uint64_t dest_offset, uint32_t stride)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ ///
+ CopyQueryHeapResults = 69,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D12GraphicsCommandList4::CopyRaytracingAccelerationStructure
+ /// vkCmdCopyAccelerationStructureKHR
+ ///
+ /// Callback function signature: bool (api::command_list *cmd_list, api::resource_view source, api::resource_view dest, api::acceleration_structure_copy_mode mode)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ ///
+ CopyAccelerationStructure = 91,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D12GraphicsCommandList4::BuildRaytracingAccelerationStructure
+ /// vkCmdBuildAccelerationStructuresKHR
+ ///
+ /// Callback function signature: bool (api::command_list *cmd_list, api::acceleration_structure_type type, api::acceleration_structure_build_flags flags, uint32_t input_count, const api::acceleration_structure_build_input *inputs, api::resource scratch, uint64_t scratch_offset, api::resource_view source, api::resource_view dest, api::acceleration_structure_build_mode mode)
+ ///
+ ///
+ /// To prevent this command from being executed, return , otherwise return .
+ /// In case of D3D12 and Vulkan, the scratch buffer handle may be zero with the buffer instead referred to via a device address passed in the related offset argument.
+ /// Scratch buffer will be in the resource state.
+ ///
+ BuildAccelerationStructure = 92,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D12GraphicsCommandList::Reset
+ /// vkBeginCommandBuffer
+ ///
+ /// Callback function signature: void (api::command_list *cmd_list)
+ ///
+ ///
+ /// Is not called for immediate command lists (since they cannot be reset).
+ ///
+ ResetCommandList = 70,
+
+ ///
+ /// Called before:
+ ///
+ /// ID3D11DeviceContext::FinishCommandList
+ /// ID3D12GraphicsCommandList::Close
+ /// vkEndCommandBuffer
+ ///
+ /// Callback function signature: void (api::command_list *cmd_list)
+ ///
+ ///
+ /// Is not called for immediate command lists (since they cannot be closed).
+ ///
+ CloseCommandList,
+
+ ///
+ /// Called when a command list is submitted to a command queue (or an immediate command list is flushed), before:
+ ///
+ /// IDirect3DDevice9::EndScene
+ /// ID3D10Device::Flush
+ /// ID3D11DeviceContext::Flush
+ /// ID3D11DeviceContext3::Flush1
+ /// ID3D12CommandQueue::ExecuteCommandLists
+ /// glFlush
+ /// vkQueueSubmit
+ ///
+ /// Callback function signature: void (api::command_queue *queue, api::command_list *cmd_list)
+ ///
+ ExecuteCommandList,
+
+ ///
+ /// Called when a secondary command list is executed on a primary command list, before:
+ ///
+ /// ID3D11DeviceContext::ExecuteCommandList
+ /// ID3D12GraphicsCommandList::ExecuteBundle
+ /// vkCmdExecuteCommands
+ ///
+ /// In addition, called after:
+ ///
+ /// ID3D11DeviceContext::FinishCommandList
+ ///
+ /// Callback function signature: void (api::command_list *cmd_list, api::command_list *secondary_cmd_list)
+ ///
+ ExecuteSecondaryCommandList,
+
+ ///
+ /// Called before:
+ ///
+ /// IDirect3DDevice9::Present
+ /// IDirect3DDevice9Ex::PresentEx
+ /// IDirect3DSwapChain9::Present
+ /// IDXGISwapChain::Present
+ /// IDXGISwapChain3::Present1
+ /// ID3D12CommandQueueDownlevel::Present
+ /// wglSwapBuffers
+ /// vkQueuePresentKHR
+ /// IVRCompositor::Submit
+ /// xrEndFrame
+ ///
+ /// Callback function signature: void (api::command_queue *queue, api::swapchain *swapchain, const api::rect *source_rect, const api::rect *dest_rect, uint32_t dirty_rect_count, const api::rect *dirty_rects)
+ ///
+ ///
+ /// The source and destination rectangle arguments are optional and may be (which indicates the swap chain is presented in its entirety).
+ ///
+ Present,
+
+ ///
+ /// Called before:
+ ///
+ /// IDXGISwapChain::SetFullscreenState
+ /// vkAcquireFullScreenExclusiveModeEXT
+ /// vkReleaseFullScreenExclusiveModeEXT
+ ///
+ /// Callback function signature: bool (api::swapchain *swapchain, bool fullscreen, void *hmonitor)
+ ///
+ ///
+ /// To prevent the fullscreen state from being changed, return , otherwise return .
+ ///
+ SetFullscreenState = 93,
+
+ ///
+ /// Called after ReShade has rendered its overlay.
+ /// Callback function signature: void (api::effect_runtime *runtime)
+ ///
+ ReShadePresent = 75,
+
+ ///
+ /// Called right before ReShade effects are rendered.
+ /// Callback function signature: void (api::effect_runtime *runtime, api::command_list *cmd_list, api::resource_view rtv, api::resource_view rtv_srgb)
+ ///
+ ReShadeBeginEffects,
+
+ ///
+ /// Called right after ReShade effects were rendered.
+ /// Callback function signature: void (api::effect_runtime *runtime, api::command_list *cmd_list, api::resource_view rtv, api::resource_view rtv_srgb)
+ ///
+ ReShadeFinishEffects,
+
+ ///
+ /// Called right after all ReShade effects were reloaded.
+ /// This occurs during effect runtime initialization or because the user pressed the "Reload" button in the overlay.
+ /// Any , and handles are invalidated when this event occurs and need to be queried again.
+ /// Callback function signature: void (api::effect_runtime *runtime)
+ ///
+ ReShadeReloadedEffects,
+
+ ///
+ /// Called before a uniform variable is changed, with the new value.
+ /// Callback function signature: bool (api::effect_runtime *runtime, api::effect_uniform_variable variable, const void *new_value, size_t new_value_size)
+ ///
+ ///
+ /// To prevent the variable value from being changed, return , otherwise return .
+ /// The new value has the data type reported by . The new value size is in bytes.
+ ///
+ ReShadeSetUniformValue,
+
+ ///
+ /// Called before a technique is enabled or disabled, with the new state.
+ /// Callback function signature: bool (api::effect_runtime *runtime, api::effect_technique technique, bool enabled)
+ ///
+ ///
+ /// To prevent the technique state from being changed, return , otherwise return .
+ ///
+ ReShadeSetTechniqueState,
+
+ ///
+ /// Called between the ImGui::NewFrame and ImGui::EndFrame calls for the ReShade overlay.
+ /// Can be used to perform custom Dear ImGui calls, but it is recommended to instead use to register a dedicated overlay.
+ /// Callback function signature: void (api::effect_runtime *runtime)
+ ///
+ ///
+ /// This is not called for effect runtimes in VR.
+ ///
+ ReShadeOverlay,
+
+ ///
+ /// Called after a screenshot was taken and saved to disk, with the path to the saved image file.
+ /// Callback function signature: void (api::effect_runtime *runtime, const char *path)
+ ///
+ ReShadeScreenshot,
+
+ ///
+ /// Called for each technique after it was rendered, usually between and .
+ /// Callback function signature: void (api::effect_runtime *runtime, api::effect_technique technique, api::command_list *cmd_list, api::resource_view rtv, api::resource_view rtv_srgb)
+ ///
+ ReShadeRenderTechnique,
+
+ ///
+ /// Called when all effects are about to be enabled or disabled.
+ /// Callback function signature: bool (api::effect_runtime *runtime, bool enabled)
+ ///
+ ///
+ /// To prevent the effects state from being changed, return , otherwise return .
+ ///
+ ReShadeSetEffectsState = 94,
+
+ ///
+ /// Called after a preset was loaded and applied.
+ /// This occurs after effect reloading or when the user chooses a new preset in the overlay.
+ /// Callback function signature: void (api::effect_runtime *runtime, const char *path)
+ ///
+ ReShadeSetCurrentPresetPath = 84,
+
+ ///
+ /// Called when the rendering order of loaded techniques is changed, with a handle array specifying the new order.
+ /// Callback function signature: bool (api::effect_runtime *runtime, size_t count, api::effect_technique *techniques)
+ ///
+ ///
+ /// To prevent the order from being changed, return , otherwise return .
+ ///
+ ReShadeReorderTechniques,
+
+ ///
+ /// Called when the ReShade overlay is about to be opened or closed.
+ /// Callback function signature: bool (api::effect_runtime *runtime, bool open, api::input_source source)
+ ///
+ ///
+ /// To prevent the overlay state from being changed, return , otherwise return .
+ ///
+ ReShadeOpenOverlay,
+
+ ///
+ /// Called when a uniform variable widget is added to the variable list in the overlay.
+ /// Can be used to replace with custom one or add widgets for specific uniform variables.
+ /// Callback function signature: bool (api::effect_runtime *runtime, api::effect_uniform_variable variable)
+ ///
+ ///
+ /// To prevent the normal widget from being added to the overlay, return , otherwise return .
+ ///
+ ReShadeOverlayUniformVariable,
+
+ ///
+ /// Called when a technique is added to the technique list in the overlay.
+ /// Can be used to replace with custom one or add widgets for specific techniques.
+ /// Callback function signature: bool (api::effect_runtime *runtime, api::effect_technique technique)
+ ///
+ ///
+ /// To prevent the normal widget from being added to the overlay, return , otherwise return .
+ ///
+ ReShadeOverlayTechnique,
+ }
+}
diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs
new file mode 100644
index 000000000..d8d210076
--- /dev/null
+++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs
@@ -0,0 +1,200 @@
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+using Serilog;
+
+using TerraFX.Interop.Windows;
+
+using static TerraFX.Interop.Windows.Windows;
+
+namespace Dalamud.Interface.Internal.ReShadeHandling;
+
+/// ReShade interface.
+[SuppressMessage(
+ "StyleCop.CSharp.LayoutRules",
+ "SA1519:Braces should not be omitted from multi-line child statement",
+ Justification = "Multiple fixed blocks")]
+internal sealed unsafe partial class ReShadeAddonInterface
+{
+ private static readonly ExportsStruct Exports;
+
+ static ReShadeAddonInterface()
+ {
+ var modules = new List();
+ foreach (var m in Process.GetCurrentProcess().Modules.Cast())
+ {
+ ExportsStruct e;
+ if (!GetProcAddressInto(m, nameof(e.ReShadeRegisterAddon), &e.ReShadeRegisterAddon) ||
+ !GetProcAddressInto(m, nameof(e.ReShadeUnregisterAddon), &e.ReShadeUnregisterAddon) ||
+ !GetProcAddressInto(m, nameof(e.ReShadeRegisterEvent), &e.ReShadeRegisterEvent) ||
+ !GetProcAddressInto(m, nameof(e.ReShadeUnregisterEvent), &e.ReShadeUnregisterEvent))
+ continue;
+
+ modules.Add(m);
+ if (modules.Count == 1)
+ {
+ try
+ {
+ var signerName = GetSignatureSignerNameWithoutVerification(m.FileName);
+ ReShadeIsSignedByReShade = signerName == "ReShade";
+ Log.Information(
+ "ReShade DLL is signed by {signerName}. {vn}={v}",
+ signerName,
+ nameof(ReShadeIsSignedByReShade),
+ ReShadeIsSignedByReShade);
+ }
+ catch (Exception ex)
+ {
+ Log.Information(ex, "ReShade DLL did not had a valid signature.");
+ }
+
+ ReShadeModule = m;
+ Exports = e;
+ }
+ }
+
+ AllReShadeModules = [..modules];
+
+ return;
+
+ bool GetProcAddressInto(ProcessModule m, ReadOnlySpan name, void* res)
+ {
+ Span name8 = stackalloc byte[Encoding.UTF8.GetByteCount(name) + 1];
+ name8[Encoding.UTF8.GetBytes(name, name8)] = 0;
+ *(nint*)res = GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)Unsafe.AsPointer(ref name8[0]));
+ return *(nint*)res != 0;
+ }
+ }
+
+ /// Gets the active ReShade module.
+ public static ProcessModule? ReShadeModule { get; private set; }
+
+ /// Gets all the detected ReShade modules.
+ public static ImmutableArray AllReShadeModules { get; private set; }
+
+ /// Gets a value indicating whether the loaded ReShade has signatures.
+ /// ReShade without addon support is signed, but may not pass signature verification.
+ public static bool ReShadeIsSignedByReShade { get; private set; }
+
+ /// Finds the address of DXGISwapChain::on_present in .
+ /// Address of the function, or 0 if not found.
+ public static nint FindReShadeDxgiSwapChainOnPresent()
+ {
+ if (ReShadeModule is not { } rsm)
+ return 0;
+
+ var m = new ReadOnlySpan((void*)rsm.BaseAddress, rsm.ModuleMemorySize);
+
+ // Signature validated against 5.0.0 to 6.2.0
+ var i = m.IndexOf(new byte[] { 0xCC, 0xF6, 0xC2, 0x01, 0x0F, 0x85 });
+ if (i == -1)
+ return 0;
+
+ return rsm.BaseAddress + i + 1;
+ }
+
+ /// Gets the name of the signer of a file that has a certificate embedded within, without verifying if the
+ /// file has a valid signature.
+ /// Path to the file.
+ /// Name of the signer.
+ // https://learn.microsoft.com/en-us/previous-versions/troubleshoot/windows/win32/get-information-authenticode-signed-executables
+ private static string GetSignatureSignerNameWithoutVerification(ReadOnlySpan path)
+ {
+ var hCertStore = default(HCERTSTORE);
+ var hMsg = default(HCRYPTMSG);
+ var pCertContext = default(CERT_CONTEXT*);
+ try
+ {
+ fixed (void* pwszFile = path)
+ {
+ uint dwMsgAndCertEncodingType;
+ uint dwContentType;
+ uint dwFormatType;
+ void* pvContext;
+ if (!CryptQueryObject(
+ CERT.CERT_QUERY_OBJECT_FILE,
+ pwszFile,
+ CERT.CERT_QUERY_CONTENT_FLAG_ALL,
+ CERT.CERT_QUERY_FORMAT_FLAG_ALL,
+ 0,
+ &dwMsgAndCertEncodingType,
+ &dwContentType,
+ &dwFormatType,
+ &hCertStore,
+ &hMsg,
+ &pvContext))
+ {
+ throw new Win32Exception("CryptQueryObject");
+ }
+ }
+
+ var pcb = 0u;
+ if (!CryptMsgGetParam(hMsg, CMSG.CMSG_SIGNER_INFO_PARAM, 0, null, &pcb))
+ throw new Win32Exception("CryptMsgGetParam(1)");
+
+ var signerInfo = GC.AllocateArray((int)pcb, true);
+ var pSignerInfo = (CMSG_SIGNER_INFO*)Unsafe.AsPointer(ref signerInfo[0]);
+ if (!CryptMsgGetParam(hMsg, CMSG.CMSG_SIGNER_INFO_PARAM, 0, pSignerInfo, &pcb))
+ throw new Win32Exception("CryptMsgGetParam(2)");
+
+ var certInfo = new CERT_INFO
+ {
+ Issuer = pSignerInfo->Issuer,
+ SerialNumber = pSignerInfo->SerialNumber,
+ };
+ pCertContext = CertFindCertificateInStore(
+ hCertStore,
+ X509.X509_ASN_ENCODING | PKCS.PKCS_7_ASN_ENCODING,
+ 0,
+ CERT.CERT_FIND_SUBJECT_CERT,
+ &certInfo,
+ null);
+ if (pCertContext == default)
+ throw new Win32Exception("CertFindCertificateInStore");
+
+ pcb = CertGetNameStringW(
+ pCertContext,
+ CERT.CERT_NAME_SIMPLE_DISPLAY_TYPE,
+ CERT.CERT_NAME_ISSUER_FLAG,
+ null,
+ null,
+ pcb);
+ if (pcb == 0)
+ throw new Win32Exception("CertGetNameStringW(1)");
+
+ var issuerName = GC.AllocateArray((int)pcb, true);
+ pcb = CertGetNameStringW(
+ pCertContext,
+ CERT.CERT_NAME_SIMPLE_DISPLAY_TYPE,
+ CERT.CERT_NAME_ISSUER_FLAG,
+ null,
+ (ushort*)Unsafe.AsPointer(ref issuerName[0]),
+ pcb);
+ if (pcb == 0)
+ throw new Win32Exception("CertGetNameStringW(2)");
+
+ // The string is null-terminated.
+ return new(issuerName.AsSpan()[..^1]);
+ }
+ finally
+ {
+ if (pCertContext != default) CertFreeCertificateContext(pCertContext);
+ if (hCertStore != default) CertCloseStore(hCertStore, 0);
+ if (hMsg != default) CryptMsgClose(hMsg);
+ }
+ }
+
+ private struct ExportsStruct
+ {
+ public delegate* unmanaged ReShadeRegisterAddon;
+ public delegate* unmanaged ReShadeUnregisterAddon;
+ public delegate* unmanaged ReShadeRegisterEvent;
+ public delegate* unmanaged ReShadeUnregisterEvent;
+ }
+}
diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.cs
new file mode 100644
index 000000000..9062a6a1b
--- /dev/null
+++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.cs
@@ -0,0 +1,251 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+using Dalamud.Hooking;
+
+using JetBrains.Annotations;
+
+using TerraFX.Interop.Windows;
+
+using static TerraFX.Interop.Windows.Windows;
+
+namespace Dalamud.Interface.Internal.ReShadeHandling;
+
+/// ReShade interface.
+internal sealed unsafe partial class ReShadeAddonInterface : IDisposable
+{
+ private const int ReShadeApiVersion = 1;
+
+ private readonly HMODULE hDalamudModule;
+
+ private readonly Hook addonModuleResolverHook;
+
+ private readonly DelegateStorage presentDelegate;
+ private readonly DelegateStorage reShadeOverlayDelegate;
+ private readonly DelegateStorage initSwapChainDelegate;
+ private readonly DelegateStorage destroySwapChainDelegate;
+
+ private bool requiresFinalize;
+
+ private ReShadeAddonInterface()
+ {
+ this.hDalamudModule = (HMODULE)Marshal.GetHINSTANCE(typeof(ReShadeAddonInterface).Assembly.ManifestModule);
+ if (!Exports.ReShadeRegisterAddon(this.hDalamudModule, ReShadeApiVersion))
+ throw new InvalidOperationException("ReShadeRegisterAddon failure.");
+
+ // https://github.com/crosire/reshade/commit/eaaa2a2c5adf5749ad17b358305da3f2d0f6baf4
+ // TODO: when ReShade gets a proper release with this commit, make this hook optional
+ this.addonModuleResolverHook = Hook.FromImport(
+ ReShadeModule!,
+ "kernel32.dll",
+ nameof(GetModuleHandleExW),
+ 0,
+ this.GetModuleHandleExWDetour);
+
+ try
+ {
+ this.addonModuleResolverHook.Enable();
+ Exports.ReShadeRegisterEvent(
+ AddonEvent.Present,
+ this.presentDelegate = new(
+ (
+ ref ApiObject commandQueue,
+ ref ApiObject swapChain,
+ RECT* pSourceRect,
+ RECT* pDestRect,
+ uint dirtyRectCount,
+ void* pDirtyRects) =>
+ this.Present?.Invoke(
+ ref commandQueue,
+ ref swapChain,
+ pSourceRect is null ? default : new(pSourceRect, 1),
+ pDestRect is null ? default : new(pDestRect, 1),
+ new(pDirtyRects, (int)dirtyRectCount))));
+ Exports.ReShadeRegisterEvent(
+ AddonEvent.ReShadeOverlay,
+ this.reShadeOverlayDelegate = new((ref ApiObject rt) => this.ReShadeOverlay?.Invoke(ref rt)));
+ Exports.ReShadeRegisterEvent(
+ AddonEvent.InitSwapChain,
+ this.initSwapChainDelegate = new((ref ApiObject rt) => this.InitSwapChain?.Invoke(ref rt)));
+ Exports.ReShadeRegisterEvent(
+ AddonEvent.DestroySwapChain,
+ this.destroySwapChainDelegate = new((ref ApiObject rt) => this.DestroySwapChain?.Invoke(ref rt)));
+ }
+ catch (Exception e1)
+ {
+ Exports.ReShadeUnregisterAddon(this.hDalamudModule);
+
+ try
+ {
+ this.addonModuleResolverHook.Disable();
+ this.addonModuleResolverHook.Dispose();
+ }
+ catch (Exception e2)
+ {
+ throw new AggregateException(e1, e2);
+ }
+
+ throw;
+ }
+
+ this.requiresFinalize = true;
+ }
+
+ /// Finalizes an instance of the class.
+ ~ReShadeAddonInterface() => this.ReleaseUnmanagedResources();
+
+ /// Delegate for .
+ /// Current command queue. Type: api::command_queue.
+ /// Current swap chain. Type: api::swapchain.
+ /// Optional; source rectangle. May contain up to 1 element.
+ /// Optional; target rectangle. May contain up to 1 element.
+ /// Dirty rectangles.
+ public delegate void PresentDelegate(
+ ref ApiObject commandQueue,
+ ref ApiObject swapChain,
+ ReadOnlySpan sourceRect,
+ ReadOnlySpan destRect,
+ ReadOnlySpan dirtyRects);
+
+ /// Delegate for .
+ /// Reference to the ReShade runtime.
+ public delegate void ReShadeOverlayDelegate(ref ApiObject effectRuntime);
+
+ /// Delegate for .
+ /// Reference to the ReShade SwapChain wrapper.
+ public delegate void ReShadeInitSwapChain(ref ApiObject swapChain);
+
+ /// Delegate for .
+ /// Reference to the ReShade SwapChain wrapper.
+ public delegate void ReShadeDestroySwapChain(ref ApiObject swapChain);
+
+ /// Delegate for .
+ /// Current command queue. Type: api::command_queue.
+ /// Current swap chain. Type: api::swapchain.
+ /// Optional; source rectangle.
+ /// Optional; target rectangle.
+ /// Number of dirty rectangles.
+ /// Optional; dirty rectangles.
+ private delegate void UnsafePresentDelegate(
+ ref ApiObject commandQueue,
+ ref ApiObject swapChain,
+ RECT* pSourceRect,
+ RECT* pDestRect,
+ uint dirtyRectCount,
+ void* pDirtyRects);
+
+ private delegate BOOL GetModuleHandleExWDelegate(uint dwFlags, ushort* lpModuleName, HMODULE* phModule);
+
+ /// Called on .
+ public event PresentDelegate? Present;
+
+ /// Called on .
+ public event ReShadeOverlayDelegate? ReShadeOverlay;
+
+ /// Called on .
+ public event ReShadeInitSwapChain? InitSwapChain;
+
+ /// Called on .
+ public event ReShadeDestroySwapChain? DestroySwapChain;
+
+ /// Registers Dalamud as a ReShade addon.
+ /// Initialized interface.
+ /// true on success.
+ public static bool TryRegisterAddon([NotNullWhen(true)] out ReShadeAddonInterface? r)
+ {
+ try
+ {
+ r = Exports.ReShadeRegisterAddon is null ? null : new();
+ return r is not null;
+ }
+ catch
+ {
+ r = null;
+ return false;
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ this.ReleaseUnmanagedResources();
+ GC.SuppressFinalize(this);
+ }
+
+ private void ReleaseUnmanagedResources()
+ {
+ if (!this.requiresFinalize)
+ return;
+ this.requiresFinalize = false;
+ // This will also unregister addon event registrations.
+ Exports.ReShadeUnregisterAddon(this.hDalamudModule);
+ this.addonModuleResolverHook.Disable();
+ this.addonModuleResolverHook.Dispose();
+ }
+
+ private BOOL GetModuleHandleExWDetour(uint dwFlags, ushort* lpModuleName, HMODULE* phModule)
+ {
+ if ((dwFlags & GET.GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS) == 0)
+ return this.addonModuleResolverHook.Original(dwFlags, lpModuleName, phModule);
+ if ((dwFlags & GET.GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT) == 0)
+ return this.addonModuleResolverHook.Original(dwFlags, lpModuleName, phModule);
+ if (lpModuleName == this.initSwapChainDelegate ||
+ lpModuleName == this.destroySwapChainDelegate ||
+ lpModuleName == this.presentDelegate ||
+ lpModuleName == this.reShadeOverlayDelegate)
+ {
+ *phModule = this.hDalamudModule;
+ return BOOL.TRUE;
+ }
+
+ return this.addonModuleResolverHook.Original(dwFlags, lpModuleName, phModule);
+ }
+
+ /// ReShade effect runtime object.
+ [StructLayout(LayoutKind.Sequential)]
+ public struct ApiObject
+ {
+ /// The vtable.
+ public VTable* Vtbl;
+
+ /// Gets this object as a typed pointer.
+ /// Address of this instance.
+ /// This call is invalid if this object is not already fixed.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ApiObject* AsPointer() => (ApiObject*)Unsafe.AsPointer(ref this);
+
+ /// Gets the native object.
+ /// The native object.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public nint GetNative() => this.Vtbl->GetNative(this.AsPointer());
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public T* GetNative() where T : unmanaged => (T*)this.GetNative();
+
+ /// VTable of .
+ [StructLayout(LayoutKind.Sequential)]
+ public struct VTable
+ {
+ ///
+ public delegate* unmanaged GetNative;
+ }
+ }
+
+ private readonly struct DelegateStorage where T : Delegate
+ {
+ [UsedImplicitly]
+ public readonly T Delegate;
+
+ public readonly void* Address;
+
+ public DelegateStorage(T @delegate)
+ {
+ this.Delegate = @delegate;
+ this.Address = (void*)Marshal.GetFunctionPointerForDelegate(@delegate);
+ }
+
+ public static implicit operator void*(DelegateStorage sto) => sto.Address;
+ }
+}
diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeHandlingMode.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeHandlingMode.cs
new file mode 100644
index 000000000..b02fd630d
--- /dev/null
+++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeHandlingMode.cs
@@ -0,0 +1,26 @@
+namespace Dalamud.Interface.Internal.ReShadeHandling;
+
+/// Available handling modes for working with ReShade.
+internal enum ReShadeHandlingMode
+{
+ /// Use the default method, whatever it is for the current Dalamud version.
+ Default = 0,
+
+ /// Unwrap ReShade from the swap chain obtained from the game.
+ UnwrapReShade,
+
+ /// Register as a ReShade addon, and draw on event.
+ ///
+ ReShadeAddonPresent,
+
+ /// Register as a ReShade addon, and draw on
+ /// event.
+ ReShadeAddonReShadeOverlay,
+
+ /// Hook DXGISwapChain::on_present(UINT flags, const DXGI_PRESENT_PARAMETERS *params) in
+ /// dxgi_swapchain.cpp.
+ HookReShadeDxgiSwapChainOnPresent,
+
+ /// Do not do anything special about it. ReShade will process Dalamud rendered stuff.
+ None = -1,
+}
diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs
new file mode 100644
index 000000000..f1210425d
--- /dev/null
+++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs
@@ -0,0 +1,194 @@
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+using TerraFX.Interop.Windows;
+
+using static TerraFX.Interop.Windows.Windows;
+
+namespace Dalamud.Interface.Internal.ReShadeHandling;
+
+/// Unwraps IUnknown wrapped by ReShade.
+internal static unsafe class ReShadeUnwrapper
+{
+ /// Unwraps if it is wrapped by ReShade.
+ /// [inout] The COM pointer to an instance of .
+ /// A COM type that is or extends .
+ /// true if peeled.
+ public static bool Unwrap(ComPtr* comptr)
+ where T : unmanaged, IUnknown.Interface
+ {
+ if (typeof(T).GetNestedType("Vtbl`1") is not { } vtblType)
+ return false;
+
+ nint vtblSize = vtblType.GetFields().Length * sizeof(nint);
+ var changed = false;
+ while (comptr->Get() != null && IsReShadedComObject(comptr->Get()))
+ {
+ // Expectation: the pointer to the underlying object should come early after the overriden vtable.
+ for (nint i = sizeof(nint); i <= 0x20; i += sizeof(nint))
+ {
+ var ppObjectBehind = (nint)comptr->Get() + i;
+
+ // Is the thing directly pointed from the address an actual something in the memory?
+ if (!IsValidReadableMemoryAddress(ppObjectBehind, 8))
+ continue;
+
+ var pObjectBehind = *(nint*)ppObjectBehind;
+
+ // Is the address of vtable readable?
+ if (!IsValidReadableMemoryAddress(pObjectBehind, sizeof(nint)))
+ continue;
+ var pObjectBehindVtbl = *(nint*)pObjectBehind;
+
+ // Is the vtable itself readable?
+ if (!IsValidReadableMemoryAddress(pObjectBehindVtbl, vtblSize))
+ continue;
+
+ // Are individual functions in vtable executable?
+ var valid = true;
+ for (var j = 0; valid && j < vtblSize; j += sizeof(nint))
+ valid &= IsValidExecutableMemoryAddress(*(nint*)(pObjectBehindVtbl + j), 1);
+ if (!valid)
+ continue;
+
+ // Interpret the object as an IUnknown.
+ // Note that `using` is not used, and `Attach` is used. We do not alter the reference count yet.
+ var punk = default(ComPtr);
+ punk.Attach((IUnknown*)pObjectBehind);
+
+ // Is the IUnknown object also the type we want?
+ using var comptr2 = default(ComPtr);
+ if (punk.As(&comptr2).FAILED)
+ continue;
+
+ comptr2.Swap(comptr);
+ changed = true;
+ break;
+ }
+
+ if (!changed)
+ break;
+ }
+
+ return changed;
+ }
+
+ private static bool BelongsInReShadeDll(nint ptr)
+ {
+ foreach (ProcessModule processModule in Process.GetCurrentProcess().Modules)
+ {
+ if (ptr < processModule.BaseAddress ||
+ ptr >= processModule.BaseAddress + processModule.ModuleMemorySize ||
+ !HasProcExported(processModule, "ReShadeRegisterAddon"u8) ||
+ !HasProcExported(processModule, "ReShadeUnregisterAddon"u8) ||
+ !HasProcExported(processModule, "ReShadeRegisterEvent"u8) ||
+ !HasProcExported(processModule, "ReShadeUnregisterEvent"u8))
+ continue;
+
+ return true;
+ }
+
+ return false;
+
+ static bool HasProcExported(ProcessModule m, ReadOnlySpan name)
+ {
+ fixed (byte* p = name)
+ return GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)p) != 0;
+ }
+ }
+
+ private static bool IsReShadedComObject(T* obj)
+ where T : unmanaged, IUnknown.Interface
+ {
+ if (!IsValidReadableMemoryAddress((nint)obj, sizeof(nint)))
+ return false;
+
+ try
+ {
+ var vtbl = (nint**)Marshal.ReadIntPtr((nint)obj);
+ if (!IsValidReadableMemoryAddress((nint)vtbl, sizeof(nint) * 3))
+ return false;
+
+ for (var i = 0; i < 3; i++)
+ {
+ var pfn = Marshal.ReadIntPtr((nint)(vtbl + i));
+ if (!IsValidExecutableMemoryAddress(pfn, 1))
+ return false;
+ if (!BelongsInReShadeDll(pfn))
+ return false;
+ }
+
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private static bool IsValidReadableMemoryAddress(nint p, nint size)
+ {
+ while (size > 0)
+ {
+ if (!IsValidUserspaceMemoryAddress(p))
+ return false;
+
+ MEMORY_BASIC_INFORMATION mbi;
+ if (VirtualQuery((void*)p, &mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION)) == 0)
+ return false;
+
+ if (mbi is not
+ {
+ State: MEM.MEM_COMMIT,
+ Protect: PAGE.PAGE_READONLY or PAGE.PAGE_READWRITE or PAGE.PAGE_EXECUTE_READ
+ or PAGE.PAGE_EXECUTE_READWRITE,
+ })
+ return false;
+
+ var regionSize = (nint)((mbi.RegionSize + 0xFFFUL) & ~0x1000UL);
+ var checkedSize = ((nint)mbi.BaseAddress + regionSize) - p;
+ size -= checkedSize;
+ p += checkedSize;
+ }
+
+ return true;
+ }
+
+ private static bool IsValidExecutableMemoryAddress(nint p, nint size)
+ {
+ while (size > 0)
+ {
+ if (!IsValidUserspaceMemoryAddress(p))
+ return false;
+
+ MEMORY_BASIC_INFORMATION mbi;
+ if (VirtualQuery((void*)p, &mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION)) == 0)
+ return false;
+
+ if (mbi is not
+ {
+ State: MEM.MEM_COMMIT,
+ Protect: PAGE.PAGE_EXECUTE or PAGE.PAGE_EXECUTE_READ or PAGE.PAGE_EXECUTE_READWRITE
+ or PAGE.PAGE_EXECUTE_WRITECOPY,
+ })
+ return false;
+
+ var regionSize = (nint)((mbi.RegionSize + 0xFFFUL) & ~0x1000UL);
+ var checkedSize = ((nint)mbi.BaseAddress + regionSize) - p;
+ size -= checkedSize;
+ p += checkedSize;
+ }
+
+ return true;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsValidUserspaceMemoryAddress(nint p)
+ {
+ // https://learn.microsoft.com/en-us/windows-hardware/drivers/gettingstarted/virtual-address-spaces
+ // A 64-bit process on 64-bit Windows has a virtual address space within the 128-terabyte range
+ // 0x000'00000000 through 0x7FFF'FFFFFFFF.
+ return p >= 0x10000 && p <= unchecked((nint)0x7FFF_FFFFFFFFUL);
+ }
+}
diff --git a/Dalamud/Interface/Internal/SwapChainHelper.cs b/Dalamud/Interface/Internal/SwapChainHelper.cs
index e30483983..6e9254680 100644
--- a/Dalamud/Interface/Internal/SwapChainHelper.cs
+++ b/Dalamud/Interface/Internal/SwapChainHelper.cs
@@ -1,20 +1,38 @@
using System.Threading;
+using Dalamud.Interface.Internal.ReShadeHandling;
+
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using TerraFX.Interop.DirectX;
+using TerraFX.Interop.Windows;
namespace Dalamud.Interface.Internal;
/// Helper for dealing with swap chains.
internal static unsafe class SwapChainHelper
{
+ private static IDXGISwapChain* foundGameDeviceSwapChain;
+
+ /// Describes how to hook methods.
+ public enum HookMode
+ {
+ /// Hooks by rewriting the native bytecode.
+ ByteCode,
+
+ /// Hooks by providing an alternative vtable.
+ VTable,
+ }
+
/// Gets the game's active instance of IDXGISwapChain that is initialized.
/// Address of the game's instance of IDXGISwapChain, or null if not available (yet.)
public static IDXGISwapChain* GameDeviceSwapChain
{
get
{
+ if (foundGameDeviceSwapChain is not null)
+ return foundGameDeviceSwapChain;
+
var kernelDev = Device.Instance();
if (kernelDev == null)
return null;
@@ -28,7 +46,7 @@ internal static unsafe class SwapChainHelper
if (swapChain->BackBuffer == null)
return null;
- return (IDXGISwapChain*)swapChain->DXGISwapChain;
+ return foundGameDeviceSwapChain = (IDXGISwapChain*)swapChain->DXGISwapChain;
}
}
@@ -42,10 +60,57 @@ internal static unsafe class SwapChainHelper
}
}
+ ///
+ public static bool IsGameDeviceSwapChain(nint punk) => IsGameDeviceSwapChain((IUnknown*)punk);
+
+ /// Determines if the given instance of IUnknown is the game device's swap chain.
+ /// Object to check.
+ /// Type of the object to check.
+ /// true if the object is the game's swap chain.
+ public static bool IsGameDeviceSwapChain(T* punk) where T : unmanaged, IUnknown.Interface
+ {
+ using var psc = default(ComPtr);
+ fixed (Guid* piid = &IID.IID_IDXGISwapChain)
+ {
+ if (punk->QueryInterface(piid, (void**)psc.GetAddressOf()).FAILED)
+ return false;
+ }
+
+ return IsGameDeviceSwapChain(psc.Get());
+ }
+
+ ///
+ public static bool IsGameDeviceSwapChain(IDXGISwapChain* punk)
+ {
+ DXGI_SWAP_CHAIN_DESC desc1;
+ if (punk->GetDesc(&desc1).FAILED)
+ return false;
+
+ DXGI_SWAP_CHAIN_DESC desc2;
+ if (GameDeviceSwapChain->GetDesc(&desc2).FAILED)
+ return false;
+
+ return desc1.OutputWindow == desc2.OutputWindow;
+ }
+
/// Wait for the game to have finished initializing the IDXGISwapChain.
public static void BusyWaitForGameDeviceSwapChain()
{
while (GameDeviceSwapChain is null)
Thread.Yield();
}
+
+ ///
+ /// Make store address of unwrapped swap chain, if it was wrapped with ReShade.
+ ///
+ /// true if it was wrapped with ReShade.
+ public static bool UnwrapReShade()
+ {
+ using var swapChain = new ComPtr(GameDeviceSwapChain);
+ if (!ReShadeUnwrapper.Unwrap(&swapChain))
+ return false;
+
+ foundGameDeviceSwapChain = swapChain.Get();
+ return true;
+ }
}
diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs
index da5dc0d98..97eec1ee1 100644
--- a/Dalamud/Interface/Internal/UiDebug.cs
+++ b/Dalamud/Interface/Internal/UiDebug.cs
@@ -189,6 +189,7 @@ internal unsafe class UiDebug
case NodeType.Image: Util.ShowStruct(*(AtkImageNode*)node, (ulong)node); break;
case NodeType.Collision: Util.ShowStruct(*(AtkCollisionNode*)node, (ulong)node); break;
case NodeType.NineGrid: Util.ShowStruct(*(AtkNineGridNode*)node, (ulong)node); break;
+ case NodeType.ClippingMask: Util.ShowStruct(*(AtkClippingMaskNode*)node, (ulong)node); break;
case NodeType.Counter: Util.ShowStruct(*(AtkCounterNode*)node, (ulong)node); break;
default: Util.ShowStruct(*node, (ulong)node); break;
}
@@ -233,48 +234,15 @@ internal unsafe class UiDebug
break;
case NodeType.Image:
var imageNode = (AtkImageNode*)node;
- if (imageNode->PartsList != null)
- {
- if (imageNode->PartId > imageNode->PartsList->PartCount)
- {
- ImGui.Text("part id > part count?");
- }
- else
- {
- var textureInfo = imageNode->PartsList->Parts[imageNode->PartId].UldAsset;
- var texType = textureInfo->AtkTexture.TextureType;
- ImGui.Text($"texture type: {texType} part_id={imageNode->PartId} part_id_count={imageNode->PartsList->PartCount}");
- if (texType == TextureType.Resource)
- {
- var texFileNameStdString = &textureInfo->AtkTexture.Resource->TexFileResourceHandle->ResourceHandle.FileName;
- var texString = texFileNameStdString->Length < 16
- ? MemoryHelper.ReadSeStringAsString(out _, (nint)texFileNameStdString->Buffer)
- : MemoryHelper.ReadSeStringAsString(out _, (nint)texFileNameStdString->BufferPtr);
-
- ImGui.Text($"texture path: {texString}");
- var kernelTexture = textureInfo->AtkTexture.Resource->KernelTextureObject;
-
- if (ImGui.TreeNode($"Texture##{(ulong)kernelTexture->D3D11ShaderResourceView:X}"))
- {
- ImGui.Image(new IntPtr(kernelTexture->D3D11ShaderResourceView), new Vector2(kernelTexture->Width, kernelTexture->Height));
- ImGui.TreePop();
- }
- }
- else if (texType == TextureType.KernelTexture)
- {
- if (ImGui.TreeNode($"Texture##{(ulong)textureInfo->AtkTexture.KernelTexture->D3D11ShaderResourceView:X}"))
- {
- ImGui.Image(new IntPtr(textureInfo->AtkTexture.KernelTexture->D3D11ShaderResourceView), new Vector2(textureInfo->AtkTexture.KernelTexture->Width, textureInfo->AtkTexture.KernelTexture->Height));
- ImGui.TreePop();
- }
- }
- }
- }
- else
- {
- ImGui.Text("no texture loaded");
- }
-
+ PrintTextureInfo(imageNode->PartsList, imageNode->PartId);
+ break;
+ case NodeType.NineGrid:
+ var ngNode = (AtkNineGridNode*)node;
+ PrintTextureInfo(ngNode->PartsList, ngNode->PartId);
+ break;
+ case NodeType.ClippingMask:
+ var cmNode = (AtkClippingMaskNode*)node;
+ PrintTextureInfo(cmNode->PartsList, cmNode->PartId);
break;
}
@@ -287,6 +255,60 @@ internal unsafe class UiDebug
if (isVisible && !popped)
ImGui.PopStyleColor();
+
+ static void PrintTextureInfo(AtkUldPartsList* partsList, uint partId)
+ {
+ if (partsList != null)
+ {
+ if (partId > partsList->PartCount)
+ {
+ ImGui.Text("part id > part count?");
+ }
+ else
+ {
+ var textureInfo = partsList->Parts[partId].UldAsset;
+ var texType = textureInfo->AtkTexture.TextureType;
+ ImGui.Text(
+ $"texture type: {texType} part_id={partId} part_id_count={partsList->PartCount}");
+ if (texType == TextureType.Resource)
+ {
+ var texFileNameStdString =
+ &textureInfo->AtkTexture.Resource->TexFileResourceHandle->ResourceHandle.FileName;
+ var texString = texFileNameStdString->Length < 16
+ ? MemoryHelper.ReadSeStringAsString(out _, (nint)texFileNameStdString->Buffer)
+ : MemoryHelper.ReadSeStringAsString(out _, (nint)texFileNameStdString->BufferPtr);
+
+ ImGui.Text($"texture path: {texString}");
+ var kernelTexture = textureInfo->AtkTexture.Resource->KernelTextureObject;
+
+ if (ImGui.TreeNode($"Texture##{(ulong)kernelTexture->D3D11ShaderResourceView:X}"))
+ {
+ ImGui.Image(
+ new IntPtr(kernelTexture->D3D11ShaderResourceView),
+ new Vector2(kernelTexture->Width, kernelTexture->Height));
+ ImGui.TreePop();
+ }
+ }
+ else if (texType == TextureType.KernelTexture)
+ {
+ if (ImGui.TreeNode(
+ $"Texture##{(ulong)textureInfo->AtkTexture.KernelTexture->D3D11ShaderResourceView:X}"))
+ {
+ ImGui.Image(
+ new IntPtr(textureInfo->AtkTexture.KernelTexture->D3D11ShaderResourceView),
+ new Vector2(
+ textureInfo->AtkTexture.KernelTexture->Width,
+ textureInfo->AtkTexture.KernelTexture->Height));
+ ImGui.TreePop();
+ }
+ }
+ }
+ }
+ else
+ {
+ ImGui.Text("no texture loaded");
+ }
+ }
}
private void PrintComponentNode(AtkResNode* node, string treePrefix)
@@ -541,11 +563,13 @@ internal unsafe class UiDebug
private Vector2 GetNodePosition(AtkResNode* node)
{
var pos = new Vector2(node->X, node->Y);
+ pos -= new Vector2(node->OriginX * (node->ScaleX - 1), node->OriginY * (node->ScaleY - 1));
var par = node->ParentNode;
while (par != null)
{
pos *= new Vector2(par->ScaleX, par->ScaleY);
pos += new Vector2(par->X, par->Y);
+ pos -= new Vector2(par->OriginX * (par->ScaleX - 1), par->OriginY * (par->ScaleY - 1));
par = par->ParentNode;
}
diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs
index 67685f38a..d42dc3669 100644
--- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs
@@ -360,7 +360,7 @@ internal sealed class ChangelogWindow : Window, IDisposable
{
case State.WindowFadeIn:
case State.ExplainerIntro:
- ImGui.TextWrapped($"Welcome to Dalamud v{Util.AssemblyVersion}!");
+ ImGui.TextWrapped($"Welcome to Dalamud v{Util.GetScmVersion()}!");
ImGuiHelpers.ScaledDummy(5);
ImGui.TextWrapped(ChangeLog);
ImGuiHelpers.ScaledDummy(5);
diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs
index 8f7c0e36c..f7ce5d145 100644
--- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs
@@ -18,7 +18,6 @@ using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
-using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
@@ -39,9 +38,6 @@ internal class ConsoleWindow : Window, IDisposable
private const int LogLinesMaximum = 1000000;
private const int HistorySize = 50;
- // Only this field may be touched from any thread.
- private readonly ConcurrentQueue<(string Line, LogEvent LogEvent)> newLogEntries;
-
// Fields below should be touched only from the main thread.
private readonly RollingList logText;
private readonly RollingList filteredLogEntries;
@@ -94,7 +90,6 @@ internal class ConsoleWindow : Window, IDisposable
this.autoScroll = configuration.LogAutoScroll;
this.autoOpen = configuration.LogOpenAtStartup;
- SerilogEventSink.Instance.LogLine += this.OnLogLine;
Service.GetAsync().ContinueWith(r => r.Result.Update += this.FrameworkOnUpdate);
@@ -114,7 +109,6 @@ internal class ConsoleWindow : Window, IDisposable
this.logLinesLimit = configuration.LogLinesLimit;
var limit = Math.Max(LogLinesMinimum, this.logLinesLimit);
- this.newLogEntries = new();
this.logText = new(limit);
this.filteredLogEntries = new(limit);
@@ -126,6 +120,9 @@ internal class ConsoleWindow : Window, IDisposable
}
}
+ /// Gets the queue where log entries that are not processed yet are stored.
+ public static ConcurrentQueue<(string Line, LogEvent LogEvent)> NewLogEntries { get; } = new();
+
///
public override void OnOpen()
{
@@ -136,7 +133,6 @@ internal class ConsoleWindow : Window, IDisposable
///
public void Dispose()
{
- SerilogEventSink.Instance.LogLine -= this.OnLogLine;
this.configuration.DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved;
if (Service.GetNullable() is { } framework)
framework.Update -= this.FrameworkOnUpdate;
@@ -324,7 +320,7 @@ internal class ConsoleWindow : Window, IDisposable
ImGuiInputTextFlags.CallbackHistory | ImGuiInputTextFlags.CallbackEdit,
this.CommandInputCallback))
{
- this.newLogEntries.Enqueue((this.commandText, new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, new MessageTemplate(string.Empty, []), [])));
+ NewLogEntries.Enqueue((this.commandText, new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, new MessageTemplate(string.Empty, []), [])));
this.ProcessCommand();
getFocus = true;
}
@@ -372,7 +368,7 @@ internal class ConsoleWindow : Window, IDisposable
this.pendingClearLog = false;
this.logText.Clear();
this.filteredLogEntries.Clear();
- this.newLogEntries.Clear();
+ NewLogEntries.Clear();
}
if (this.pendingRefilter)
@@ -388,7 +384,7 @@ internal class ConsoleWindow : Window, IDisposable
var numPrevFilteredLogEntries = this.filteredLogEntries.Count;
var addedLines = 0;
- while (this.newLogEntries.TryDequeue(out var logLine))
+ while (NewLogEntries.TryDequeue(out var logLine))
addedLines += this.HandleLogLine(logLine.Line, logLine.LogEvent);
this.newRolledLines = addedLines - (this.filteredLogEntries.Count - numPrevFilteredLogEntries);
}
@@ -1062,11 +1058,6 @@ internal class ConsoleWindow : Window, IDisposable
/// Queues filtering the log entries again, before next call to .
private void QueueRefilter() => this.pendingRefilter = true;
- /// Enqueues the new log line to the log-to-be-processed queue.
- /// See for the handler for the queued log entries.
- private void OnLogLine(object sender, (string Line, LogEvent LogEvent) logEvent) =>
- this.newLogEntries.Enqueue(logEvent);
-
private bool DrawToggleButtonWithTooltip(
string buttonId, string tooltip, FontAwesomeIcon icon, ref bool enabledState)
{
diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs
index e67ff3cf5..e95d2e1b8 100644
--- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs
@@ -315,7 +315,7 @@ internal class PluginImageCache : IInternalDisposableService
private Task RunInDownloadQueue(Func> func, ulong requestedFrame)
{
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
this.downloadQueue.Add(Tuple.Create(requestedFrame, async () =>
{
try
@@ -332,7 +332,7 @@ internal class PluginImageCache : IInternalDisposableService
private Task RunInLoadQueue(Func> func)
{
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
this.loadQueue.Add(async () =>
{
try
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
index d4700ddb5..40753a20d 100644
--- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
@@ -118,7 +118,8 @@ internal class PluginInstallerWindow : Window, IDisposable
private List pluginListInstalled = new();
private List pluginListUpdatable = new();
private bool hasDevPlugins = false;
-
+ private bool hasHiddenPlugins = false;
+
private string searchText = string.Empty;
private bool isSearchTextPrefilled = false;
@@ -304,7 +305,7 @@ internal class PluginInstallerWindow : Window, IDisposable
return;
var versionInfo = t.Result;
- if (versionInfo.AssemblyVersion != Util.GetGitHash() &&
+ if (versionInfo.AssemblyVersion != Util.GetScmVersion() &&
versionInfo.Track != "release" &&
string.Equals(versionInfo.Key, config.DalamudBetaKey, StringComparison.OrdinalIgnoreCase))
this.staleDalamudNewVersion = versionInfo.AssemblyVersion;
@@ -1277,6 +1278,19 @@ internal class PluginInstallerWindow : Window, IDisposable
proxies.Add(new PluginInstallerAvailablePluginProxy(null, installedPlugin));
}
+ var configuration = Service.Get();
+ bool IsProxyHidden(PluginInstallerAvailablePluginProxy proxy)
+ {
+ var isHidden =
+ configuration.HiddenPluginInternalName.Contains(proxy.RemoteManifest?.InternalName);
+ if (this.categoryManager.CurrentCategoryKind == PluginCategoryManager.CategoryKind.Hidden)
+ return isHidden;
+ return !isHidden;
+ }
+
+ // Filter out plugins that are not hidden
+ proxies = proxies.Where(IsProxyHidden).ToList();
+
return proxies;
}
#pragma warning restore SA1201
@@ -1305,6 +1319,12 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.PopID();
}
+
+ // Reset the category to "All" if we're on the "Hidden" category and there are no hidden plugins (we removed the last one)
+ if (i == 0 && this.categoryManager.CurrentCategoryKind == PluginCategoryManager.CategoryKind.Hidden)
+ {
+ this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.All;
+ }
}
private void DrawInstalledPluginList(InstalledPluginListFilter filter)
@@ -1471,6 +1491,10 @@ internal class PluginInstallerWindow : Window, IDisposable
if (!Service.Get().DoPluginTest)
continue;
break;
+ case PluginCategoryManager.CategoryInfo.AppearCondition.AnyHiddenPlugins:
+ if (!this.hasHiddenPlugins)
+ continue;
+ break;
default:
throw new ArgumentOutOfRangeException();
}
@@ -1540,7 +1564,7 @@ internal class PluginInstallerWindow : Window, IDisposable
DrawWarningIcon();
DrawLinesCentered("A new version of Dalamud is available.\n" +
"Please restart the game to ensure compatibility with updated plugins.\n" +
- $"old: {Util.GetGitHash()} new: {this.staleDalamudNewVersion}");
+ $"old: {Util.GetScmVersion()} new: {this.staleDalamudNewVersion}");
ImGuiHelpers.ScaledDummy(10);
}
@@ -2276,6 +2300,16 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.TextColored(ImGuiColors.DalamudGrey3, Locs.PluginBody_AuthorWithoutDownloadCount(log.Author));
}
+ if (log.Date != DateTime.MinValue)
+ {
+ var whenText = log.Date.LocRelativePastLong();
+ var whenSize = ImGui.CalcTextSize(whenText);
+ ImGui.SameLine(ImGui.GetWindowWidth() - whenSize.X - (25 * ImGuiHelpers.GlobalScale));
+ ImGui.TextColored(ImGuiColors.DalamudGrey3, whenText);
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip("Published on " + log.Date.LocAbsolute());
+ }
+
cursor.Y += ImGui.GetTextLineHeightWithSpacing();
ImGui.SetCursorPos(cursor);
@@ -2446,12 +2480,19 @@ internal class PluginInstallerWindow : Window, IDisposable
pluginManager.RefilterPluginMasters();
}
- if (ImGui.Selectable(Locs.PluginContext_HidePlugin))
+ var isHidden = configuration.HiddenPluginInternalName.Contains(manifest.InternalName);
+ switch (isHidden)
{
- Log.Debug($"Adding {manifest.InternalName} to hidden plugins");
- configuration.HiddenPluginInternalName.Add(manifest.InternalName);
- configuration.QueueSave();
- pluginManager.RefilterPluginMasters();
+ case false when ImGui.Selectable(Locs.PluginContext_HidePlugin):
+ configuration.HiddenPluginInternalName.Add(manifest.InternalName);
+ configuration.QueueSave();
+ pluginManager.RefilterPluginMasters();
+ break;
+ case true when ImGui.Selectable(Locs.PluginContext_UnhidePlugin):
+ configuration.HiddenPluginInternalName.Remove(manifest.InternalName);
+ configuration.QueueSave();
+ pluginManager.RefilterPluginMasters();
+ break;
}
if (ImGui.Selectable(Locs.PluginContext_DeletePluginConfig))
@@ -2614,7 +2655,24 @@ internal class PluginInstallerWindow : Window, IDisposable
var applicableChangelog = plugin.IsTesting ? remoteManifest?.Changelog : remoteManifest?.TestingChangelog;
var hasChangelog = !applicableChangelog.IsNullOrWhitespace();
- var didDrawChangelogInsideCollapsible = false;
+ var didDrawApplicableChangelogInsideCollapsible = false;
+
+ Version? availablePluginUpdateVersion = null;
+ string? availableChangelog = null;
+ var didDrawAvailableChangelogInsideCollapsible = false;
+
+ if (availablePluginUpdate != default)
+ {
+ availablePluginUpdateVersion =
+ availablePluginUpdate.UseTesting ?
+ availablePluginUpdate.UpdateManifest.TestingAssemblyVersion :
+ availablePluginUpdate.UpdateManifest.AssemblyVersion;
+
+ availableChangelog =
+ availablePluginUpdate.UseTesting ?
+ availablePluginUpdate.UpdateManifest.TestingChangelog :
+ availablePluginUpdate.UpdateManifest.Changelog;
+ }
var flags = PluginHeaderFlags.None;
if (plugin.IsThirdParty)
@@ -2748,28 +2806,33 @@ internal class PluginInstallerWindow : Window, IDisposable
{
if (ImGui.TreeNode(Locs.PluginBody_CurrentChangeLog(plugin.EffectiveVersion)))
{
- didDrawChangelogInsideCollapsible = true;
+ didDrawApplicableChangelogInsideCollapsible = true;
this.DrawInstalledPluginChangelog(applicableChangelog);
ImGui.TreePop();
}
}
- if (availablePluginUpdate != default && !availablePluginUpdate.UpdateManifest.Changelog.IsNullOrWhitespace())
+ if (!availableChangelog.IsNullOrWhitespace() && ImGui.TreeNode(Locs.PluginBody_UpdateChangeLog(availablePluginUpdateVersion)))
{
- var availablePluginUpdateVersion = availablePluginUpdate.UseTesting ? availablePluginUpdate.UpdateManifest.TestingAssemblyVersion : availablePluginUpdate.UpdateManifest.AssemblyVersion;
- var availableChangelog = availablePluginUpdate.UseTesting ? availablePluginUpdate.UpdateManifest.TestingChangelog : availablePluginUpdate.UpdateManifest.Changelog;
- if (!availableChangelog.IsNullOrWhitespace() && ImGui.TreeNode(Locs.PluginBody_UpdateChangeLog(availablePluginUpdateVersion)))
- {
- this.DrawInstalledPluginChangelog(availableChangelog);
- ImGui.TreePop();
- }
+ this.DrawInstalledPluginChangelog(availableChangelog);
+ ImGui.TreePop();
+ didDrawAvailableChangelogInsideCollapsible = true;
}
}
- if (thisWasUpdated && hasChangelog && !didDrawChangelogInsideCollapsible)
+ if (thisWasUpdated &&
+ hasChangelog &&
+ !didDrawApplicableChangelogInsideCollapsible)
{
this.DrawInstalledPluginChangelog(applicableChangelog);
}
+
+ if (this.categoryManager.CurrentCategoryKind == PluginCategoryManager.CategoryKind.UpdateablePlugins &&
+ !availableChangelog.IsNullOrWhitespace() &&
+ !didDrawAvailableChangelogInsideCollapsible)
+ {
+ this.DrawInstalledPluginChangelog(availableChangelog);
+ }
ImGui.PopID();
}
@@ -2826,6 +2889,7 @@ internal class PluginInstallerWindow : Window, IDisposable
}
configuration.QueueSave();
+ _ = pluginManager.ReloadPluginMastersAsync();
}
if (repoManifest?.IsTestingExclusive == true)
@@ -3606,6 +3670,7 @@ internal class PluginInstallerWindow : Window, IDisposable
private void OnAvailablePluginsChanged()
{
var pluginManager = Service.Get();
+ var configuration = Service.Get();
lock (this.listLock)
{
@@ -3615,6 +3680,8 @@ internal class PluginInstallerWindow : Window, IDisposable
this.pluginListUpdatable = pluginManager.UpdatablePlugins.ToList();
this.ResortPlugins();
}
+
+ this.hasHiddenPlugins = this.pluginListAvailable.Any(x => configuration.HiddenPluginInternalName.Contains(x.InternalName));
this.UpdateCategoriesOnPluginsChange();
}
@@ -3708,7 +3775,7 @@ internal class PluginInstallerWindow : Window, IDisposable
this.errorModalMessage = message;
this.errorModalDrawing = true;
this.errorModalOnNextFrame = true;
- this.errorModalTaskCompletionSource = new TaskCompletionSource();
+ this.errorModalTaskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
return this.errorModalTaskCompletionSource.Task;
}
@@ -3716,7 +3783,7 @@ internal class PluginInstallerWindow : Window, IDisposable
{
this.updateModalOnNextFrame = true;
this.updateModalPlugin = plugin;
- this.updateModalTaskCompletionSource = new TaskCompletionSource();
+ this.updateModalTaskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
return this.updateModalTaskCompletionSource.Task;
}
@@ -3875,7 +3942,7 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string TabBody_NoPluginsUpdateable => Loc.Localize("InstallerNoPluginsUpdate", "No plugins have updates available at the moment.");
- public static string TabBody_NoPluginsDev => Loc.Localize("InstallerNoPluginsDev", "You don't have any dev plugins. Add them some the settings.");
+ public static string TabBody_NoPluginsDev => Loc.Localize("InstallerNoPluginsDev", "You don't have any dev plugins. Add them from the settings.");
#endregion
@@ -3942,6 +4009,8 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string PluginContext_MarkAllSeen => Loc.Localize("InstallerMarkAllSeen", "Mark all as seen");
public static string PluginContext_HidePlugin => Loc.Localize("InstallerHidePlugin", "Hide from installer");
+
+ public static string PluginContext_UnhidePlugin => Loc.Localize("InstallerUnhidePlugin", "Unhide from installer");
public static string PluginContext_DeletePluginConfig => Loc.Localize("InstallerDeletePluginConfig", "Reset plugin data");
diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/NamePlateAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/NamePlateAgingStep.cs
new file mode 100644
index 000000000..5a03a6dc2
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/NamePlateAgingStep.cs
@@ -0,0 +1,123 @@
+using System.Collections.Generic;
+
+using Dalamud.Game.Gui.NamePlate;
+using Dalamud.Game.Text.SeStringHandling;
+using Dalamud.Game.Text.SeStringHandling.Payloads;
+
+using ImGuiNET;
+
+namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps;
+
+///
+/// Tests for nameplates.
+///
+internal class NamePlateAgingStep : IAgingStep
+{
+ private SubStep currentSubStep;
+ private Dictionary? updateCount;
+
+ private enum SubStep
+ {
+ Start,
+ Confirm,
+ }
+
+ ///
+ public string Name => "Test Nameplates";
+
+ ///
+ public SelfTestStepResult RunStep()
+ {
+ var namePlateGui = Service.Get();
+
+ switch (this.currentSubStep)
+ {
+ case SubStep.Start:
+ namePlateGui.OnNamePlateUpdate += this.OnNamePlateUpdate;
+ namePlateGui.OnDataUpdate += this.OnDataUpdate;
+ namePlateGui.RequestRedraw();
+ this.updateCount = new Dictionary();
+ this.currentSubStep++;
+ break;
+
+ case SubStep.Confirm:
+ ImGui.Text("Click to redraw all visible nameplates");
+ if (ImGui.Button("Request redraw"))
+ namePlateGui.RequestRedraw();
+
+ ImGui.TextUnformatted("Can you see marker icons above nameplates, and does\n" +
+ "the update count increase when using request redraw?");
+
+ if (ImGui.Button("Yes"))
+ {
+ this.CleanUp();
+ return SelfTestStepResult.Pass;
+ }
+
+ ImGui.SameLine();
+
+ if (ImGui.Button("No"))
+ {
+ this.CleanUp();
+ return SelfTestStepResult.Fail;
+ }
+
+ break;
+ }
+
+ return SelfTestStepResult.Waiting;
+ }
+
+ ///
+ public void CleanUp()
+ {
+ var namePlateGui = Service.Get();
+ namePlateGui.OnNamePlateUpdate -= this.OnNamePlateUpdate;
+ namePlateGui.OnDataUpdate -= this.OnDataUpdate;
+ namePlateGui.RequestRedraw();
+ this.updateCount = null;
+ this.currentSubStep = SubStep.Start;
+ }
+
+ private void OnDataUpdate(INamePlateUpdateContext context, IReadOnlyList handlers)
+ {
+ foreach (var handler in handlers)
+ {
+ // Force nameplates to be visible
+ handler.VisibilityFlags |= 1;
+
+ // Set marker icon based on nameplate kind, and flicker when updating
+ if (handler.IsUpdating || context.IsFullUpdate)
+ {
+ handler.MarkerIconId = 66181 + (int)handler.NamePlateKind;
+ }
+ else
+ {
+ handler.MarkerIconId = 66161 + (int)handler.NamePlateKind;
+ }
+ }
+ }
+
+ private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList handlers)
+ {
+ foreach (var handler in handlers)
+ {
+ // Append GameObject address to name
+ var gameObjectAddress = handler.GameObject?.Address ?? 0;
+
+ handler.Name = handler.Name.Append(new SeString(new UIForegroundPayload(9)))
+ .Append($" (0x{gameObjectAddress:X})")
+ .Append(new SeString(UIForegroundPayload.UIForegroundOff));
+
+ // Track update count and set it as title
+ var count = this.updateCount!.GetValueOrDefault(handler.GameObjectId);
+ this.updateCount[handler.GameObjectId] = count + 1;
+
+ handler.TitleParts.Text = $"Updates: {count}";
+ handler.TitleParts.TextWrap = (new SeString(new UIForegroundPayload(43)),
+ new SeString(UIForegroundPayload.UIForegroundOff));
+ handler.DisplayTitle = true;
+ handler.IsPrefixTitle = false;
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs
index e3172d5c2..51c9b35f6 100644
--- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs
@@ -28,6 +28,7 @@ internal class SelfTestWindow : Window
new EnterTerritoryAgingStep(148, "Central Shroud"),
new ItemPayloadAgingStep(),
new ContextMenuAgingStep(),
+ new NamePlateAgingStep(),
new ActorTableAgingStep(),
new FateTableAgingStep(),
new AetheryteListAgingStep(),
@@ -82,6 +83,7 @@ internal class SelfTestWindow : Window
if (ImGuiComponents.IconButton(FontAwesomeIcon.StepForward))
{
this.stepResults.Add((SelfTestStepResult.NotRan, null));
+ this.steps[this.currentStep].CleanUp();
this.currentStep++;
this.lastTestStart = DateTimeOffset.Now;
diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs
index 2707f67df..d3298f61a 100644
--- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs
+++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs
@@ -1,8 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using CheapLoc;
+
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Colors;
+using Dalamud.Interface.Internal.ReShadeHandling;
using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Interface.Internal.Windows.Settings.Widgets;
using Dalamud.Interface.Utility;
@@ -11,28 +13,39 @@ using Dalamud.Utility;
namespace Dalamud.Interface.Internal.Windows.Settings.Tabs;
-[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")]
+[SuppressMessage(
+ "StyleCop.CSharp.DocumentationRules",
+ "SA1600:Elements should be documented",
+ Justification = "Internals")]
public class SettingsTabExperimental : SettingsTab
{
public override SettingsEntry[] Entries { get; } =
- {
+ [
new SettingsEntry(
Loc.Localize("DalamudSettingsPluginTest", "Get plugin testing builds"),
string.Format(
- Loc.Localize("DalamudSettingsPluginTestHint", "Receive testing prereleases for selected plugins.\nTo opt-in to testing builds for a plugin, you have to right click it in the \"{0}\" tab of the plugin installer and select \"{1}\"."),
+ Loc.Localize(
+ "DalamudSettingsPluginTestHint",
+ "Receive testing prereleases for selected plugins.\nTo opt-in to testing builds for a plugin, you have to right click it in the \"{0}\" tab of the plugin installer and select \"{1}\"."),
PluginCategoryManager.Locs.Group_Installed,
PluginInstallerWindow.Locs.PluginContext_TestingOptIn),
c => c.DoPluginTest,
(v, c) => c.DoPluginTest = v),
new HintSettingsEntry(
- Loc.Localize("DalamudSettingsPluginTestWarning", "Testing plugins may contain bugs or crash your game. Please only enable this if you are aware of the risks."),
+ Loc.Localize(
+ "DalamudSettingsPluginTestWarning",
+ "Testing plugins may contain bugs or crash your game. Please only enable this if you are aware of the risks."),
ImGuiColors.DalamudRed),
-
+
new GapSettingsEntry(5),
-
+
new SettingsEntry(
- Loc.Localize("DalamudSettingEnablePluginUIAdditionalOptions", "Add a button to the title bar of plugin windows to open additional options"),
- Loc.Localize("DalamudSettingEnablePluginUIAdditionalOptionsHint", "This will allow you to pin certain plugin windows, make them clickthrough or adjust their opacity.\nThis may not be supported by all of your plugins. Contact the plugin author if you want them to support this feature."),
+ Loc.Localize(
+ "DalamudSettingEnablePluginUIAdditionalOptions",
+ "Add a button to the title bar of plugin windows to open additional options"),
+ Loc.Localize(
+ "DalamudSettingEnablePluginUIAdditionalOptionsHint",
+ "This will allow you to pin certain plugin windows, make them clickthrough or adjust their opacity.\nThis may not be supported by all of your plugins. Contact the plugin author if you want them to support this feature."),
c => c.EnablePluginUiAdditionalOptions,
(v, c) => c.EnablePluginUiAdditionalOptions = v),
@@ -40,7 +53,9 @@ public class SettingsTabExperimental : SettingsTab
new ButtonSettingsEntry(
Loc.Localize("DalamudSettingsClearHidden", "Clear hidden plugins"),
- Loc.Localize("DalamudSettingsClearHiddenHint", "Restore plugins you have previously hidden from the plugin installer."),
+ Loc.Localize(
+ "DalamudSettingsClearHiddenHint",
+ "Restore plugins you have previously hidden from the plugin installer."),
() =>
{
Service.Get().HiddenPluginInternalName.Clear();
@@ -55,6 +70,47 @@ public class SettingsTabExperimental : SettingsTab
new ThirdRepoSettingsEntry(),
+ new GapSettingsEntry(5, true),
+
+ new EnumSettingsEntry(
+ Loc.Localize("DalamudSettingsReShadeHandlingMode", "ReShade handling mode"),
+ Loc.Localize(
+ "DalamudSettingsReShadeHandlingModeHint",
+ "You may try different options to work around problems you may encounter.\nRestart is required for changes to take effect."),
+ c => c.ReShadeHandlingMode,
+ (v, c) => c.ReShadeHandlingMode = v,
+ fallbackValue: ReShadeHandlingMode.Default,
+ warning: static rshm =>
+ rshm is ReShadeHandlingMode.UnwrapReShade or ReShadeHandlingMode.None ||
+ Service.Get().SwapChainHookMode == SwapChainHelper.HookMode.ByteCode
+ ? null
+ : "Current option will be ignored and no special ReShade handling will be done, because SwapChain vtable hook mode is set.")
+ {
+ FriendlyEnumNameGetter = x => x switch
+ {
+ ReShadeHandlingMode.Default => "Default",
+ ReShadeHandlingMode.UnwrapReShade => "Unwrap",
+ ReShadeHandlingMode.ReShadeAddonPresent => "ReShade Addon (present)",
+ ReShadeHandlingMode.ReShadeAddonReShadeOverlay => "ReShade Addon (reshade_overlay)",
+ ReShadeHandlingMode.HookReShadeDxgiSwapChainOnPresent => "Hook ReShade::DXGISwapChain::OnPresent",
+ ReShadeHandlingMode.None => "Do not handle",
+ _ => "",
+ },
+ },
+
+ /* // Making this a console command instead, for now
+ new GapSettingsEntry(5, true),
+
+ new EnumSettingsEntry(
+ Loc.Localize("DalamudSettingsSwapChainHookMode", "Swap chain hooking mode"),
+ Loc.Localize(
+ "DalamudSettingsSwapChainHookModeHint",
+ "Depending on addons aside from Dalamud you use, you may have to use different options for Dalamud and other addons to cooperate.\nRestart is required for changes to take effect."),
+ c => c.SwapChainHookMode,
+ (v, c) => c.SwapChainHookMode = v,
+ fallbackValue: SwapChainHelper.HookMode.ByteCode),
+ */
+
/* Disabling profiles after they've been enabled doesn't make much sense, at least not if the user has already created profiles.
new GapSettingsEntry(5, true),
@@ -64,7 +120,7 @@ public class SettingsTabExperimental : SettingsTab
c => c.ProfilesEnabled,
(v, c) => c.ProfilesEnabled = v),
*/
- };
+ ];
public override string Title => Loc.Localize("DalamudSettingsExperimental", "Experimental");
@@ -72,7 +128,9 @@ public class SettingsTabExperimental : SettingsTab
{
base.Draw();
- ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, "Total memory used by Dalamud & Plugins: " + Util.FormatBytes(GC.GetTotalMemory(false)));
+ ImGuiHelpers.SafeTextColoredWrapped(
+ ImGuiColors.DalamudGrey,
+ "Total memory used by Dalamud & Plugins: " + Util.FormatBytes(GC.GetTotalMemory(false)));
ImGuiHelpers.ScaledDummy(15);
}
}
diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs
index c991907ec..5e3648ac6 100644
--- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs
+++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs
@@ -15,7 +15,7 @@ public class SettingsTabGeneral : SettingsTab
new GapSettingsEntry(5),
- new SettingsEntry(
+ new EnumSettingsEntry(
Loc.Localize("DalamudSettingsChannel", "Dalamud Chat Channel"),
Loc.Localize("DalamudSettingsChannelHint", "Select the chat channel that is to be used for general Dalamud messages."),
c => c.GeneralChatType,
diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/EnumSettingsEntry{T}.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/EnumSettingsEntry{T}.cs
new file mode 100644
index 000000000..f40654542
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/EnumSettingsEntry{T}.cs
@@ -0,0 +1,175 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+using Dalamud.Configuration.Internal;
+
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Utility;
+using Dalamud.Interface.Utility.Raii;
+
+using ImGuiNET;
+
+namespace Dalamud.Interface.Internal.Windows.Settings.Widgets;
+
+[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")]
+internal sealed class EnumSettingsEntry : SettingsEntry
+ where T : struct, Enum
+{
+ private readonly LoadSettingDelegate load;
+ private readonly SaveSettingDelegate save;
+ private readonly Action? change;
+
+ private readonly T fallbackValue;
+
+ private T valueBacking;
+
+ public EnumSettingsEntry(
+ string name,
+ string description,
+ LoadSettingDelegate load,
+ SaveSettingDelegate save,
+ Action? change = null,
+ Func? warning = null,
+ Func? validity = null,
+ Func? visibility = null,
+ T fallbackValue = default)
+ {
+ this.load = load;
+ this.save = save;
+ this.change = change;
+ this.Name = name;
+ this.Description = description;
+ this.CheckWarning = warning;
+ this.CheckValidity = validity;
+ this.CheckVisibility = visibility;
+
+ this.fallbackValue = fallbackValue;
+ }
+
+ public delegate T LoadSettingDelegate(DalamudConfiguration config);
+
+ public delegate void SaveSettingDelegate(T value, DalamudConfiguration config);
+
+ public T Value
+ {
+ get => this.valueBacking;
+ set
+ {
+ if (Equals(value, this.valueBacking))
+ return;
+ this.valueBacking = value;
+ this.change?.Invoke(value);
+ }
+ }
+
+ public string Description { get; }
+
+ public Action>? CustomDraw { get; init; }
+
+ public Func? CheckValidity { get; init; }
+
+ public Func? CheckWarning { get; init; }
+
+ public Func? CheckVisibility { get; init; }
+
+ public Func FriendlyEnumNameGetter { get; init; } = x => x.ToString();
+
+ public Func FriendlyEnumDescriptionGetter { get; init; } = _ => string.Empty;
+
+ public override bool IsVisible => this.CheckVisibility?.Invoke() ?? true;
+
+ public override void Draw()
+ {
+ Debug.Assert(this.Name != null, "this.Name != null");
+
+ if (this.CustomDraw is not null)
+ {
+ this.CustomDraw.Invoke(this);
+ }
+ else
+ {
+ ImGuiHelpers.SafeTextWrapped(this.Name);
+
+ var idx = this.valueBacking;
+ var values = Enum.GetValues();
+
+ if (!values.Contains(idx))
+ {
+ idx = Enum.IsDefined(this.fallbackValue)
+ ? this.fallbackValue
+ : throw new InvalidOperationException("No fallback value for enum");
+ this.valueBacking = idx;
+ }
+
+ if (ImGui.BeginCombo($"###{this.Id.ToString()}", this.FriendlyEnumNameGetter(idx)))
+ {
+ foreach (var value in values)
+ {
+ if (ImGui.Selectable(this.FriendlyEnumNameGetter(value), idx.Equals(value)))
+ {
+ this.valueBacking = value;
+ }
+ }
+
+ ImGui.EndCombo();
+ }
+ }
+
+ using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey))
+ {
+ var desc = this.FriendlyEnumDescriptionGetter(this.valueBacking);
+ if (!string.IsNullOrWhiteSpace(desc))
+ {
+ ImGuiHelpers.SafeTextWrapped(desc);
+ ImGuiHelpers.ScaledDummy(2);
+ }
+
+ ImGuiHelpers.SafeTextWrapped(this.Description);
+ }
+
+ if (this.CheckValidity != null)
+ {
+ var validityMsg = this.CheckValidity.Invoke(this.Value);
+ this.IsValid = string.IsNullOrEmpty(validityMsg);
+
+ if (!this.IsValid)
+ {
+ using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
+ {
+ ImGui.Text(validityMsg);
+ }
+ }
+ }
+ else
+ {
+ this.IsValid = true;
+ }
+
+ var warningMessage = this.CheckWarning?.Invoke(this.Value);
+
+ if (warningMessage != null)
+ {
+ using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
+ {
+ ImGui.Text(warningMessage);
+ }
+ }
+ }
+
+ public override void Load()
+ {
+ this.valueBacking = this.load(Service.Get());
+
+ if (this.CheckValidity != null)
+ {
+ this.IsValid = this.CheckValidity(this.Value) == null;
+ }
+ else
+ {
+ this.IsValid = true;
+ }
+ }
+
+ public override void Save() => this.save(this.Value, Service.Get());
+}
diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/SettingsEntry{T}.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/SettingsEntry{T}.cs
index 2ac4187cf..cffe0a5da 100644
--- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/SettingsEntry{T}.cs
+++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/SettingsEntry{T}.cs
@@ -1,15 +1,13 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
-using System.Linq;
-using System.Numerics;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
-using Dalamud.Utility;
+
using ImGuiNET;
namespace Dalamud.Interface.Internal.Windows.Settings.Widgets;
@@ -22,7 +20,6 @@ internal sealed class SettingsEntry : SettingsEntry
private readonly Action? change;
private object? valueBacking;
- private object? fallbackValue;
public SettingsEntry(
string name,
@@ -32,8 +29,7 @@ internal sealed class SettingsEntry : SettingsEntry
Action? change = null,
Func? warning = null,
Func? validity = null,
- Func? visibility = null,
- object? fallbackValue = null)
+ Func? visibility = null)
{
this.load = load;
this.save = save;
@@ -43,8 +39,6 @@ internal sealed class SettingsEntry : SettingsEntry
this.CheckWarning = warning;
this.CheckValidity = validity;
this.CheckVisibility = visibility;
-
- this.fallbackValue = fallbackValue;
}
public delegate T? LoadSettingDelegate(DalamudConfiguration config);
@@ -118,34 +112,6 @@ internal sealed class SettingsEntry : SettingsEntry
this.change?.Invoke(this.Value);
}
}
- else if (type.IsEnum)
- {
- ImGuiHelpers.SafeTextWrapped(this.Name);
-
- var idx = (Enum)(this.valueBacking ?? 0);
- var values = Enum.GetValues(type);
- var descriptions =
- values.Cast().ToDictionary(x => x, x => x.GetAttribute() ?? new SettingsAnnotationAttribute(x.ToString(), string.Empty));
-
- if (!descriptions.ContainsKey(idx))
- {
- idx = (Enum)this.fallbackValue ?? throw new Exception("No fallback value for enum");
- this.valueBacking = idx;
- }
-
- if (ImGui.BeginCombo($"###{this.Id.ToString()}", descriptions[idx].FriendlyName))
- {
- foreach (Enum value in values)
- {
- if (ImGui.Selectable(descriptions[value].FriendlyName, idx.Equals(value)))
- {
- this.valueBacking = value;
- }
- }
-
- ImGui.EndCombo();
- }
- }
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey))
{
@@ -197,18 +163,3 @@ internal sealed class SettingsEntry : SettingsEntry
public override void Save() => this.save(this.Value, Service.Get());
}
-
-[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")]
-[AttributeUsage(AttributeTargets.Field)]
-internal class SettingsAnnotationAttribute : Attribute
-{
- public SettingsAnnotationAttribute(string friendlyName, string description)
- {
- this.FriendlyName = friendlyName;
- this.Description = description;
- }
-
- public string FriendlyName { get; set; }
-
- public string Description { get; set; }
-}
diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs
index e4f9d4adc..853556239 100644
--- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs
@@ -496,7 +496,7 @@ internal sealed partial class FontAtlasFactory
$"{nameof(FontAtlasAutoRebuildMode.Async)}.");
}
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
try
{
var rebuildIndex = Interlocked.Increment(ref this.buildIndex);
diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
index 0e26145f0..b84a857da 100644
--- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
@@ -242,7 +242,7 @@ internal abstract class FontHandle : IFontHandle
if (this.Available)
return Task.FromResult(this);
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
this.ImFontChanged += OnImFontChanged;
this.Disposed += OnDisposed;
if (this.Available)
diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs b/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs
index 92152b1fb..156ffa56f 100644
--- a/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs
+++ b/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs
@@ -45,6 +45,10 @@ internal sealed partial class TextureManager
ISharedImmediateTexture ITextureProvider.GetFromFile(FileInfo file) =>
this.Shared.GetFromFile(file);
+ ///
+ public ISharedImmediateTexture GetFromFileAbsolute(string fullPath) =>
+ this.Shared.GetFromFileAbsolute(fullPath);
+
///
ISharedImmediateTexture ITextureProvider.GetFromManifestResource(Assembly assembly, string name) =>
this.Shared.GetFromManifestResource(assembly, name);
@@ -141,7 +145,12 @@ internal sealed partial class TextureManager
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SharedImmediateTexture.PureImpl GetFromFile(FileInfo file) =>
- this.fileDict.GetOrAdd(file.FullName, FileSystemSharedImmediateTexture.CreatePlaceholder)
+ this.GetFromFileAbsolute(file.FullName);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public SharedImmediateTexture.PureImpl GetFromFileAbsolute(string fullPath) =>
+ this.fileDict.GetOrAdd(fullPath, FileSystemSharedImmediateTexture.CreatePlaceholder)
.PublicUseInstance;
///
diff --git a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs
index 27f97168e..68e2dde47 100644
--- a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs
+++ b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs
@@ -313,6 +313,14 @@ internal sealed class TextureManagerPluginScoped
return shared;
}
+ ///
+ public ISharedImmediateTexture GetFromFileAbsolute(string fullPath)
+ {
+ var shared = this.ManagerOrThrow.Shared.GetFromFileAbsolute(fullPath);
+ shared.AddOwnerPlugin(this.plugin);
+ return shared;
+ }
+
///
public ISharedImmediateTexture GetFromManifestResource(Assembly assembly, string name)
{
diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs
index ad3188925..6e21bc0e8 100644
--- a/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs
+++ b/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs
@@ -24,7 +24,8 @@ internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDispos
private readonly string? debugName;
private readonly LocalPlugin? ownerPlugin;
private readonly CancellationToken cancellationToken;
- private readonly TaskCompletionSource firstUpdateTaskCompletionSource = new();
+ private readonly TaskCompletionSource firstUpdateTaskCompletionSource =
+ new(TaskCreationOptions.RunContinuationsAsynchronously);
private ImGuiViewportTextureArgs args;
private D3D11_TEXTURE2D_DESC desc;
diff --git a/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs
index 2ff42bc2a..a6584f9aa 100644
--- a/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs
+++ b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs
@@ -59,7 +59,7 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
{
var first = true;
var encoders = textureManager.Wic.GetSupportedEncoderInfos().ToList();
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
Service.Get().Draw += DrawChoices;
encoder = await tcs.Task;
@@ -108,7 +108,7 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
string path;
{
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
this.fileDialogManager.SaveFileDialog(
"Save texture...",
$"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}",
diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs
index 673dba29b..a8eacb02d 100644
--- a/Dalamud/IoC/Internal/ServiceContainer.cs
+++ b/Dalamud/IoC/Internal/ServiceContainer.cs
@@ -3,6 +3,7 @@ using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
+using System.Threading;
using System.Threading.Tasks;
using Dalamud.Logging.Internal;
@@ -83,53 +84,57 @@ internal class ServiceContainer : IServiceProvider, IServiceType
/// Scoped objects to be included in the constructor.
/// The scope to be used to create scoped services.
/// The created object.
- public async Task
ISharedImmediateTexture GetFromFile(FileInfo file);
+ /// Gets a shared texture corresponding to the given file on the filesystem.
+ /// The file on the filesystem to load. Requires a full path.
+ /// The shared texture that you may use to obtain the loaded texture wrap and load states.
+ ///
+ /// This function does not throw exceptions.
+ /// Caching the returned object is not recommended. Performance benefit will be minimal.
+ ///
+ ISharedImmediateTexture GetFromFileAbsolute(string fullPath);
+
/// Gets a shared texture corresponding to the given file of the assembly manifest resources.
/// The assembly containing manifest resources.
/// The case-sensitive name of the manifest resource being requested.
diff --git a/Dalamud/Service/LoadingDialog.cs b/Dalamud/Service/LoadingDialog.cs
index 64af02171..f788ffb71 100644
--- a/Dalamud/Service/LoadingDialog.cs
+++ b/Dalamud/Service/LoadingDialog.cs
@@ -1,34 +1,46 @@
-using System.Drawing;
+using System.Collections.Concurrent;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Text;
using System.Threading;
-using System.Threading.Tasks;
-using System.Windows.Forms;
+
+using CheapLoc;
using Dalamud.Plugin.Internal;
using Dalamud.Utility;
-using Windows.Win32.Foundation;
-using Windows.Win32.UI.WindowsAndMessaging;
+
+using Serilog;
+using Serilog.Events;
+
+using TerraFX.Interop.Windows;
+
+using static TerraFX.Interop.Windows.TASKDIALOG_FLAGS;
+using static TerraFX.Interop.Windows.Windows;
namespace Dalamud;
///
/// Class providing an early-loading dialog.
///
-internal class LoadingDialog
+[SuppressMessage(
+ "StyleCop.CSharp.LayoutRules",
+ "SA1519:Braces should not be omitted from multi-line child statement",
+ Justification = "Multiple fixed blocks")]
+internal sealed unsafe class LoadingDialog
{
- // TODO: We can't localize any of what's in here at the moment, because Localization is an EarlyLoadedService.
-
- private static int wasGloballyHidden = 0;
-
+ private readonly RollingList logs = new(20);
+
private Thread? thread;
- private TaskDialogButton? inProgressHideButton;
- private TaskDialogPage? page;
- private bool canHide;
- private State currentState = State.LoadingDalamud;
+ private HWND hwndTaskDialog;
private DateTime firstShowTime;
-
+ private State currentState = State.LoadingDalamud;
+ private bool canHide;
+
///
/// Enum representing the state of the dialog.
///
@@ -38,18 +50,25 @@ internal class LoadingDialog
/// Show a message stating that Dalamud is currently loading.
///
LoadingDalamud,
-
+
///
/// Show a message stating that Dalamud is currently loading plugins.
///
LoadingPlugins,
-
+
///
/// Show a message stating that Dalamud is currently updating plugins.
///
AutoUpdatePlugins,
}
-
+
+ /// Gets the queue where log entries that are not processed yet are stored.
+ public static ConcurrentQueue<(string Line, LogEvent LogEvent)> NewLogEntries { get; } = new();
+
+ /// Gets a value indicating whether the initial Dalamud loading dialog will not show again until next
+ /// game restart.
+ public static bool IsGloballyHidden { get; private set; }
+
///
/// Gets or sets the current state of the dialog.
///
@@ -58,13 +77,16 @@ internal class LoadingDialog
get => this.currentState;
set
{
+ if (this.currentState == value)
+ return;
+
this.currentState = value;
- this.UpdatePage();
+ this.UpdateMainInstructionText();
}
}
-
+
///
- /// Gets or sets a value indicating whether or not the dialog can be hidden by the user.
+ /// Gets or sets a value indicating whether the dialog can be hidden by the user.
///
/// Thrown if called before the dialog has been created.
public bool CanHide
@@ -72,8 +94,11 @@ internal class LoadingDialog
get => this.canHide;
set
{
+ if (this.canHide == value)
+ return;
+
this.canHide = value;
- this.UpdatePage();
+ this.UpdateButtonEnabled();
}
}
@@ -82,19 +107,19 @@ internal class LoadingDialog
///
public void Show()
{
- if (Volatile.Read(ref wasGloballyHidden) == 1)
+ if (IsGloballyHidden)
return;
-
+
if (this.thread?.IsAlive == true)
return;
-
+
this.thread = new Thread(this.ThreadStart)
{
Name = "Dalamud Loading Dialog",
};
this.thread.SetApartmentState(ApartmentState.STA);
this.thread.Start();
-
+
this.firstShowTime = DateTime.Now;
}
@@ -103,150 +128,287 @@ internal class LoadingDialog
///
public void HideAndJoin()
{
- if (this.thread == null || !this.thread.IsAlive)
+ IsGloballyHidden = true;
+ if (this.thread?.IsAlive is not true)
return;
-
- this.inProgressHideButton?.PerformClick();
- this.thread!.Join();
+
+ SendMessageW(this.hwndTaskDialog, WM.WM_CLOSE, default, default);
+ this.thread.Join();
}
- private void UpdatePage()
+ private void UpdateMainInstructionText()
{
- if (this.page == null)
+ if (this.hwndTaskDialog == default)
return;
- this.page.Heading = this.currentState switch
+ fixed (void* pszText = this.currentState switch
+ {
+ State.LoadingDalamud => Loc.Localize(
+ "LoadingDialogMainInstructionLoadingDalamud",
+ "Dalamud is loading..."),
+ State.LoadingPlugins => Loc.Localize(
+ "LoadingDialogMainInstructionLoadingPlugins",
+ "Waiting for plugins to load..."),
+ State.AutoUpdatePlugins => Loc.Localize(
+ "LoadingDialogMainInstructionAutoUpdatePlugins",
+ "Updating plugins..."),
+ _ => string.Empty, // should not happen
+ })
{
- State.LoadingDalamud => "Dalamud is loading...",
- State.LoadingPlugins => "Waiting for plugins to load...",
- State.AutoUpdatePlugins => "Updating plugins...",
- _ => throw new ArgumentOutOfRangeException(),
- };
+ SendMessageW(
+ this.hwndTaskDialog,
+ (uint)TASKDIALOG_MESSAGES.TDM_SET_ELEMENT_TEXT,
+ (WPARAM)(int)TASKDIALOG_ELEMENTS.TDE_MAIN_INSTRUCTION,
+ (LPARAM)pszText);
+ }
+ }
- var context = string.Empty;
- if (this.currentState == State.LoadingPlugins)
+ private void UpdateContentText()
+ {
+ if (this.hwndTaskDialog == default)
+ return;
+
+ var contentBuilder = new StringBuilder(
+ Loc.Localize(
+ "LoadingDialogContentInfo",
+ "Some of the plugins you have installed through Dalamud are taking a long time to load.\n" +
+ "This is likely normal, please wait a little while longer."));
+
+ if (this.CurrentState == State.LoadingPlugins)
{
- context = "\nPreparing...";
-
var tracker = Service.GetNullable()?.StartupLoadTracking;
if (tracker != null)
{
- var nameString = tracker.GetPendingInternalNames()
- .Select(x => tracker.GetPublicName(x))
- .Where(x => x != null)
- .Aggregate(string.Empty, (acc, x) => acc + x + ", ");
-
+ var nameString = string.Join(
+ ", ",
+ tracker.GetPendingInternalNames()
+ .Select(x => tracker.GetPublicName(x))
+ .Where(x => x != null));
+
if (!nameString.IsNullOrEmpty())
- context = $"\nWaiting for: {nameString[..^2]}";
+ {
+ contentBuilder
+ .AppendLine()
+ .AppendLine()
+ .Append(
+ string.Format(
+ Loc.Localize("LoadingDialogContentCurrentPlugin", "Waiting for: {0}"),
+ nameString));
+ }
}
}
-
+
// Add some text if loading takes more than a few minutes
if (DateTime.Now - this.firstShowTime > TimeSpan.FromMinutes(3))
- context += "\nIt's been a while now. Please report this issue on our Discord server.";
-
- this.page.Text = this.currentState switch
{
- State.LoadingDalamud => "Please wait while Dalamud loads...",
- State.LoadingPlugins => "Please wait while Dalamud loads plugins...",
- State.AutoUpdatePlugins => "Please wait while Dalamud updates your plugins...",
- _ => throw new ArgumentOutOfRangeException(),
-#pragma warning disable SA1513
- } + context;
-#pragma warning restore SA1513
-
- this.inProgressHideButton!.Enabled = this.canHide;
- }
-
- private async Task DialogStatePeriodicUpdate(CancellationToken token)
- {
- using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(50));
- while (!token.IsCancellationRequested)
- {
- await timer.WaitForNextTickAsync(token);
- this.UpdatePage();
+ contentBuilder
+ .AppendLine()
+ .AppendLine()
+ .Append(
+ Loc.Localize(
+ "LoadingDialogContentTakingTooLong",
+ "It's been a while now. Please report this issue on our Discord server."));
}
+
+ fixed (void* pszText = contentBuilder.ToString())
+ {
+ SendMessageW(
+ this.hwndTaskDialog,
+ (uint)TASKDIALOG_MESSAGES.TDM_SET_ELEMENT_TEXT,
+ (WPARAM)(int)TASKDIALOG_ELEMENTS.TDE_CONTENT,
+ (LPARAM)pszText);
+ }
+ }
+
+ private void UpdateExpandedInformation()
+ {
+ const int maxCharactersPerLine = 80;
+
+ if (NewLogEntries.IsEmpty)
+ return;
+
+ var sb = new StringBuilder();
+ while (NewLogEntries.TryDequeue(out var e))
+ {
+ var t = e.Line.AsSpan();
+ var first = true;
+ while (!t.IsEmpty)
+ {
+ var i = t.IndexOfAny('\r', '\n');
+ var line = i == -1 ? t : t[..i];
+ t = i == -1 ? ReadOnlySpan.Empty : t[(i + 1)..];
+ if (line.IsEmpty)
+ continue;
+
+ sb.Clear();
+ if (first)
+ sb.Append($"{e.LogEvent.Timestamp:HH:mm:ss} | ");
+ else
+ sb.Append(" | ");
+ first = false;
+ if (line.Length < maxCharactersPerLine)
+ sb.Append(line);
+ else
+ sb.Append($"{line[..(maxCharactersPerLine - 3)]}...");
+ this.logs.Add(sb.ToString());
+ }
+ }
+
+ sb.Clear();
+ foreach (var l in this.logs)
+ sb.AppendLine(l);
+
+ fixed (void* pszText = sb.ToString())
+ {
+ SendMessageW(
+ this.hwndTaskDialog,
+ (uint)TASKDIALOG_MESSAGES.TDM_SET_ELEMENT_TEXT,
+ (WPARAM)(int)TASKDIALOG_ELEMENTS.TDE_EXPANDED_INFORMATION,
+ (LPARAM)pszText);
+ }
+ }
+
+ private void UpdateButtonEnabled()
+ {
+ if (this.hwndTaskDialog == default)
+ return;
+
+ SendMessageW(this.hwndTaskDialog, (uint)TASKDIALOG_MESSAGES.TDM_ENABLE_BUTTON, IDOK, this.canHide ? 1 : 0);
+ }
+
+ private HRESULT TaskDialogCallback(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam)
+ {
+ switch ((TASKDIALOG_NOTIFICATIONS)msg)
+ {
+ case TASKDIALOG_NOTIFICATIONS.TDN_CREATED:
+ this.hwndTaskDialog = hwnd;
+
+ this.UpdateMainInstructionText();
+ this.UpdateContentText();
+ this.UpdateExpandedInformation();
+ this.UpdateButtonEnabled();
+ SendMessageW(hwnd, (int)TASKDIALOG_MESSAGES.TDM_SET_PROGRESS_BAR_MARQUEE, 1, 0);
+
+ // Bring to front
+ SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, SWP.SWP_NOSIZE | SWP.SWP_NOMOVE);
+ SetWindowPos(hwnd, HWND.HWND_NOTOPMOST, 0, 0, 0, 0, SWP.SWP_NOSIZE | SWP.SWP_NOMOVE);
+ ShowWindow(hwnd, SW.SW_SHOW);
+ SetForegroundWindow(hwnd);
+ SetFocus(hwnd);
+ SetActiveWindow(hwnd);
+ return S.S_OK;
+
+ case TASKDIALOG_NOTIFICATIONS.TDN_DESTROYED:
+ this.hwndTaskDialog = default;
+ return S.S_OK;
+
+ case TASKDIALOG_NOTIFICATIONS.TDN_TIMER:
+ this.UpdateContentText();
+ this.UpdateExpandedInformation();
+ return S.S_OK;
+ }
+
+ return S.S_OK;
}
private void ThreadStart()
{
- Application.EnableVisualStyles();
-
- this.inProgressHideButton = new TaskDialogButton("Hide", this.canHide);
-
// We don't have access to the asset service here.
var workingDirectory = Service.Get().StartInfo.WorkingDirectory;
- TaskDialogIcon? dialogIcon = null;
- if (!workingDirectory.IsNullOrEmpty())
+ using var extractedIcon =
+ string.IsNullOrEmpty(workingDirectory)
+ ? null
+ : Icon.ExtractAssociatedIcon(Path.Combine(workingDirectory, "Dalamud.Injector.exe"));
+
+ fixed (void* pszEmpty = "-")
+ fixed (void* pszWindowTitle = "Dalamud")
+ fixed (void* pszDalamudBoot = "Dalamud.Boot.dll")
+ fixed (void* pszThemesManifestResourceName = "RT_MANIFEST_THEMES")
+ fixed (void* pszHide = Loc.Localize("LoadingDialogHide", "Hide"))
+ fixed (void* pszShowLatestLogs = Loc.Localize("LoadingDialogShowLatestLogs", "Show Latest Logs"))
+ fixed (void* pszHideLatestLogs = Loc.Localize("LoadingDialogHideLatestLogs", "Hide Latest Logs"))
{
- var extractedIcon = Icon.ExtractAssociatedIcon(Path.Combine(workingDirectory, "Dalamud.Injector.exe"));
- if (extractedIcon != null)
+ var taskDialogButton = new TASKDIALOG_BUTTON
{
- dialogIcon = new TaskDialogIcon(extractedIcon);
+ nButtonID = IDOK,
+ pszButtonText = (ushort*)pszHide,
+ };
+ var taskDialogConfig = new TASKDIALOGCONFIG
+ {
+ cbSize = (uint)sizeof(TASKDIALOGCONFIG),
+ hwndParent = default,
+ hInstance = (HINSTANCE)Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().ManifestModule),
+ dwFlags = (int)TDF_CAN_BE_MINIMIZED |
+ (int)TDF_SHOW_MARQUEE_PROGRESS_BAR |
+ (int)TDF_EXPAND_FOOTER_AREA |
+ (int)TDF_CALLBACK_TIMER |
+ (extractedIcon is null ? 0 : (int)TDF_USE_HICON_MAIN),
+ dwCommonButtons = 0,
+ pszWindowTitle = (ushort*)pszWindowTitle,
+ pszMainIcon = extractedIcon is null ? TD.TD_INFORMATION_ICON : (ushort*)extractedIcon.Handle,
+ pszMainInstruction = null,
+ pszContent = null,
+ cButtons = 1,
+ pButtons = &taskDialogButton,
+ nDefaultButton = IDOK,
+ cRadioButtons = 0,
+ pRadioButtons = null,
+ nDefaultRadioButton = 0,
+ pszVerificationText = null,
+ pszExpandedInformation = (ushort*)pszEmpty,
+ pszExpandedControlText = (ushort*)pszShowLatestLogs,
+ pszCollapsedControlText = (ushort*)pszHideLatestLogs,
+ pszFooterIcon = null,
+ pszFooter = null,
+ pfCallback = &HResultFuncBinder,
+ lpCallbackData = 0,
+ cxWidth = 360,
+ };
+
+ HANDLE hActCtx = default;
+ GCHandle gch = default;
+ nuint cookie = 0;
+ try
+ {
+ var actctx = new ACTCTXW
+ {
+ cbSize = (uint)sizeof(ACTCTXW),
+ dwFlags = ACTCTX_FLAG_HMODULE_VALID | ACTCTX_FLAG_RESOURCE_NAME_VALID,
+ lpResourceName = (ushort*)pszThemesManifestResourceName,
+ hModule = GetModuleHandleW((ushort*)pszDalamudBoot),
+ };
+ hActCtx = CreateActCtxW(&actctx);
+ if (hActCtx == default)
+ throw new Win32Exception("CreateActCtxW failure.");
+
+ if (!ActivateActCtx(hActCtx, &cookie))
+ throw new Win32Exception("ActivateActCtx failure.");
+
+ gch = GCHandle.Alloc((Func)this.TaskDialogCallback);
+ taskDialogConfig.lpCallbackData = GCHandle.ToIntPtr(gch);
+ TaskDialogIndirect(&taskDialogConfig, null, null, null).ThrowOnError();
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "TaskDialogIndirect failure.");
+ }
+ finally
+ {
+ if (gch.IsAllocated)
+ gch.Free();
+ if (cookie != 0)
+ DeactivateActCtx(0, cookie);
+ ReleaseActCtx(hActCtx);
}
}
- dialogIcon ??= TaskDialogIcon.Information;
- this.page = new TaskDialogPage
- {
- ProgressBar = new TaskDialogProgressBar(TaskDialogProgressBarState.Marquee),
- Caption = "Dalamud",
- Icon = dialogIcon,
- Buttons = { this.inProgressHideButton },
- AllowMinimize = false,
- AllowCancel = false,
- Expander = new TaskDialogExpander
- {
- CollapsedButtonText = "What does this mean?",
- ExpandedButtonText = "What does this mean?",
- Text = "Some of the plugins you have installed through Dalamud are taking a long time to load.\n" +
- "This is likely normal, please wait a little while longer.",
- },
- SizeToContent = true,
- };
-
- this.UpdatePage();
+ IsGloballyHidden = true;
- // Call private TaskDialog ctor
- var ctor = typeof(TaskDialog).GetConstructor(
- BindingFlags.Instance | BindingFlags.NonPublic,
- null,
- Array.Empty(),
- null);
+ return;
- var taskDialog = (TaskDialog)ctor!.Invoke(Array.Empty())!;
-
- this.page.Created += (_, _) =>
- {
- var hwnd = new HWND(taskDialog.Handle);
-
- // Bring to front
- Windows.Win32.PInvoke.SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0,
- SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOMOVE);
- Windows.Win32.PInvoke.SetWindowPos(hwnd, HWND.HWND_NOTOPMOST, 0, 0, 0, 0,
- SET_WINDOW_POS_FLAGS.SWP_SHOWWINDOW | SET_WINDOW_POS_FLAGS.SWP_NOSIZE |
- SET_WINDOW_POS_FLAGS.SWP_NOMOVE);
- Windows.Win32.PInvoke.SetForegroundWindow(hwnd);
- Windows.Win32.PInvoke.SetFocus(hwnd);
- Windows.Win32.PInvoke.SetActiveWindow(hwnd);
- };
-
- // Call private "ShowDialogInternal"
- var showDialogInternal = typeof(TaskDialog).GetMethod(
- "ShowDialogInternal",
- BindingFlags.Instance | BindingFlags.NonPublic,
- null,
- [typeof(IntPtr), typeof(TaskDialogPage), typeof(TaskDialogStartupLocation)],
- null);
-
- var cts = new CancellationTokenSource();
- _ = this.DialogStatePeriodicUpdate(cts.Token);
-
- showDialogInternal!.Invoke(
- taskDialog,
- [IntPtr.Zero, this.page, TaskDialogStartupLocation.CenterScreen]);
-
- Interlocked.Exchange(ref wasGloballyHidden, 1);
- cts.Cancel();
+ [UnmanagedCallersOnly]
+ static HRESULT HResultFuncBinder(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam, nint user) =>
+ ((Func)GCHandle.FromIntPtr(user).Target!)
+ .Invoke(hwnd, msg, wParam, lParam);
}
}
diff --git a/Dalamud/Service/ServiceManager.cs b/Dalamud/Service/ServiceManager.cs
index 7483b0a27..29016bc69 100644
--- a/Dalamud/Service/ServiceManager.cs
+++ b/Dalamud/Service/ServiceManager.cs
@@ -44,7 +44,9 @@ internal static class ServiceManager
private static readonly List LoadedServices = new();
#endif
- private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new();
+ private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource =
+ new(TaskCreationOptions.RunContinuationsAsynchronously);
+
private static readonly CancellationTokenSource UnloadCancellationTokenSource = new();
private static ManualResetEvent unloadResetEvent = new(false);
@@ -126,7 +128,13 @@ internal static class ServiceManager
/// Instance of .
/// Instance of .
/// Instance of .
- public static void InitializeProvidedServices(Dalamud dalamud, ReliableFileStorage fs, DalamudConfiguration configuration, TargetSigScanner scanner)
+ /// Instance of .
+ public static void InitializeProvidedServices(
+ Dalamud dalamud,
+ ReliableFileStorage fs,
+ DalamudConfiguration configuration,
+ TargetSigScanner scanner,
+ Localization localization)
{
#if DEBUG
lock (LoadedServices)
@@ -136,6 +144,7 @@ internal static class ServiceManager
ProvideService(configuration);
ProvideService(new ServiceContainer());
ProvideService(scanner);
+ ProvideService(localization);
}
return;
@@ -152,6 +161,7 @@ internal static class ServiceManager
ProvideService(configuration);
ProvideService(new ServiceContainer());
ProvideService(scanner);
+ ProvideService(localization);
return;
void ProvideService(T service) where T : IServiceType => Service.Provide(service);
@@ -242,19 +252,20 @@ internal static class ServiceManager
try
{
// Wait for all blocking constructors to complete first.
- await WaitWithTimeoutConsent(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x]),
+ await WaitWithTimeoutConsent(
+ blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x]),
LoadingDialog.State.LoadingDalamud);
// 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,
+ await WaitWithTimeoutConsent(
+ blockerTasks,
LoadingDialog.State.LoadingPlugins);
Log.Verbose("=============== BLOCKINGSERVICES & TASKS INITIALIZED ===============");
Timings.Event("BlockingServices Initialized");
BlockingServicesLoadedTaskCompletionSource.SetResult();
- loadingDialog.HideAndJoin();
}
catch (Exception e)
{
@@ -269,11 +280,16 @@ internal static class ServiceManager
Log.Error(e, "Failed resolving blocking services");
}
+ finally
+ {
+ loadingDialog.HideAndJoin();
+ }
return;
async Task WaitWithTimeoutConsent(IEnumerable tasksEnumerable, LoadingDialog.State state)
{
+ loadingDialog.CurrentState = state;
var tasks = tasksEnumerable.AsReadOnlyCollection();
if (tasks.Count == 0)
return;
@@ -286,7 +302,6 @@ internal static class ServiceManager
{
loadingDialog.Show();
loadingDialog.CanHide = true;
- loadingDialog.CurrentState = state;
}
}
}).ConfigureAwait(false);
diff --git a/Dalamud/Service/Service{T}.cs b/Dalamud/Service/Service{T}.cs
index 57acd2ccf..b4bfff917 100644
--- a/Dalamud/Service/Service{T}.cs
+++ b/Dalamud/Service/Service{T}.cs
@@ -332,7 +332,7 @@ internal static class Service where T : IServiceType
break;
}
- instanceTcs = new TaskCompletionSource();
+ instanceTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
instanceTcs.SetException(new UnloadedException());
}
diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs
index 514785823..0109339fe 100644
--- a/Dalamud/Storage/Assets/DalamudAssetManager.cs
+++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs
@@ -85,7 +85,7 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud
.Where(x => x is not DalamudAsset.Empty4X4)
.Where(x => x.GetAttribute()?.Required is false)
.Select(this.CreateStreamAsync)
- .Select(x => x.ToContentDisposedTask()))
+ .Select(x => x.ToContentDisposedTask(true)))
.ContinueWith(r => Log.Verbose($"Optional assets load state: {r}"));
}
diff --git a/Dalamud/Support/BugBait.cs b/Dalamud/Support/BugBait.cs
index 22628303e..c82e5e652 100644
--- a/Dalamud/Support/BugBait.cs
+++ b/Dalamud/Support/BugBait.cs
@@ -36,7 +36,7 @@ internal static class BugBait
Reporter = reporter,
Name = plugin.InternalName,
Version = isTesting ? plugin.TestingAssemblyVersion?.ToString() : plugin.AssemblyVersion.ToString(),
- DalamudHash = Util.GetGitHash(),
+ DalamudHash = Util.GetScmVersion(),
};
if (includeException)
diff --git a/Dalamud/Support/CurrentProcessModules.cs b/Dalamud/Support/CurrentProcessModules.cs
index bc0abcf50..b89d2eb63 100644
--- a/Dalamud/Support/CurrentProcessModules.cs
+++ b/Dalamud/Support/CurrentProcessModules.cs
@@ -1,145 +1,40 @@
-using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.InteropServices;
-using System.Threading;
-using System.Threading.Tasks;
using Serilog;
-using TerraFX.Interop.Windows;
-
namespace Dalamud.Support;
/// Tracks the loaded process modules.
-[ServiceManager.EarlyLoadedService]
-internal sealed unsafe partial class CurrentProcessModules : IInternalDisposableService
+internal static unsafe partial class CurrentProcessModules
{
- private static readonly ConcurrentQueue LogQueue = new();
- private static readonly SemaphoreSlim LogSemaphore = new(0);
-
- private static Process? process;
- private static nint cookie;
-
- private readonly CancellationTokenSource logTaskStop = new();
- private readonly Task logTask;
-
- [ServiceManager.ServiceConstructor]
- private CurrentProcessModules()
- {
- var res = LdrRegisterDllNotification(0, &DllNotificationCallback, 0, out cookie);
- if (res != STATUS.STATUS_SUCCESS)
- {
- Log.Error("{what}: LdrRegisterDllNotification failure: 0x{err}", nameof(CurrentProcessModules), res);
- cookie = 0;
- this.logTask = Task.CompletedTask;
- return;
- }
-
- this.logTask = Task.Factory.StartNew(
- () =>
- {
- while (!this.logTaskStop.IsCancellationRequested)
- {
- LogSemaphore.Wait();
- while (LogQueue.TryDequeue(out var log))
- Log.Verbose(log);
- }
- },
- this.logTaskStop.Token,
- TaskCreationOptions.LongRunning,
- TaskScheduler.Default);
- }
-
- private enum LdrDllNotificationReason : uint
- {
- Loaded = 1,
- Unloaded = 2,
- }
+ private static ProcessModuleCollection? moduleCollection;
/// Gets all the loaded modules, up to date.
public static ProcessModuleCollection ModuleCollection
{
get
{
- if (cookie == 0)
+ ref var t = ref *GetDllChangedStorage();
+ if (t != 0)
{
- // This service has not been initialized; return a fresh copy without storing it.
- return Process.GetCurrentProcess().Modules;
+ t = 0;
+ moduleCollection = null;
+ Log.Verbose("{what}: Fetching fresh copy of current process modules.", nameof(CurrentProcessModules));
}
- if (process is null)
- Log.Verbose("{what}: Fetchling fresh copy of current process modules.", nameof(CurrentProcessModules));
-
- return (process ??= Process.GetCurrentProcess()).Modules;
+ try
+ {
+ return moduleCollection ??= Process.GetCurrentProcess().Modules;
+ }
+ catch (Exception e)
+ {
+ Log.Verbose(e, "{what}: Failed to fetch module list.", nameof(CurrentProcessModules));
+ return new([]);
+ }
}
}
- ///
- void IInternalDisposableService.DisposeService()
- {
- if (Interlocked.Exchange(ref cookie, 0) is var copy and not 0)
- LdrUnregisterDllNotification(copy);
- if (!this.logTask.IsCompleted)
- {
- this.logTaskStop.Cancel();
- LogSemaphore.Release();
- this.logTask.Wait();
- }
- }
-
- [UnmanagedCallersOnly]
- private static void DllNotificationCallback(
- LdrDllNotificationReason reason,
- LdrDllNotificationData* data,
- nint context) => process = null;
-
- ///
- /// Registers for notification when a DLL is first loaded.
- /// This notification occurs before dynamic linking takes place.
- /// Docs.
- ///
- /// This parameter must be zero.
- /// A pointer to a callback function to call when the DLL is loaded.
- /// A pointer to context data for the callback function.
- /// A pointer to a variable to receive an identifier for the callback function.
- /// This identifier is used to unregister the notification callback function.
- /// Returns an NTSTATUS or error code.
- [LibraryImport("ntdll.dll", SetLastError = true)]
- private static partial int LdrRegisterDllNotification(
- uint flags,
- delegate* unmanaged
- notificationFunction,
- nint context,
- out nint cookie);
-
- ///
- /// Cancels DLL load notification previously registered by calling the LdrRegisterDllNotification function.
- ///
- /// Docs.
- ///
- /// A pointer to the callback identifier received from the LdrRegisterDllNotification call
- /// that registered for notification.
- ///
- /// Returns an NTSTATUS or error code.
- [LibraryImport("ntdll.dll", SetLastError = true)]
- private static partial int LdrUnregisterDllNotification(nint cookie);
-
- [StructLayout(LayoutKind.Sequential)]
- private struct LdrDllNotificationData
- {
- /// Reserved.
- public uint Flags;
-
- /// The full path name of the DLL module.
- public UNICODE_STRING* FullDllName;
-
- /// The base file name of the DLL module.
- public UNICODE_STRING* BaseDllName;
-
- /// A pointer to the base address for the DLL in memory.
- public nint DllBase;
-
- /// The size of the DLL image, in bytes.
- public uint SizeOfImage;
- }
+ [LibraryImport("Dalamud.Boot.dll")]
+ private static partial int* GetDllChangedStorage();
}
diff --git a/Dalamud/Support/Troubleshooting.cs b/Dalamud/Support/Troubleshooting.cs
index 3ba088c66..4af8d5ffc 100644
--- a/Dalamud/Support/Troubleshooting.cs
+++ b/Dalamud/Support/Troubleshooting.cs
@@ -69,8 +69,8 @@ public static class Troubleshooting
LoadedPlugins = pluginManager?.InstalledPlugins?.Select(x => x.Manifest as LocalPluginManifest)?.OrderByDescending(x => x.InternalName).ToArray(),
PluginStates = pluginManager?.InstalledPlugins?.Where(x => !x.IsDev).ToDictionary(x => x.Manifest.InternalName, x => x.IsBanned ? "Banned" : x.State.ToString()),
EverStartedLoadingPlugins = pluginManager?.InstalledPlugins.Where(x => x.HasEverStartedLoad).Select(x => x.InternalName).ToList(),
- DalamudVersion = Util.AssemblyVersion,
- DalamudGitHash = Util.GetGitHash(),
+ DalamudVersion = Util.GetScmVersion(),
+ DalamudGitHash = Util.GetGitHash() ?? "Unknown",
GameVersion = startInfo.GameVersion?.ToString() ?? "Unknown",
Language = startInfo.Language.ToString(),
BetaKey = configuration.DalamudBetaKey,
diff --git a/Dalamud/Utility/AsyncUtils.cs b/Dalamud/Utility/AsyncUtils.cs
index 9533f2ab0..4de561275 100644
--- a/Dalamud/Utility/AsyncUtils.cs
+++ b/Dalamud/Utility/AsyncUtils.cs
@@ -21,7 +21,7 @@ public static class AsyncUtils
/// Returns the first task that completes, according to .
public static Task FirstSuccessfulTask(ICollection> tasks)
{
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var remainingTasks = tasks.Count;
foreach (var task in tasks)
diff --git a/Dalamud/Utility/DynamicPriorityQueueLoader.cs b/Dalamud/Utility/DynamicPriorityQueueLoader.cs
index 8109d2e94..83fd366bb 100644
--- a/Dalamud/Utility/DynamicPriorityQueueLoader.cs
+++ b/Dalamud/Utility/DynamicPriorityQueueLoader.cs
@@ -238,7 +238,7 @@ internal class DynamicPriorityQueueLoader : IDisposable
params IDisposable?[] disposables)
: base(basis, cancellationToken, disposables)
{
- this.taskCompletionSource = new();
+ this.taskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
this.immediateLoadFunction = immediateLoadFunction;
}
diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs
index e6bf8c1f3..7d7bb1380 100644
--- a/Dalamud/Utility/Util.cs
+++ b/Dalamud/Utility/Util.cs
@@ -19,6 +19,7 @@ using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
+using Dalamud.Support;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using Serilog;
@@ -27,8 +28,6 @@ using Windows.Win32.Storage.FileSystem;
using Windows.Win32.System.Memory;
using Windows.Win32.System.Ole;
-using Dalamud.Support;
-
using static TerraFX.Interop.Windows.Windows;
using Win32_PInvoke = Windows.Win32.PInvoke;
@@ -63,6 +62,7 @@ public static class Util
];
private static readonly Type GenericSpanType = typeof(Span<>);
+ private static string? scmVersionInternal;
private static string? gitHashInternal;
private static int? gitCommitCountInternal;
private static string? gitHashClientStructsInternal;
@@ -129,11 +129,28 @@ public static class Util
}
///
- /// Gets the git hash value from the assembly
- /// or null if it cannot be found.
+ /// Gets the SCM Version from the assembly, or null if it cannot be found. This method will generally return
+ /// the git describe output for this build, which will be a raw version if this is a stable build or an
+ /// appropriately-annotated version if this is *not* stable. Local builds will return a `Local Build` text string.
+ ///
+ /// The SCM version of the assembly.
+ public static string GetScmVersion()
+ {
+ if (scmVersionInternal != null) return scmVersionInternal;
+
+ var asm = typeof(Util).Assembly;
+ var attrs = asm.GetCustomAttributes();
+
+ return scmVersionInternal = attrs.First(a => a.Key == "SCMVersion").Value
+ ?? asm.GetName().Version!.ToString();
+ }
+
+ ///
+ /// Gets the git commit hash value from the assembly or null if it cannot be found. Will be null for Debug builds,
+ /// and will be suffixed with `-dirty` if in release with pending changes.
///
/// The git hash of the assembly.
- public static string GetGitHash()
+ public static string? GetGitHash()
{
if (gitHashInternal != null)
return gitHashInternal;
@@ -141,15 +158,14 @@ public static class Util
var asm = typeof(Util).Assembly;
var attrs = asm.GetCustomAttributes();
- gitHashInternal = attrs.First(a => a.Key == "GitHash").Value;
-
- return gitHashInternal;
+ return gitHashInternal = attrs.First(a => a.Key == "GitHash").Value;
}
///
/// Gets the amount of commits in the current branch, or null if undetermined.
///
/// The amount of commits in the current branch.
+ [Obsolete($"Planned for removal in API 11. Use {nameof(GetScmVersion)} for version tracking.")]
public static int? GetGitCommitCount()
{
if (gitCommitCountInternal != null)
@@ -171,7 +187,7 @@ public static class Util
/// or null if it cannot be found.
///
/// The git hash of the assembly.
- public static string GetGitHashClientStructs()
+ public static string? GetGitHashClientStructs()
{
if (gitHashClientStructsInternal != null)
return gitHashClientStructsInternal;
@@ -270,7 +286,7 @@ public static class Util
{
if ((mbi.Protect & (1 << i)) == 0)
continue;
- if (c++ == 0)
+ if (c++ != 0)
sb.Append(" | ");
sb.Append(PageProtectionFlagNames[i]);
}
diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs
index 07f7b3fda..731e3ab00 160000
--- a/lib/FFXIVClientStructs
+++ b/lib/FFXIVClientStructs
@@ -1 +1 @@
-Subproject commit 07f7b3fda2da0f9f8891241ca70839c2acdf2c4a
+Subproject commit 731e3ab0006ce56c4fe789aee148bc967965b914