Merge branch 'api13'

This commit is contained in:
goat 2025-08-07 13:38:52 +02:00
commit 579ecdc4b2
1080 changed files with 1130188 additions and 6574 deletions

8
.gitattributes vendored
View file

@ -17,7 +17,7 @@
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
@ -46,9 +46,9 @@
###############################################################################
# diff behavior for common document formats
#
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
@ -61,3 +61,5 @@
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain
imgui/**/Generated/**/*.cs linguist-generated=true

5
.gitignore vendored
View file

@ -327,4 +327,7 @@ ASALocalRun/
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
.mfractor/
# HexaGen generated files
#imgui/**/Generated/**/*

9
.gitmodules vendored
View file

@ -1,6 +1,3 @@
[submodule "lib/ImGuiScene"]
path = lib/ImGuiScene
url = https://github.com/goatcorp/ImGuiScene
[submodule "lib/FFXIVClientStructs"]
path = lib/FFXIVClientStructs
url = https://github.com/aers/FFXIVClientStructs
@ -19,3 +16,9 @@
[submodule "lib/cimguizmo"]
path = lib/cimguizmo
url = https://github.com/goatcorp/gc-cimguizmo
[submodule "lib/Hexa.NET.ImGui"]
path = lib/Hexa.NET.ImGui
url = https://github.com/goatcorp/Hexa.NET.ImGui.git
[submodule "lib/Lumina.Excel"]
path = lib/Lumina.Excel
url = https://github.com/NotAdam/Lumina.Excel.git

View file

@ -49,7 +49,6 @@
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<PreprocessorDefinitions>CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
@ -66,6 +65,7 @@
<ClCompile>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>false</IntrinsicFunctions>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Use</PrecompiledHeader>
<DisableSpecificWarnings Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">26812</DisableSpecificWarnings>
@ -80,6 +80,7 @@
<ClCompile>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Use</PrecompiledHeader>
<DisableSpecificWarnings Condition="'$(Configuration)|$(Platform)'=='Release|x64'">26812</DisableSpecificWarnings>
@ -203,7 +204,6 @@
<Target Name="CopyOutputDlls" AfterTargets="PostBuildEvent">
<Copy SourceFiles="$(OutDir)$(TargetName).dll" DestinationFolder="..\bin\$(Configuration)\" />
<Copy SourceFiles="$(OutDir)$(TargetName).pdb" DestinationFolder="..\bin\$(Configuration)\" />
<Copy SourceFiles="$(OutDir)nethost.dll" DestinationFolder="..\bin\$(Configuration)\" />
</Target>
</Project>

View file

@ -117,6 +117,7 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
config.NoLoadThirdPartyPlugins = json.value("NoLoadThirdPartyPlugins", config.NoLoadThirdPartyPlugins);
config.BootLogPath = json.value("BootLogPath", config.BootLogPath);
config.BootDebugDirectX = json.value("BootDebugDirectX", config.BootDebugDirectX);
config.BootShowConsole = json.value("BootShowConsole", config.BootShowConsole);
config.BootDisableFallbackConsole = json.value("BootDisableFallbackConsole", config.BootDisableFallbackConsole);
config.BootWaitMessageBox = json.value("BootWaitMessageBox", config.BootWaitMessageBox);

View file

@ -55,6 +55,7 @@ struct DalamudStartInfo {
bool NoLoadThirdPartyPlugins;
std::string BootLogPath;
bool BootDebugDirectX = false;
bool BootShowConsole = false;
bool BootDisableFallbackConsole = false;
WaitMessageboxFlags BootWaitMessageBox = WaitMessageboxFlags::None;

View file

@ -1,6 +1,10 @@
#include "pch.h"
#include <d3d11.h>
#include <dxgi1_3.h>
#include "DalamudStartInfo.h"
#include "hooks.h"
#include "logging.h"
#include "utils.h"
#include "veh.h"
@ -90,6 +94,69 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
if ((g_startInfo.BootWaitMessageBox & DalamudStartInfo::WaitMessageboxFlags::BeforeInitialize) != DalamudStartInfo::WaitMessageboxFlags::None)
MessageBoxW(nullptr, L"Press OK to continue (BeforeInitialize)", L"Dalamud Boot", MB_OK);
if (g_startInfo.BootDebugDirectX) {
logging::I("Enabling DirectX Debugging.");
const auto hD3D11 = GetModuleHandleW(L"d3d11.dll");
const auto hDXGI = GetModuleHandleW(L"dxgi.dll");
const auto pfnD3D11CreateDevice = static_cast<decltype(&D3D11CreateDevice)>(
hD3D11 ? static_cast<void*>(GetProcAddress(hD3D11, "D3D11CreateDevice")) : nullptr);
if (pfnD3D11CreateDevice) {
static hooks::direct_hook<decltype(D3D11CreateDevice)> s_hookD3D11CreateDevice(
"d3d11.dll!D3D11CreateDevice",
pfnD3D11CreateDevice);
s_hookD3D11CreateDevice.set_detour([](
IDXGIAdapter* pAdapter,
D3D_DRIVER_TYPE DriverType,
HMODULE Software,
UINT Flags,
const D3D_FEATURE_LEVEL* pFeatureLevels,
UINT FeatureLevels,
UINT SDKVersion,
ID3D11Device** ppDevice,
D3D_FEATURE_LEVEL* pFeatureLevel,
ID3D11DeviceContext** ppImmediateContext
) -> HRESULT {
return s_hookD3D11CreateDevice.call_original(
pAdapter,
DriverType,
Software,
(Flags & ~D3D11_CREATE_DEVICE_PREVENT_ALTERING_LAYER_SETTINGS_FROM_REGISTRY) | D3D11_CREATE_DEVICE_DEBUG,
pFeatureLevels,
FeatureLevels,
SDKVersion,
ppDevice,
pFeatureLevel,
ppImmediateContext);
});
} else {
logging::W("Could not find d3d11!D3D11CreateDevice.");
}
const auto pfnCreateDXGIFactory = static_cast<decltype(&CreateDXGIFactory)>(
hDXGI ? static_cast<void*>(GetProcAddress(hDXGI, "CreateDXGIFactory")) : nullptr);
const auto pfnCreateDXGIFactory1 = static_cast<decltype(&CreateDXGIFactory1)>(
hDXGI ? static_cast<void*>(GetProcAddress(hDXGI, "CreateDXGIFactory1")) : nullptr);
static const auto pfnCreateDXGIFactory2 = static_cast<decltype(&CreateDXGIFactory2)>(
hDXGI ? static_cast<void*>(GetProcAddress(hDXGI, "CreateDXGIFactory2")) : nullptr);
if (pfnCreateDXGIFactory2) {
static hooks::direct_hook<decltype(CreateDXGIFactory)> s_hookCreateDXGIFactory(
"dxgi.dll!CreateDXGIFactory",
pfnCreateDXGIFactory);
static hooks::direct_hook<decltype(CreateDXGIFactory1)> s_hookCreateDXGIFactory1(
"dxgi.dll!CreateDXGIFactory1",
pfnCreateDXGIFactory1);
s_hookCreateDXGIFactory.set_detour([](REFIID riid, _COM_Outptr_ void **ppFactory) -> HRESULT {
return pfnCreateDXGIFactory2(DXGI_CREATE_FACTORY_DEBUG, riid, ppFactory);
});
s_hookCreateDXGIFactory1.set_detour([](REFIID riid, _COM_Outptr_ void **ppFactory) -> HRESULT {
return pfnCreateDXGIFactory2(DXGI_CREATE_FACTORY_DEBUG, riid, ppFactory);
});
} else {
logging::W("Could not find dxgi!CreateDXGIFactory2.");
}
}
if (minHookLoaded) {
logging::I("Applying fixes...");
xivfixes::apply_all(true);

View file

@ -101,6 +101,11 @@ public record DalamudStartInfo
/// </summary>
public bool BootShowConsole { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to enable D3D11 and DXGI debugging if possible.
/// </summary>
public bool BootDebugDirectX { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the fallback console should be shown, if needed.
/// </summary>

View file

@ -26,7 +26,6 @@
<ItemGroup>
<PackageReference Include="Lumina" Version="$(LuminaVersion)" />
<PackageReference Include="Lumina.Excel" Version="$(LuminaExcelVersion)" />
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">
<PrivateAssets>all</PrivateAssets>
@ -38,15 +37,6 @@
<ProjectReference Include="..\Dalamud\Dalamud.csproj">
<Private>false</Private>
</ProjectReference>
<ProjectReference Include="..\lib\ImGuiScene\deps\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj">
<Private>false</Private>
</ProjectReference>
<ProjectReference Include="..\lib\ImGuiScene\ImGuiScene\ImGuiScene.csproj">
<Private>false</Private>
</ProjectReference>
<ProjectReference Include="..\lib\ImGuiScene\deps\SDL2-CS\SDL2-CS.csproj">
<Private>false</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>

View file

@ -1,8 +1,8 @@
using System;
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Windowing;
using ImGuiNET;
namespace Dalamud.CorePlugin
{

View file

@ -39,7 +39,6 @@
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpplatest</LanguageStandard>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<PreprocessorDefinitions>CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
@ -56,6 +55,7 @@
<ClCompile>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>false</IntrinsicFunctions>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
@ -67,6 +67,7 @@
<ClCompile>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
@ -107,4 +108,4 @@
<Delete Files="$(OutDir)$(TargetName).lib" />
<Delete Files="$(OutDir)$(TargetName).exp" />
</Target>
</Project>
</Project>

View file

@ -91,6 +91,7 @@ namespace Dalamud.Injector
startInfo = ExtractAndInitializeStartInfoFromArguments(startInfo, args);
// Remove already handled arguments
args.Remove("--debug-directx");
args.Remove("--console");
args.Remove("--msgbox1");
args.Remove("--msgbox2");
@ -464,6 +465,7 @@ namespace Dalamud.Injector
startInfo.LogName ??= string.Empty;
// Set boot defaults
startInfo.BootDebugDirectX = args.Contains("--debug-directx");
startInfo.BootShowConsole = args.Contains("--console");
startInfo.BootEnableEtw = args.Contains("--etw");
startInfo.BootLogPath = GetLogPath(startInfo.LogPath, "dalamud.boot", startInfo.LogName);

View file

@ -32,12 +32,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Test", "Dalamud.Tes
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{E15BDA6D-E881-4482-94BA-BE5527E917FF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImGui.NET-472", "lib\ImGuiScene\deps\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj", "{0483026E-C6CE-4B1A-AA68-46544C08140B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImGuiScene", "lib\ImGuiScene\ImGuiScene\ImGuiScene.csproj", "{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SDL2-CS", "lib\ImGuiScene\deps\SDL2-CS\SDL2-CS.csproj", "{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.CorePlugin", "Dalamud.CorePlugin\Dalamud.CorePlugin.csproj", "{4AFDB34A-7467-4D41-B067-53BC4101D9D0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs", "lib\FFXIVClientStructs\FFXIVClientStructs\FFXIVClientStructs.csproj", "{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}"
@ -68,6 +62,24 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cimplot", "external\cimplot
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cimguizmo", "external\cimguizmo\cimguizmo.vcxproj", "{F258347D-31BE-4605-98CE-40E43BDF6F9D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.Bindings.ImGui", "imgui\Dalamud.Bindings.ImGui\Dalamud.Bindings.ImGui.csproj", "{B0AA8737-33A3-4766-8CBE-A48F2EF283BA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.Bindings.ImGuizmo", "imgui\Dalamud.Bindings.ImGuizmo\Dalamud.Bindings.ImGuizmo.csproj", "{5E6EDD75-AE95-43A6-9D67-95B840EB4B71}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.Bindings.ImPlot", "imgui\Dalamud.Bindings.ImPlot\Dalamud.Bindings.ImPlot.csproj", "{9C70BD06-D52C-425E-9C14-5D66BC6046EF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Bindings", "Bindings", "{A217B3DF-607A-4EFB-B107-3C4809348043}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StandaloneImGuiTestbed", "imgui\StandaloneImGuiTestbed\StandaloneImGuiTestbed.csproj", "{4702A911-2513-478C-A434-2776393FDE77}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImGuiScene", "imgui\ImGuiScene\ImGuiScene.csproj", "{66753AC7-0029-4373-9CC4-7760B1F46141}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Lumina", "Lumina", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumina.Excel.Generator", "lib\Lumina.Excel\src\Lumina.Excel.Generator\Lumina.Excel.Generator.csproj", "{5A44DF0C-C9DA-940F-4D6B-4A11D13AEA3D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumina.Excel", "lib\Lumina.Excel\src\Lumina.Excel\Lumina.Excel.csproj", "{88FB719B-EB41-73C5-8D25-C03E0C69904F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -98,18 +110,6 @@ Global
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.Build.0 = Debug|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.Build.0 = Release|x64
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.ActiveCfg = Debug|x64
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.Build.0 = Debug|x64
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.ActiveCfg = Release|x64
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.Build.0 = Release|x64
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.ActiveCfg = Debug|x64
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.Build.0 = Debug|x64
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.ActiveCfg = Release|x64
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.Build.0 = Release|x64
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|Any CPU.ActiveCfg = Debug|x64
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|Any CPU.Build.0 = Debug|x64
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|Any CPU.ActiveCfg = Release|x64
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|Any CPU.Build.0 = Release|x64
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|Any CPU.ActiveCfg = Debug|x64
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|Any CPU.Build.0 = Debug|x64
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|Any CPU.ActiveCfg = Release|x64
@ -154,6 +154,34 @@ Global
{F258347D-31BE-4605-98CE-40E43BDF6F9D}.Debug|Any CPU.Build.0 = Debug|x64
{F258347D-31BE-4605-98CE-40E43BDF6F9D}.Release|Any CPU.ActiveCfg = Release|x64
{F258347D-31BE-4605-98CE-40E43BDF6F9D}.Release|Any CPU.Build.0 = Release|x64
{B0AA8737-33A3-4766-8CBE-A48F2EF283BA}.Debug|Any CPU.ActiveCfg = Debug|x64
{B0AA8737-33A3-4766-8CBE-A48F2EF283BA}.Debug|Any CPU.Build.0 = Debug|x64
{B0AA8737-33A3-4766-8CBE-A48F2EF283BA}.Release|Any CPU.ActiveCfg = Release|x64
{B0AA8737-33A3-4766-8CBE-A48F2EF283BA}.Release|Any CPU.Build.0 = Release|x64
{5E6EDD75-AE95-43A6-9D67-95B840EB4B71}.Debug|Any CPU.ActiveCfg = Debug|x64
{5E6EDD75-AE95-43A6-9D67-95B840EB4B71}.Debug|Any CPU.Build.0 = Debug|x64
{5E6EDD75-AE95-43A6-9D67-95B840EB4B71}.Release|Any CPU.ActiveCfg = Release|x64
{5E6EDD75-AE95-43A6-9D67-95B840EB4B71}.Release|Any CPU.Build.0 = Release|x64
{9C70BD06-D52C-425E-9C14-5D66BC6046EF}.Debug|Any CPU.ActiveCfg = Debug|x64
{9C70BD06-D52C-425E-9C14-5D66BC6046EF}.Debug|Any CPU.Build.0 = Debug|x64
{9C70BD06-D52C-425E-9C14-5D66BC6046EF}.Release|Any CPU.ActiveCfg = Release|x64
{9C70BD06-D52C-425E-9C14-5D66BC6046EF}.Release|Any CPU.Build.0 = Release|x64
{4702A911-2513-478C-A434-2776393FDE77}.Debug|Any CPU.ActiveCfg = Debug|x64
{4702A911-2513-478C-A434-2776393FDE77}.Debug|Any CPU.Build.0 = Debug|x64
{4702A911-2513-478C-A434-2776393FDE77}.Release|Any CPU.ActiveCfg = Release|x64
{4702A911-2513-478C-A434-2776393FDE77}.Release|Any CPU.Build.0 = Release|x64
{66753AC7-0029-4373-9CC4-7760B1F46141}.Debug|Any CPU.ActiveCfg = Debug|x64
{66753AC7-0029-4373-9CC4-7760B1F46141}.Debug|Any CPU.Build.0 = Debug|x64
{66753AC7-0029-4373-9CC4-7760B1F46141}.Release|Any CPU.ActiveCfg = Release|x64
{66753AC7-0029-4373-9CC4-7760B1F46141}.Release|Any CPU.Build.0 = Release|x64
{5A44DF0C-C9DA-940F-4D6B-4A11D13AEA3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5A44DF0C-C9DA-940F-4D6B-4A11D13AEA3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5A44DF0C-C9DA-940F-4D6B-4A11D13AEA3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5A44DF0C-C9DA-940F-4D6B-4A11D13AEA3D}.Release|Any CPU.Build.0 = Release|Any CPU
{88FB719B-EB41-73C5-8D25-C03E0C69904F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{88FB719B-EB41-73C5-8D25-C03E0C69904F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{88FB719B-EB41-73C5-8D25-C03E0C69904F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{88FB719B-EB41-73C5-8D25-C03E0C69904F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -161,9 +189,6 @@ Global
GlobalSection(NestedProjects) = preSolution
{5B832F73-5F54-4ADC-870F-D0095EF72C9A} = {19775C83-7117-4A5F-AA00-18889F46A490}
{8874326B-E755-4D13-90B4-59AB263A3E6B} = {19775C83-7117-4A5F-AA00-18889F46A490}
{0483026E-C6CE-4B1A-AA68-46544C08140B} = {DBE5345E-6594-4A59-B183-1C3D5592269D}
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A} = {DBE5345E-6594-4A59-B183-1C3D5592269D}
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8} = {DBE5345E-6594-4A59-B183-1C3D5592269D}
{4AFDB34A-7467-4D41-B067-53BC4101D9D0} = {8F079208-C227-4D96-9427-2BEBE0003944}
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {8BBACF2D-7AB8-4610-A115-0E363D35C291}
{E0D51896-604F-4B40-8CFE-51941607B3A1} = {8BBACF2D-7AB8-4610-A115-0E363D35C291}
@ -175,6 +200,14 @@ Global
{8BBACF2D-7AB8-4610-A115-0E363D35C291} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
{76CAA246-C405-4A8C-B0AE-F4A0EF3D4E16} = {DBE5345E-6594-4A59-B183-1C3D5592269D}
{F258347D-31BE-4605-98CE-40E43BDF6F9D} = {DBE5345E-6594-4A59-B183-1C3D5592269D}
{B0AA8737-33A3-4766-8CBE-A48F2EF283BA} = {A217B3DF-607A-4EFB-B107-3C4809348043}
{5E6EDD75-AE95-43A6-9D67-95B840EB4B71} = {A217B3DF-607A-4EFB-B107-3C4809348043}
{9C70BD06-D52C-425E-9C14-5D66BC6046EF} = {A217B3DF-607A-4EFB-B107-3C4809348043}
{4702A911-2513-478C-A434-2776393FDE77} = {A217B3DF-607A-4EFB-B107-3C4809348043}
{66753AC7-0029-4373-9CC4-7760B1F46141} = {A217B3DF-607A-4EFB-B107-3C4809348043}
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
{5A44DF0C-C9DA-940F-4D6B-4A11D13AEA3D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{88FB719B-EB41-73C5-8D25-C03E0C69904F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {79B65AC9-C940-410E-AB61-7EA7E12C7599}

View file

@ -23,6 +23,7 @@ using Dalamud.Utility;
using Newtonsoft.Json;
using Serilog;
using Serilog.Events;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Dalamud.Configuration.Internal;
@ -152,23 +153,11 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary>
public float GlobalUiScale { get; set; } = 1.0f;
/// <summary>
/// Gets or sets a value indicating whether to use AXIS fonts from the game.
/// </summary>
[Obsolete($"See {nameof(DefaultFontSpec)}")]
public bool UseAxisFontsFromGame { get; set; } = true;
/// <summary>
/// Gets or sets the default font spec.
/// </summary>
public IFontSpec? DefaultFontSpec { get; set; }
/// <summary>
/// Gets or sets the gamma value to apply for Dalamud fonts. Do not use.
/// </summary>
[Obsolete("It happens that nobody touched this setting", true)]
public float FontGammaLevel { get; set; } = 1.4f;
/// <summary>Gets or sets the opacity of the IME state indicator.</summary>
/// <value>0 will hide the state indicator. 1 will make the state indicator fully visible. Values outside the
/// range will be clamped to [0, 1].</value>
@ -604,11 +593,15 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
{
// https://source.chromium.org/chromium/chromium/src/+/main:ui/gfx/animation/animation_win.cc;l=29?q=ReducedMotion&ss=chromium
var winAnimEnabled = 0;
var success = NativeFunctions.SystemParametersInfo(
(uint)NativeFunctions.AccessibilityParameter.SPI_GETCLIENTAREAANIMATION,
0,
ref winAnimEnabled,
0);
var success = false;
unsafe
{
success = Windows.Win32.PInvoke.SystemParametersInfo(
SYSTEM_PARAMETERS_INFO_ACTION.SPI_GETCLIENTAREAANIMATION,
0,
&winAnimEnabled,
0);
}
if (!success)
{

View file

@ -13,8 +13,9 @@ using Dalamud.Plugin.Internal;
using Dalamud.Storage;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
using PInvoke;
using Serilog;
using Windows.Win32.Foundation;
using Windows.Win32.Security;
#if DEBUG
[assembly: InternalsVisibleTo("Dalamud.CorePlugin")]
@ -29,7 +30,7 @@ namespace Dalamud;
/// The main Dalamud class containing all subsystems.
/// </summary>
[ServiceManager.ProvidedService]
internal sealed class Dalamud : IServiceType
internal sealed unsafe class Dalamud : IServiceType
{
#region Internals
@ -79,7 +80,7 @@ internal sealed class Dalamud : IServiceType
{
Log.Verbose("=============== GAME THREAD KICKOFF ===============");
Timings.Event("Game thread kickoff");
NativeFunctions.SetEvent(mainThreadContinueEvent);
Windows.Win32.PInvoke.SetEvent(new HANDLE(mainThreadContinueEvent));
}
void HandleServiceInitFailure(Task t)
@ -116,9 +117,9 @@ internal sealed class Dalamud : IServiceType
HandleServiceInitFailure(t);
});
this.DefaultExceptionFilter = NativeFunctions.SetUnhandledExceptionFilter(nint.Zero);
NativeFunctions.SetUnhandledExceptionFilter(this.DefaultExceptionFilter);
Log.Debug($"SE default exception filter at {this.DefaultExceptionFilter.ToInt64():X}");
this.DefaultExceptionFilter = SetExceptionHandler(nint.Zero);
SetExceptionHandler(this.DefaultExceptionFilter);
Log.Debug($"SE default exception filter at {new IntPtr(this.DefaultExceptionFilter):X}");
var debugSig = "40 55 53 57 48 8D AC 24 70 AD FF FF";
this.DebugExceptionFilter = Service<TargetSigScanner>.Get().ScanText(debugSig);
@ -170,8 +171,9 @@ internal sealed class Dalamud : IServiceType
if (!reportCrashesSetting && !pmHasDevPlugins)
{
// Leaking on purpose for now
var attribs = Kernel32.SECURITY_ATTRIBUTES.Create();
Kernel32.CreateMutex(attribs, false, "DALAMUD_CRASHES_NO_MORE");
var attribs = default(SECURITY_ATTRIBUTES);
attribs.nLength = (uint)Unsafe.SizeOf<SECURITY_ATTRIBUTES>();
Windows.Win32.PInvoke.CreateMutex(attribs, false, "DALAMUD_CRASHES_NO_MORE");
}
this.unloadSignal.Set();
@ -189,27 +191,29 @@ internal sealed class Dalamud : IServiceType
/// Replace the current exception handler with the default one.
/// </summary>
internal void UseDefaultExceptionHandler() =>
this.SetExceptionHandler(this.DefaultExceptionFilter);
SetExceptionHandler(this.DefaultExceptionFilter);
/// <summary>
/// Replace the current exception handler with a debug one.
/// </summary>
internal void UseDebugExceptionHandler() =>
this.SetExceptionHandler(this.DebugExceptionFilter);
SetExceptionHandler(this.DebugExceptionFilter);
/// <summary>
/// Disable the current exception handler.
/// </summary>
internal void UseNoExceptionHandler() =>
this.SetExceptionHandler(nint.Zero);
SetExceptionHandler(nint.Zero);
/// <summary>
/// Helper function to set the exception handler.
/// </summary>
private void SetExceptionHandler(nint newFilter)
private static nint SetExceptionHandler(nint newFilter)
{
var oldFilter = NativeFunctions.SetUnhandledExceptionFilter(newFilter);
Log.Debug("Set ExceptionFilter to {0}, old: {1}", newFilter, oldFilter);
var oldFilter =
Windows.Win32.PInvoke.SetUnhandledExceptionFilter((delegate* unmanaged[Stdcall]<global::Windows.Win32.System.Diagnostics.Debug.EXCEPTION_POINTERS*, int>)newFilter);
Log.Debug("Set ExceptionFilter to {0}, old: {1}", newFilter, (nint)oldFilter);
return (nint)oldFilter;
}
private void SetupClientStructsResolver(DirectoryInfo cacheDir)

View file

@ -6,7 +6,7 @@
<PropertyGroup Label="Feature">
<Description>XIV Launcher addon framework</Description>
<DalamudVersion>12.0.1.5</DalamudVersion>
<DalamudVersion>13.0.0.0</DalamudVersion>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version>
<FileVersion>$(DalamudVersion)</FileVersion>
@ -35,6 +35,10 @@
<Deterministic>true</Deterministic>
<Nullable>annotations</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- Enable when cswin32 properly supports implementing COM interfaces and we can
make IDropTarget work -->
<!-- <DisableRuntimeMarshalling>true</DisableRuntimeMarshalling> -->
</PropertyGroup>
<PropertyGroup Label="Configuration">
@ -64,12 +68,13 @@
<PackageReference Include="goatcorp.Reloaded.Assembler" Version="1.0.14-goatcorp3" />
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" />
<PackageReference Include="Lumina" Version="$(LuminaVersion)" />
<PackageReference Include="Lumina.Excel" Version="$(LuminaExcelVersion)" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="9.0.0-preview.1.24081.5" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.46-beta">
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="8.0.7" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.183">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="MinSharp" Version="1.0.4" />
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />
<PackageReference Include="SharpDX.Mathematics" Version="4.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
<PackageReference Include="Serilog" Version="4.0.2" />
<PackageReference Include="Serilog.Sinks.Async" Version="2.0.0" />
@ -87,13 +92,23 @@
<PackageReference Include="System.Resources.Extensions" Version="8.0.0" />
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.22621.2" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Interface\ImGuiBackend\Renderers\imgui-frag.hlsl.bytes">
<LogicalName>imgui-frag.hlsl.bytes</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="Interface\ImGuiBackend\Renderers\imgui-vertex.hlsl.bytes">
<LogicalName>imgui-vertex.hlsl.bytes</LogicalName>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Dalamud.Common\Dalamud.Common.csproj" />
<ProjectReference Include="..\imgui\Dalamud.Bindings.ImGuizmo\Dalamud.Bindings.ImGuizmo.csproj" />
<ProjectReference Include="..\imgui\Dalamud.Bindings.ImGui\Dalamud.Bindings.ImGui.csproj" />
<ProjectReference Include="..\imgui\Dalamud.Bindings.ImPlot\Dalamud.Bindings.ImPlot.csproj" />
<ProjectReference Include="..\imgui\ImGuiScene\ImGuiScene.csproj" />
<ProjectReference Include="..\lib\FFXIVClientStructs\FFXIVClientStructs\FFXIVClientStructs.csproj" />
<ProjectReference Include="..\lib\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj" />
<ProjectReference Include="..\lib\ImGuiScene\deps\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj" />
<ProjectReference Include="..\lib\ImGuiScene\deps\SDL2-CS\SDL2-CS.csproj" />
<ProjectReference Include="..\lib\ImGuiScene\ImGuiScene\ImGuiScene.csproj" />
<ProjectReference Include="..\lib\Lumina.Excel\src\Lumina.Excel\Lumina.Excel.csproj" />
</ItemGroup>
<ItemGroup>

View file

@ -1,3 +1,3 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=interface_005Cfontawesome/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Int64 x:Key="/Default/PerformanceThreshold/AnalysisFileSizeThreshold/=CSHARP/@EntryIndexedValue">300000</s:Int64>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=interface_005Cfontawesome/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=interface_005Ctextures_005Ctexturewraps_005Cinternal_005Cdrawlisttexturewrap/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View file

@ -1,6 +1,8 @@
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@ -15,12 +17,11 @@ using Dalamud.Storage;
using Dalamud.Support;
using Dalamud.Utility;
using Newtonsoft.Json;
using PInvoke;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using static Dalamud.NativeFunctions;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Dalamud;
@ -58,7 +59,7 @@ public sealed class EntryPoint
var info = JsonConvert.DeserializeObject<DalamudStartInfo>(infoStr)!;
if ((info.BootWaitMessageBox & 4) != 0)
MessageBoxW(IntPtr.Zero, "Press OK to continue (BeforeDalamudConstruct)", "Dalamud Boot", MessageBoxType.Ok);
Windows.Win32.PInvoke.MessageBox(HWND.Null, "Press OK to continue (BeforeDalamudConstruct)", "Dalamud Boot", MESSAGEBOX_STYLE.MB_OK);
new Thread(() => RunThread(info, mainThreadContinueEvent)).Start();
}
@ -135,6 +136,8 @@ public sealed class EntryPoint
/// <param name="mainThreadContinueEvent">Event used to signal the main thread to continue.</param>
private static void RunThread(DalamudStartInfo info, IntPtr mainThreadContinueEvent)
{
NativeLibrary.Load(Path.Combine(info.WorkingDirectory!, "cimgui.dll"));
// Setup logger
InitLogging(info.LogPath!, info.BootShowConsole, true, info.LogName);
SerilogEventSink.Instance.LogLine += SerilogOnLogLine;
@ -259,10 +262,12 @@ public sealed class EntryPoint
var symbolPath = Path.Combine(info.AssetDirectory, "UIRes", "pdb");
var searchPath = $".;{symbolPath}";
// Remove any existing Symbol Handler and Init a new one with our search path added
SymCleanup(GetCurrentProcess());
var currentProcess = Windows.Win32.PInvoke.GetCurrentProcess_SafeHandle();
if (!SymInitialize(GetCurrentProcess(), searchPath, true))
// Remove any existing Symbol Handler and Init a new one with our search path added
Windows.Win32.PInvoke.SymCleanup(currentProcess);
if (!Windows.Win32.PInvoke.SymInitialize(currentProcess, searchPath, true))
throw new Win32Exception();
}
catch (Exception ex)
@ -304,14 +309,14 @@ public sealed class EntryPoint
// ignored
}
const MessageBoxType flags = NativeFunctions.MessageBoxType.YesNo | NativeFunctions.MessageBoxType.IconError | NativeFunctions.MessageBoxType.SystemModal;
var result = MessageBoxW(
Process.GetCurrentProcess().MainWindowHandle,
const MESSAGEBOX_STYLE flags = MESSAGEBOX_STYLE.MB_YESNO | MESSAGEBOX_STYLE.MB_ICONERROR | MESSAGEBOX_STYLE.MB_SYSTEMMODAL;
var result = Windows.Win32.PInvoke.MessageBox(
new HWND(Process.GetCurrentProcess().MainWindowHandle),
$"An internal error in a Dalamud plugin occurred.\nThe game must close.\n\n{ex.GetType().Name}\n{info}\n\n{pluginInfo}More information has been recorded separately{supportText}.\n\nDo you want to disable all plugins the next time you start the game?",
"Dalamud",
flags);
if (result == (int)User32.MessageBoxResult.IDYES)
if (result == MESSAGEBOX_RESULT.IDYES)
{
Log.Information("User chose to disable plugins on next launch...");
var config = Service<DalamudConfiguration>.Get();

View file

@ -1,4 +1,4 @@
using Dalamud.Plugin.Services;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
@ -33,17 +33,10 @@ internal unsafe class AddonEventEntry
/// </summary>
public required nint Node { get; init; }
/// <summary>
/// Gets the handler that gets called when this event is triggered.
/// </summary>
[Obsolete("Use AddonEventDelegate Delegate instead")]
public IAddonEventManager.AddonEventHandler Handler { get; init; }
/// <summary>
/// Gets the delegate that gets called when this event is triggered.
/// </summary>
[Api13ToDo("Make this field required")]
public IAddonEventManager.AddonEventDelegate Delegate { get; init; }
public required IAddonEventManager.AddonEventDelegate Delegate { get; init; }
/// <summary>
/// Gets the unique id for this event.

View file

@ -73,29 +73,6 @@ internal unsafe class AddonEventManager : IInternalDisposableService
this.addonLifecycle.UnregisterListener(this.finalizeEventListener);
}
/// <summary>
/// Registers an event handler for the specified addon, node, and type.
/// </summary>
/// <param name="pluginId">Unique ID for this plugin.</param>
/// <param name="atkUnitBase">The parent addon for this event.</param>
/// <param name="atkResNode">The node that will trigger this event.</param>
/// <param name="eventType">The event type for this event.</param>
/// <param name="eventHandler">The handler to call when event is triggered.</param>
/// <returns>IAddonEventHandle used to remove the event.</returns>
internal IAddonEventHandle? AddEvent(Guid pluginId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler)
{
if (this.pluginEventControllers.TryGetValue(pluginId, out var controller))
{
return controller.AddEvent(atkUnitBase, atkResNode, eventType, eventHandler);
}
else
{
Log.Verbose($"Unable to locate controller for {pluginId}. No event was added.");
}
return null;
}
/// <summary>
/// Registers an event handler for the specified addon, node, and type.
/// </summary>
@ -260,10 +237,6 @@ internal class AddonEventManagerPluginScoped : IInternalDisposableService, IAddo
}).Wait();
}
/// <inheritdoc/>
public IAddonEventHandle? AddEvent(IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler)
=> this.eventManagerService.AddEvent(this.plugin.EffectiveWorkingPluginId, atkUnitBase, atkResNode, eventType, eventHandler);
/// <inheritdoc/>
public IAddonEventHandle? AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventDelegate eventDelegate)
=> this.eventManagerService.AddEvent(this.plugin.EffectiveWorkingPluginId, atkUnitBase, atkResNode, eventType, eventDelegate);

View file

@ -121,12 +121,6 @@ public enum AddonEventType : byte
/// </summary>
ListItemClick = 35,
/// <summary>
/// AtkComponentList Toggle.
/// </summary>
[Obsolete("Use ListItemClick")]
ListItemToggle = 35,
/// <summary>
/// AtkComponentList Double Click.
/// </summary>

View file

@ -1,10 +1,32 @@
namespace Dalamud.Game.Addon.Events;
namespace Dalamud.Game.Addon.Events.EventDataTypes;
/// <summary>
/// Object representing data that is relevant in handling native events.
/// </summary>
public class AddonEventData
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonEventData"/> class.
/// </summary>
internal AddonEventData()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AddonEventData"/> class.
/// </summary>
/// <param name="eventData">Other event data to copy.</param>
internal AddonEventData(AddonEventData eventData)
{
this.AtkEventType = eventData.AtkEventType;
this.Param = eventData.Param;
this.AtkEventPointer = eventData.AtkEventPointer;
this.AtkEventDataPointer = eventData.AtkEventDataPointer;
this.AddonPointer = eventData.AddonPointer;
this.NodeTargetPointer = eventData.NodeTargetPointer;
this.AtkEventListener = eventData.AtkEventListener;
}
/// <summary>
/// Gets the AtkEventType for this event.
/// </summary>
@ -18,8 +40,8 @@ public class AddonEventData
/// <summary>
/// Gets the pointer to the AtkEvent object for this event.
/// </summary>
/// <remarks>Note: This is not a pointer to the AtkEventData object.<br/><br/></remarks>
/// <remarks>Warning: AtkEvent->Node has been modified to be the AtkUnitBase*, and AtkEvent->Target has been modified to be the AtkResNode* that triggered this event.</remarks>
/// <remarks>Note: This is not a pointer to the AtkEventData object.<br/><br/>
/// Warning: AtkEvent->Node has been modified to be the AtkUnitBase*, and AtkEvent->Target has been modified to be the AtkResNode* that triggered this event.</remarks>
public nint AtkEventPointer { get; internal set; }
/// <summary>

View file

@ -0,0 +1,82 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using AtkMouseData = FFXIVClientStructs.FFXIV.Component.GUI.AtkEventData.AtkMouseData;
using ModifierFlag = FFXIVClientStructs.FFXIV.Component.GUI.AtkEventData.AtkMouseData.ModifierFlag;
namespace Dalamud.Game.Addon.Events.EventDataTypes;
/// <inheritdoc />
public unsafe class AddonMouseEventData : AddonEventData
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonMouseEventData"/> class.
/// </summary>
internal AddonMouseEventData()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AddonMouseEventData"/> class.
/// </summary>
/// <param name="eventData">Other event data to copy.</param>
internal AddonMouseEventData(AddonEventData eventData)
: base(eventData)
{
}
/// <summary>
/// Gets a value indicating whether the event was a Left Mouse Click.
/// </summary>
public bool IsLeftClick => this.MouseData.ButtonId is 0;
/// <summary>
/// Gets a value indicating whether the event was a Right Mouse Click.
/// </summary>
public bool IsRightClick => this.MouseData.ButtonId is 1;
/// <summary>
/// Gets a value indicating whether there are any modifiers set such as alt, control, shift, or dragging.
/// </summary>
public bool IsNoModifier => this.MouseData.Modifier is 0;
/// <summary>
/// Gets a value indicating whether alt was being held when this event triggered.
/// </summary>
public bool IsAltHeld => this.MouseData.Modifier.HasFlag(ModifierFlag.Alt);
/// <summary>
/// Gets a value indicating whether control was being held when this event triggered.
/// </summary>
public bool IsControlHeld => this.MouseData.Modifier.HasFlag(ModifierFlag.Ctrl);
/// <summary>
/// Gets a value indicating whether shift was being held when this event triggered.
/// </summary>
public bool IsShiftHeld => this.MouseData.Modifier.HasFlag(ModifierFlag.Shift);
/// <summary>
/// Gets a value indicating whether this event is a mouse drag or not.
/// </summary>
public bool IsDragging => this.MouseData.Modifier.HasFlag(ModifierFlag.Dragging);
/// <summary>
/// Gets a value indicating whether the event was a scroll up.
/// </summary>
public bool IsScrollUp => this.MouseData.WheelDirection is 1;
/// <summary>
/// Gets a value indicating whether the event was a scroll down.
/// </summary>
public bool IsScrollDown => this.MouseData.WheelDirection is -1;
/// <summary>
/// Gets the position of the mouse when this event was triggered.
/// </summary>
public Vector2 Position => new(this.MouseData.PosX, this.MouseData.PosY);
private AtkEventData* AtkEventData => (AtkEventData*)this.AtkEventDataPointer;
private AtkMouseData MouseData => this.AtkEventData->MouseData;
}

View file

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.Addon.Events.EventDataTypes;
using Dalamud.Game.Gui;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
@ -29,49 +30,6 @@ internal unsafe class PluginEventController : IDisposable
private List<AddonEventEntry> Events { get; } = new();
/// <summary>
/// Adds a tracked event.
/// </summary>
/// <param name="atkUnitBase">The Parent addon for the event.</param>
/// <param name="atkResNode">The Node for the event.</param>
/// <param name="atkEventType">The Event Type.</param>
/// <param name="handler">The delegate to call when invoking this event.</param>
/// <returns>IAddonEventHandle used to remove the event.</returns>
[Obsolete("Use AddEvent that uses AddonEventDelegate instead of AddonEventHandler")]
public IAddonEventHandle AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType atkEventType, IAddonEventManager.AddonEventHandler handler)
{
var node = (AtkResNode*)atkResNode;
var addon = (AtkUnitBase*)atkUnitBase;
var eventType = (AtkEventType)atkEventType;
var eventId = this.GetNextParamKey();
var eventGuid = Guid.NewGuid();
var eventHandle = new AddonEventHandle
{
AddonName = addon->NameString,
ParamKey = eventId,
EventType = atkEventType,
EventGuid = eventGuid,
};
var eventEntry = new AddonEventEntry
{
Addon = atkUnitBase,
Handler = handler,
Delegate = null,
Node = atkResNode,
EventType = atkEventType,
ParamKey = eventId,
Handle = eventHandle,
};
Log.Verbose($"Adding Event. {eventEntry.LogString}");
this.EventListener.RegisterEvent(addon, node, eventType, eventId);
this.Events.Add(eventEntry);
return eventHandle;
}
/// <summary>
/// Adds a tracked event.
/// </summary>
@ -100,7 +58,6 @@ internal unsafe class PluginEventController : IDisposable
{
Addon = atkUnitBase,
Delegate = eventDelegate,
Handler = null,
Node = atkResNode,
EventType = atkEventType,
ParamKey = eventId,
@ -184,7 +141,7 @@ internal unsafe class PluginEventController : IDisposable
if (currentAddonPointer != eventEntry.Addon) return;
// Make sure the addon is not unloaded
var atkUnitBase = (AtkUnitBase*)currentAddonPointer;
var atkUnitBase = currentAddonPointer.Struct;
if (atkUnitBase->UldManager.LoadedState == AtkLoadState.Unloaded) return;
// Does this addon contain the node this event is for? (by address)
@ -230,7 +187,6 @@ internal unsafe class PluginEventController : IDisposable
this.EventListener.UnregisterEvent(atkResNode, eventType, eventEntry.ParamKey);
}
[Api13ToDo("Remove invoke from eventInfo.Handler, and remove nullability from eventInfo.Delegate?.Invoke")]
private void PluginEventListHandler(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventPtr, AtkEventData* eventDataPtr)
{
try
@ -238,10 +194,7 @@ internal unsafe class PluginEventController : IDisposable
if (eventPtr is null) return;
if (this.Events.FirstOrDefault(handler => handler.ParamKey == eventParam) is not { } eventInfo) return;
// We stored the AtkUnitBase* in EventData->Node, and EventData->Target contains the node that triggered the event.
eventInfo.Handler?.Invoke((AddonEventType)eventType, (nint)eventPtr->Node, (nint)eventPtr->Target);
eventInfo.Delegate?.Invoke((AddonEventType)eventType, new AddonEventData
eventInfo.Delegate.Invoke((AddonEventType)eventType, new AddonEventData
{
AddonPointer = (nint)eventPtr->Node,
NodeTargetPointer = (nint)eventPtr->Target,

View file

@ -1,8 +1,4 @@
using System.Runtime.CompilerServices;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Dalamud.Game.NativeWrapper;
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
@ -17,7 +13,6 @@ public abstract unsafe class AddonArgs
public const string InvalidAddon = "NullAddon";
private string? addonName;
private IntPtr addon;
/// <summary>
/// Gets the name of the addon this args referrers to.
@ -27,10 +22,10 @@ public abstract unsafe class AddonArgs
/// <summary>
/// Gets the pointer to the addons AtkUnitBase.
/// </summary>
public nint Addon
public AtkUnitBasePtr Addon
{
get => this.AddonInternal;
init => this.AddonInternal = value;
get;
internal set;
}
/// <summary>
@ -38,42 +33,32 @@ public abstract unsafe class AddonArgs
/// </summary>
public abstract AddonArgsType Type { get; }
/// <summary>
/// Gets or sets the pointer to the addons AtkUnitBase.
/// </summary>
internal nint AddonInternal
{
get => this.addon;
set
{
this.addon = value;
// Note: always clear addonName on updating the addon being pointed.
// Same address may point to a different addon.
this.addonName = null;
}
}
/// <summary>
/// Checks if addon name matches the given span of char.
/// </summary>
/// <param name="name">The name to check.</param>
/// <returns>Whether it is the case.</returns>
internal bool IsAddon(ReadOnlySpan<char> name)
internal bool IsAddon(string name)
{
if (this.Addon == nint.Zero) return false;
if (name.Length is 0 or > 0x20)
if (this.Addon.IsNull)
return false;
var addonPointer = (AtkUnitBase*)this.Addon;
if (addonPointer->Name[0] == 0) return false;
if (name.Length is 0 or > 32)
return false;
// note: might want to rewrite this to just compare to NameString
return MemoryHelper.EqualsZeroTerminatedString(
name,
(nint)Unsafe.AsPointer(ref addonPointer->Name[0]),
null,
0x20);
if (string.IsNullOrEmpty(this.Addon.Name))
return false;
return name == this.Addon.Name;
}
/// <summary>
/// Clears this AddonArgs values.
/// </summary>
internal virtual void Clear()
{
this.addonName = null;
this.Addon = 0;
}
/// <summary>
@ -82,11 +67,13 @@ public abstract unsafe class AddonArgs
/// <returns>The name of the addon for this object. <see cref="InvalidAddon"/> when invalid.</returns>
private string GetAddonName()
{
if (this.Addon == nint.Zero) return InvalidAddon;
if (this.Addon.IsNull) return InvalidAddon;
var addonPointer = (AtkUnitBase*)this.Addon;
if (addonPointer->Name[0] == 0) return InvalidAddon;
var name = this.Addon.Name;
return this.addonName ??= addonPointer->NameString;
if (string.IsNullOrEmpty(name))
return InvalidAddon;
return this.addonName ??= name;
}
}

View file

@ -41,4 +41,14 @@ public class AddonReceiveEventArgs : AddonArgs, ICloneable
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.AtkEventType = default;
this.EventParam = default;
this.AtkEvent = default;
this.Data = default;
}
}

View file

@ -38,4 +38,12 @@ public class AddonRefreshArgs : AddonArgs, ICloneable
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.AtkValueCount = default;
this.AtkValues = default;
}
}

View file

@ -1,4 +1,4 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for OnRequestedUpdate events.
@ -31,4 +31,12 @@ public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.NumberArrayData = default;
this.StringArrayData = default;
}
}

View file

@ -1,4 +1,4 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
@ -38,4 +38,12 @@ public class AddonSetupArgs : AddonArgs, ICloneable
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.AtkValueCount = default;
this.AtkValues = default;
}
}

View file

@ -35,4 +35,11 @@ public class AddonUpdateArgs : AddonArgs, ICloneable
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.TimeDeltaInternal = default;
}
}

View file

@ -238,7 +238,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
}
using var returner = this.argsPool.Rent(out AddonSetupArgs arg);
arg.AddonInternal = (nint)addon;
arg.Clear();
arg.Addon = (nint)addon;
arg.AtkValueCount = valueCount;
arg.AtkValues = (nint)values;
this.InvokeListenersSafely(AddonEvent.PreSetup, arg);
@ -270,7 +271,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
}
using var returner = this.argsPool.Rent(out AddonFinalizeArgs arg);
arg.AddonInternal = (nint)atkUnitBase[0];
arg.Clear();
arg.Addon = (nint)atkUnitBase[0];
this.InvokeListenersSafely(AddonEvent.PreFinalize, arg);
try
@ -286,7 +288,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
private void OnAddonDraw(AtkUnitBase* addon)
{
using var returner = this.argsPool.Rent(out AddonDrawArgs arg);
arg.AddonInternal = (nint)addon;
arg.Clear();
arg.Addon = (nint)addon;
this.InvokeListenersSafely(AddonEvent.PreDraw, arg);
try
@ -304,7 +307,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
{
using var returner = this.argsPool.Rent(out AddonUpdateArgs arg);
arg.AddonInternal = (nint)addon;
arg.Clear();
arg.Addon = (nint)addon;
arg.TimeDeltaInternal = delta;
this.InvokeListenersSafely(AddonEvent.PreUpdate, arg);
@ -325,7 +329,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
var result = false;
using var returner = this.argsPool.Rent(out AddonRefreshArgs arg);
arg.AddonInternal = (nint)addon;
arg.Clear();
arg.Addon = (nint)addon;
arg.AtkValueCount = valueCount;
arg.AtkValues = (nint)values;
this.InvokeListenersSafely(AddonEvent.PreRefresh, arg);
@ -348,7 +353,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
{
using var returner = this.argsPool.Rent(out AddonRequestedUpdateArgs arg);
arg.AddonInternal = (nint)addon;
arg.Clear();
arg.Addon = (nint)addon;
arg.NumberArrayData = (nint)numberArrayData;
arg.StringArrayData = (nint)stringArrayData;
this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, arg);

View file

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Hooking;
@ -86,7 +86,8 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
}
using var returner = this.argsPool.Rent(out AddonReceiveEventArgs arg);
arg.AddonInternal = (nint)addon;
arg.Clear();
arg.Addon = (nint)addon;
arg.AtkEventType = (byte)eventType;
arg.EventParam = eventParam;
arg.AtkEvent = (IntPtr)atkEvent;

View file

@ -119,8 +119,6 @@ internal partial class ChatHandlers : IServiceType
if (string.IsNullOrEmpty(this.configuration.LastVersion) || !Util.AssemblyVersion.StartsWith(this.configuration.LastVersion))
{
var linkPayload = chatGui.AddChatLinkHandler(
"dalamud",
8459324,
(_, _) => dalamudInterface.OpenPluginInstallerTo(PluginInstallerOpenKind.Changelogs));
var updateMessage = new SeStringBuilder()

View file

@ -97,7 +97,7 @@ internal sealed class AetheryteEntry : IAetheryteEntry
public uint GilCost => this.data.GilCost;
/// <inheritdoc />
public bool IsFavourite => this.data.IsFavourite != 0;
public bool IsFavourite => this.data.IsFavourite;
/// <inheritdoc />
public bool IsSharedHouse => this.data.IsSharedHouse;

View file

@ -63,6 +63,12 @@ public enum ConditionFlag
/// <summary>
/// Unable to execute command while mounted.
/// </summary>
RidingPillion = 10,
/// <summary>
/// Unable to execute command while mounted.
/// </summary>
[Obsolete("Renamed to RidingPillion", true)]
Mounted2 = 10,
/// <summary>
@ -188,12 +194,6 @@ public enum ConditionFlag
/// </summary>
ExecutingCraftingAction = 40,
/// <summary>
/// Unable to execute command while crafting.
/// </summary>
[Obsolete("Renamed to ExecutingCraftingAction.")]
Crafting40 = 40,
/// <summary>
/// Unable to execute command while preparing to craft.
/// </summary>
@ -205,12 +205,6 @@ public enum ConditionFlag
/// <remarks> Includes fishing. </remarks>
ExecutingGatheringAction = 42,
/// <summary>
/// Unable to execute command while gathering.
/// </summary>
[Obsolete("Renamed to ExecutingGatheringAction.")]
Gathering42 = 42,
/// <summary>
/// Unable to execute command while fishing.
/// </summary>
@ -235,12 +229,6 @@ public enum ConditionFlag
/// </summary>
Jumping = 48,
/// <summary>
/// Unable to execute command while auto-run is active.
/// </summary>
[Obsolete("To avoid confusion, renamed to UsingChocoboTaxi.")]
AutorunActive = 49,
/// <summary>
/// Unable to execute command while auto-run is active.
/// </summary>
@ -282,12 +270,6 @@ public enum ConditionFlag
/// </summary>
BoundByDuty56 = 56,
/// <summary>
/// Unable to execute command at this time.
/// </summary>
[Obsolete("Renamed to MountOrOrnamentTransition.")]
Unknown57 = 57,
/// <summary>
/// Unable to execute command at this time.
/// </summary>
@ -449,6 +431,12 @@ public enum ConditionFlag
/// <summary>
/// Unable to execute command in this state.
/// </summary>
MountImmobile = 88,
/// <summary>
/// Unable to execute command in this state.
/// </summary>
[Obsolete("Renamed to MountImmobile", true)]
InThisState88 = 88,
/// <summary>
@ -461,12 +449,6 @@ public enum ConditionFlag
/// </summary>
RolePlaying = 90,
/// <summary>
/// Unable to execute command while bound by duty.
/// </summary>
[Obsolete("Use InDutyQueue")]
BoundToDuty97 = 91,
/// <summary>
/// Unable to execute command while bound by duty.
/// Specifically triggered when you are in a queue for a duty but not inside a duty.
@ -483,12 +465,6 @@ public enum ConditionFlag
/// </summary>
WaitingToVisitOtherWorld = 93,
/// <summary>
/// Unable to execute command while using a parasol.
/// </summary>
[Obsolete("Renamed to UsingFashionAccessory.")]
UsingParasol = 94,
/// <summary>
/// Unable to execute command while using a fashion accessory.
/// </summary>

View file

@ -3,7 +3,6 @@ using System.Numerics;
using Dalamud.Data;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory;
using Dalamud.Utility;
using Lumina.Excel;
@ -69,12 +68,6 @@ public interface IFate : IEquatable<IFate>
/// </summary>
byte Progress { get; }
/// <summary>
/// Gets a value indicating whether this <see cref="Fate"/> has a EXP bonus.
/// </summary>
[Obsolete($"Use {nameof(HasBonus)} instead")]
bool HasExpBonus { get; }
/// <summary>
/// Gets a value indicating whether this <see cref="Fate"/> has a bonus.
/// </summary>
@ -222,10 +215,6 @@ internal unsafe partial class Fate : IFate
/// <inheritdoc/>
public byte Progress => this.Struct->Progress;
/// <inheritdoc/>
[Api13ToDo("Remove")]
public bool HasExpBonus => this.HasBonus;
/// <inheritdoc/>
public bool HasBonus => this.Struct->IsBonus;

View file

@ -1,13 +1,11 @@
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.System.Input;
using ImGuiNET;
using Serilog;
namespace Dalamud.Game.ClientState.GamePad;

View file

@ -66,6 +66,24 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
/// <inheritdoc/>
public int Length => objectTableLength;
/// <inheritdoc/>
public IEnumerable<IBattleChara> PlayerObjects => this.GetPlayerObjects();
/// <inheritdoc/>
public IEnumerable<IGameObject> CharacterManagerObjects => this.GetObjectsInRange(..199);
/// <inheritdoc/>
public IEnumerable<IGameObject> ClientObjects => this.GetObjectsInRange(200..448);
/// <inheritdoc/>
public IEnumerable<IGameObject> EventObjects => this.GetObjectsInRange(449..488);
/// <inheritdoc/>
public IEnumerable<IGameObject> StandObjects => this.GetObjectsInRange(489..628);
/// <inheritdoc/>
public IEnumerable<IGameObject> ReactionEventObjects => this.GetObjectsInRange(629..728);
/// <inheritdoc/>
public IGameObject? this[int index]
{
@ -146,6 +164,28 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
};
}
private IEnumerable<IBattleChara> GetPlayerObjects()
{
for (var index = 0; index < 200; index += 2)
{
if (this[index] is IBattleChara { ObjectKind: ObjectKind.Player } gameObject)
{
yield return gameObject;
}
}
}
private IEnumerable<IGameObject> GetObjectsInRange(Range range)
{
for (var index = range.Start.Value; index <= range.End.Value; index++)
{
if (this[index] is { } gameObject)
{
yield return gameObject;
}
}
}
/// <summary>Stores an object table entry, with preallocated concrete types.</summary>
/// <remarks>Initializes a new instance of the <see cref="CachedEntry"/> struct.</remarks>
/// <param name="gameObjectPtr">A pointer to the object table entry this entry should be pointing to.</param>

View file

@ -77,10 +77,10 @@ internal unsafe class BattleChara : Character, IBattleChara
public StatusList StatusList => new(this.Struct->GetStatusManager());
/// <inheritdoc/>
public bool IsCasting => this.Struct->GetCastInfo()->IsCasting > 0;
public bool IsCasting => this.Struct->GetCastInfo()->IsCasting;
/// <inheritdoc/>
public bool IsCastInterruptible => this.Struct->GetCastInfo()->Interruptible > 0;
public bool IsCastInterruptible => this.Struct->GetCastInfo()->Interruptible;
/// <inheritdoc/>
public byte CastActionType => (byte)this.Struct->GetCastInfo()->ActionType;

View file

@ -128,7 +128,7 @@ internal unsafe class PartyMember : IPartyMember
/// <summary>
/// Gets the position of the party member.
/// </summary>
public Vector3 Position => new(this.Struct->X, this.Struct->Y, this.Struct->Z);
public Vector3 Position => this.Struct->Position;
/// <summary>
/// Gets the content ID of the party member.

View file

@ -55,7 +55,7 @@ public unsafe class Status
/// <summary>
/// Gets the source ID of this status.
/// </summary>
public uint SourceId => this.Struct->SourceId;
public uint SourceId => this.Struct->SourceObject.ObjectId;
/// <summary>
/// Gets the source actor associated with this status.

View file

@ -1031,6 +1031,13 @@ public enum SystemConfigOption
[GameConfigOption("TitleScreenType", ConfigType.UInt)]
TitleScreenType,
/// <summary>
/// System option with the internal name DisplayObjectLimitType2.
/// This option is a UInt.
/// </summary>
[GameConfigOption("DisplayObjectLimitType2", ConfigType.UInt)]
DisplayObjectLimitType2,
/// <summary>
/// System option with the internal name AccessibilitySoundVisualEnable.
/// This option is a UInt.
@ -1115,6 +1122,13 @@ public enum SystemConfigOption
[GameConfigOption("CameraZoom", ConfigType.UInt)]
CameraZoom,
/// <summary>
/// System option with the internal name DynamicAroundRangeMode.
/// This option is a UInt.
/// </summary>
[GameConfigOption("DynamicAroundRangeMode", ConfigType.UInt)]
DynamicAroundRangeMode,
/// <summary>
/// System option with the internal name DynamicRezoType.
/// This option is a UInt.

View file

@ -2032,6 +2032,13 @@ public enum UiConfigOption
[GameConfigOption("NamePlateDispJobIconInInstanceOther", ConfigType.UInt)]
NamePlateDispJobIconInInstanceOther,
/// <summary>
/// UiConfig option with the internal name LogNamePlateDispEnemyCast.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogNamePlateDispEnemyCast", ConfigType.UInt)]
LogNamePlateDispEnemyCast,
/// <summary>
/// UiConfig option with the internal name ActiveInfo.
/// This option is a UInt.
@ -2690,6 +2697,594 @@ public enum UiConfigOption
[GameConfigOption("LogColorOtherClass", ConfigType.UInt)]
LogColorOtherClass,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleEnableChatBubble.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleEnableChatBubble", ConfigType.UInt)]
LogChatBubbleEnableChatBubble,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleShowMax.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleShowMax", ConfigType.UInt)]
LogChatBubbleShowMax,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleShowOwnMessage.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleShowOwnMessage", ConfigType.UInt)]
LogChatBubbleShowOwnMessage,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleShowDuringBattle.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleShowDuringBattle", ConfigType.UInt)]
LogChatBubbleShowDuringBattle,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleSizeType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleSizeType", ConfigType.UInt)]
LogChatBubbleSizeType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleShowLargePvP.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleShowLargePvP", ConfigType.UInt)]
LogChatBubbleShowLargePvP,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleShowQuickChat.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleShowQuickChat", ConfigType.UInt)]
LogChatBubbleShowQuickChat,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleDispRowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleDispRowType", ConfigType.UInt)]
LogChatBubbleDispRowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleSayShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleSayShowType", ConfigType.UInt)]
LogChatBubbleSayShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleSayFontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleSayFontColor", ConfigType.UInt)]
LogChatBubbleSayFontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleSayWindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleSayWindowColor", ConfigType.UInt)]
LogChatBubbleSayWindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleYellShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleYellShowType", ConfigType.UInt)]
LogChatBubbleYellShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleYellFontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleYellFontColor", ConfigType.UInt)]
LogChatBubbleYellFontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleYellWindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleYellWindowColor", ConfigType.UInt)]
LogChatBubbleYellWindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleShoutShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleShoutShowType", ConfigType.UInt)]
LogChatBubbleShoutShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleShoutFontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleShoutFontColor", ConfigType.UInt)]
LogChatBubbleShoutFontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleShoutWindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleShoutWindowColor", ConfigType.UInt)]
LogChatBubbleShoutWindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleTellShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleTellShowType", ConfigType.UInt)]
LogChatBubbleTellShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleTellFontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleTellFontColor", ConfigType.UInt)]
LogChatBubbleTellFontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleTellWindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleTellWindowColor", ConfigType.UInt)]
LogChatBubbleTellWindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubblePartyShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubblePartyShowType", ConfigType.UInt)]
LogChatBubblePartyShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubblePartyFontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubblePartyFontColor", ConfigType.UInt)]
LogChatBubblePartyFontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubblePartyWindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubblePartyWindowColor", ConfigType.UInt)]
LogChatBubblePartyWindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleAllianceShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleAllianceShowType", ConfigType.UInt)]
LogChatBubbleAllianceShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleAllianceFontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleAllianceFontColor", ConfigType.UInt)]
LogChatBubbleAllianceFontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleAllianceWindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleAllianceWindowColor", ConfigType.UInt)]
LogChatBubbleAllianceWindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleFcShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleFcShowType", ConfigType.UInt)]
LogChatBubbleFcShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleFcFontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleFcFontColor", ConfigType.UInt)]
LogChatBubbleFcFontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleFcWindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleFcWindowColor", ConfigType.UInt)]
LogChatBubbleFcWindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleBeginnerShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleBeginnerShowType", ConfigType.UInt)]
LogChatBubbleBeginnerShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleBeginnerFontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleBeginnerFontColor", ConfigType.UInt)]
LogChatBubbleBeginnerFontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleBeginnerWindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleBeginnerWindowColor", ConfigType.UInt)]
LogChatBubbleBeginnerWindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubblePvpteamShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubblePvpteamShowType", ConfigType.UInt)]
LogChatBubblePvpteamShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubblePvpteamFontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubblePvpteamFontColor", ConfigType.UInt)]
LogChatBubblePvpteamFontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubblePvpteamWindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubblePvpteamWindowColor", ConfigType.UInt)]
LogChatBubblePvpteamWindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs1ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs1ShowType", ConfigType.UInt)]
LogChatBubbleLs1ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs1FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs1FontColor", ConfigType.UInt)]
LogChatBubbleLs1FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs1WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs1WindowColor", ConfigType.UInt)]
LogChatBubbleLs1WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs2ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs2ShowType", ConfigType.UInt)]
LogChatBubbleLs2ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs2FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs2FontColor", ConfigType.UInt)]
LogChatBubbleLs2FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs2WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs2WindowColor", ConfigType.UInt)]
LogChatBubbleLs2WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs3ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs3ShowType", ConfigType.UInt)]
LogChatBubbleLs3ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs3FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs3FontColor", ConfigType.UInt)]
LogChatBubbleLs3FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs3WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs3WindowColor", ConfigType.UInt)]
LogChatBubbleLs3WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs4ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs4ShowType", ConfigType.UInt)]
LogChatBubbleLs4ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs4FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs4FontColor", ConfigType.UInt)]
LogChatBubbleLs4FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs4WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs4WindowColor", ConfigType.UInt)]
LogChatBubbleLs4WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs5ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs5ShowType", ConfigType.UInt)]
LogChatBubbleLs5ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs5FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs5FontColor", ConfigType.UInt)]
LogChatBubbleLs5FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs5WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs5WindowColor", ConfigType.UInt)]
LogChatBubbleLs5WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs6ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs6ShowType", ConfigType.UInt)]
LogChatBubbleLs6ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs6FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs6FontColor", ConfigType.UInt)]
LogChatBubbleLs6FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs6WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs6WindowColor", ConfigType.UInt)]
LogChatBubbleLs6WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs7ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs7ShowType", ConfigType.UInt)]
LogChatBubbleLs7ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs7FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs7FontColor", ConfigType.UInt)]
LogChatBubbleLs7FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs7WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs7WindowColor", ConfigType.UInt)]
LogChatBubbleLs7WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs8ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs8ShowType", ConfigType.UInt)]
LogChatBubbleLs8ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs8FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs8FontColor", ConfigType.UInt)]
LogChatBubbleLs8FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleLs8WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleLs8WindowColor", ConfigType.UInt)]
LogChatBubbleLs8WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls1ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls1ShowType", ConfigType.UInt)]
LogChatBubbleCwls1ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls1FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls1FontColor", ConfigType.UInt)]
LogChatBubbleCwls1FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls1WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls1WindowColor", ConfigType.UInt)]
LogChatBubbleCwls1WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls2ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls2ShowType", ConfigType.UInt)]
LogChatBubbleCwls2ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls2FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls2FontColor", ConfigType.UInt)]
LogChatBubbleCwls2FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls2WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls2WindowColor", ConfigType.UInt)]
LogChatBubbleCwls2WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls3ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls3ShowType", ConfigType.UInt)]
LogChatBubbleCwls3ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls3FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls3FontColor", ConfigType.UInt)]
LogChatBubbleCwls3FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls3WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls3WindowColor", ConfigType.UInt)]
LogChatBubbleCwls3WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls4ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls4ShowType", ConfigType.UInt)]
LogChatBubbleCwls4ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls4FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls4FontColor", ConfigType.UInt)]
LogChatBubbleCwls4FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls4WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls4WindowColor", ConfigType.UInt)]
LogChatBubbleCwls4WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls5ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls5ShowType", ConfigType.UInt)]
LogChatBubbleCwls5ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls5FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls5FontColor", ConfigType.UInt)]
LogChatBubbleCwls5FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls5WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls5WindowColor", ConfigType.UInt)]
LogChatBubbleCwls5WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls6ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls6ShowType", ConfigType.UInt)]
LogChatBubbleCwls6ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls6FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls6FontColor", ConfigType.UInt)]
LogChatBubbleCwls6FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls6WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls6WindowColor", ConfigType.UInt)]
LogChatBubbleCwls6WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls7ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls7ShowType", ConfigType.UInt)]
LogChatBubbleCwls7ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls7FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls7FontColor", ConfigType.UInt)]
LogChatBubbleCwls7FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls7WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls7WindowColor", ConfigType.UInt)]
LogChatBubbleCwls7WindowColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls8ShowType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls8ShowType", ConfigType.UInt)]
LogChatBubbleCwls8ShowType,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls8FontColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls8FontColor", ConfigType.UInt)]
LogChatBubbleCwls8FontColor,
/// <summary>
/// UiConfig option with the internal name LogChatBubbleCwls8WindowColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogChatBubbleCwls8WindowColor", ConfigType.UInt)]
LogChatBubbleCwls8WindowColor,
/// <summary>
/// UiConfig option with the internal name LogPermeationRateInput.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogPermeationRateInput", ConfigType.UInt)]
LogPermeationRateInput,
/// <summary>
/// UiConfig option with the internal name ChatType.
/// This option is a UInt.
@ -3453,6 +4048,27 @@ public enum UiConfigOption
[GameConfigOption("GPoseMotionFilterAction", ConfigType.UInt)]
GPoseMotionFilterAction,
/// <summary>
/// UiConfig option with the internal name GPoseRollRotationCameraCorrection.
/// This option is a UInt.
/// </summary>
[GameConfigOption("GPoseRollRotationCameraCorrection", ConfigType.UInt)]
GPoseRollRotationCameraCorrection,
/// <summary>
/// UiConfig option with the internal name GPoseInitCameraFocus.
/// This option is a UInt.
/// </summary>
[GameConfigOption("GPoseInitCameraFocus", ConfigType.UInt)]
GPoseInitCameraFocus,
/// <summary>
/// UiConfig option with the internal name GposePortraitRotateType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("GposePortraitRotateType", ConfigType.UInt)]
GposePortraitRotateType,
/// <summary>
/// UiConfig option with the internal name LsListSortPriority.
/// This option is a UInt.

View file

@ -12,6 +12,7 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Memory;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
@ -50,6 +51,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
private ImmutableDictionary<(string PluginName, uint CommandId), Action<uint, SeString>>? dalamudLinkHandlersCopy;
private uint dalamudChatHandlerId = 1000;
[ServiceManager.ServiceConstructor]
private ChatGui()
@ -161,6 +163,42 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
#endregion
#region Chat Links
/// <summary>
/// Register a chat link handler.
/// </summary>
/// <remarks>Internal use only.</remarks>
/// <param name="commandAction">The action to be executed.</param>
/// <returns>Returns an SeString payload for the link.</returns>
public DalamudLinkPayload AddChatLinkHandler(Action<uint, SeString> commandAction)
{
return this.AddChatLinkHandler("Dalamud", this.dalamudChatHandlerId++, commandAction);
}
/// <inheritdoc/>
/// <remarks>Internal use only.</remarks>
public DalamudLinkPayload AddChatLinkHandler(uint commandId, Action<uint, SeString> commandAction)
{
return this.AddChatLinkHandler("Dalamud", commandId, commandAction);
}
/// <inheritdoc/>
/// <remarks>Internal use only.</remarks>
public void RemoveChatLinkHandler(uint commandId)
{
this.RemoveChatLinkHandler("Dalamud", commandId);
}
/// <inheritdoc/>
/// <remarks>Internal use only.</remarks>
public void RemoveChatLinkHandler()
{
this.RemoveChatLinkHandler("Dalamud");
}
#endregion
/// <summary>
/// Process a chat queue.
/// </summary>
@ -220,7 +258,6 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
/// <param name="commandId">The ID of the command to run.</param>
/// <param name="commandAction">The command action itself.</param>
/// <returns>A payload for handling.</returns>
[Api13ToDo("Plugins should not specify their own command IDs here. We should assign them ourselves.")]
internal DalamudLinkPayload AddChatLinkHandler(string pluginName, uint commandId, Action<uint, SeString> commandAction)
{
var payload = new DalamudLinkPayload { Plugin = pluginName, CommandId = commandId };
@ -478,11 +515,15 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
[ServiceManager.ServiceDependency]
private readonly ChatGui chatGuiService = Service<ChatGui>.Get();
private readonly LocalPlugin plugin;
/// <summary>
/// Initializes a new instance of the <see cref="ChatGuiPluginScoped"/> class.
/// </summary>
internal ChatGuiPluginScoped()
/// <param name="plugin">The plugin.</param>
internal ChatGuiPluginScoped(LocalPlugin plugin)
{
this.plugin = plugin;
this.chatGuiService.ChatMessage += this.OnMessageForward;
this.chatGuiService.CheckMessageHandled += this.OnCheckMessageForward;
this.chatGuiService.ChatMessageHandled += this.OnMessageHandledForward;
@ -524,6 +565,18 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
this.ChatMessageUnhandled = null;
}
/// <inheritdoc/>
public DalamudLinkPayload AddChatLinkHandler(uint commandId, Action<uint, SeString> commandAction)
=> this.chatGuiService.AddChatLinkHandler(this.plugin.InternalName, commandId, commandAction);
/// <inheritdoc/>
public void RemoveChatLinkHandler(uint commandId)
=> this.chatGuiService.RemoveChatLinkHandler(this.plugin.InternalName, commandId);
/// <inheritdoc/>
public void RemoveChatLinkHandler()
=> this.chatGuiService.RemoveChatLinkHandler(this.plugin.InternalName);
/// <inheritdoc/>
public void Print(XivChatEntry chat)
=> this.chatGuiService.Print(chat);

View file

@ -451,14 +451,14 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
case ContextMenuType.Default:
{
var ownerAddonId = ((AgentContext*)this.SelectedAgent)->OwnerAddon;
module->OpenAddon(this.AddonContextSubNameId, (uint)valueCount, values, this.SelectedAgent, 71, checked((ushort)ownerAddonId), 4);
module->OpenAddon(this.AddonContextSubNameId, (uint)valueCount, values, &this.SelectedAgent->AtkEventInterface, 71, checked((ushort)ownerAddonId), 4);
break;
}
case ContextMenuType.Inventory:
{
var ownerAddonId = ((AgentInventoryContext*)this.SelectedAgent)->OwnerAddonId;
module->OpenAddon(this.AddonContextSubNameId, (uint)valueCount, values, this.SelectedAgent, 0, checked((ushort)ownerAddonId), 4);
module->OpenAddon(this.AddonContextSubNameId, (uint)valueCount, values, &this.SelectedAgent->AtkEventInterface, 0, checked((ushort)ownerAddonId), 4);
break;
}

View file

@ -5,6 +5,7 @@ using System.Threading;
using Dalamud.Configuration.Internal;
using Dalamud.Game.Addon.Events;
using Dalamud.Game.Addon.Events.EventDataTypes;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.Text.SeStringHandling;
@ -330,7 +331,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
this.entriesReadOnlyCopy = null;
}
private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR").ToPointer();
private AtkUnitBase* GetDtr() => this.gameGui.GetAddonByName("_DTR").Struct;
private void Update(IFramework unused)
{
@ -427,7 +428,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
private void FixCollision(AddonEvent eventType, AddonArgs addonInfo)
{
var addon = (AtkUnitBase*)addonInfo.Addon;
var addon = addonInfo.Addon.Struct;
if (addon->RootNode is null || addon->UldManager.NodeList is null) return;
float minX = addon->RootNode->Width;
@ -585,8 +586,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
newTextNode->LineSpacing = 12;
newTextNode->AlignmentFontType = 5;
newTextNode->FontSize = 14;
newTextNode->TextFlags = (byte)TextFlags.Edge;
newTextNode->TextFlags2 = 0;
newTextNode->TextFlags = TextFlags.Edge;
if (this.emptyString == null)
this.emptyString = Utf8String.FromString(" ");
@ -596,24 +596,13 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
newTextNode->TextColor = new ByteColor { R = 255, G = 255, B = 255, A = 255 };
newTextNode->EdgeColor = new ByteColor { R = 142, G = 106, B = 12, A = 255 };
// ICreatable was restored, this may be necessary if AtkUldManager.CreateAtkTextNode(); is used instead of Create<T>
// // Memory is filled with random data after being created, zero out some things to avoid issues.
// newTextNode->UnkPtr_1 = null;
// newTextNode->SelectStart = 0;
// newTextNode->SelectEnd = 0;
// newTextNode->FontCacheHandle = 0;
// newTextNode->CharSpacing = 0;
// newTextNode->BackgroundColor = new ByteColor { R = 0, G = 0, B = 0, A = 0 };
// newTextNode->TextId = 0;
// newTextNode->SheetType = 0;
return newTextNode;
}
private void DtrEventHandler(AddonEventType atkEventType, IntPtr atkUnitBase, IntPtr atkResNode)
private void DtrEventHandler(AddonEventType atkEventType, AddonEventData eventData)
{
var addon = (AtkUnitBase*)atkUnitBase;
var node = (AtkResNode*)atkResNode;
var addon = (AtkUnitBase*)eventData.AddonPointer;
var node = (AtkResNode*)eventData.NodeTargetPointer;
DtrBarEntry? dtrBarEntry = null;
this.entriesLock.EnterReadLock();
@ -652,7 +641,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
break;
case AddonEventType.MouseClick:
dtrBarEntry.OnClick.Invoke();
dtrBarEntry.OnClick?.Invoke(new AddonMouseEventData(eventData));
break;
}
}

View file

@ -1,4 +1,5 @@
using Dalamud.Configuration.Internal;
using Dalamud.Game.Addon.Events.EventDataTypes;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility;
@ -42,12 +43,6 @@ public interface IReadOnlyDtrBarEntry
/// Gets a value indicating whether the user has hidden this entry from view through the Dalamud settings.
/// </summary>
public bool UserHidden { get; }
/// <summary>
/// Triggers the click action of this entry.
/// </summary>
/// <returns>True, if a click action was registered and executed.</returns>
public bool TriggerClickAction();
}
/// <summary>
@ -71,9 +66,9 @@ public interface IDtrBarEntry : IReadOnlyDtrBarEntry
public new bool Shown { get; set; }
/// <summary>
/// Gets or sets a action to be invoked when the user clicks on the dtr entry.
/// Gets or sets an action to be invoked when the user clicks on the dtr entry.
/// </summary>
public Action? OnClick { get; set; }
public Action<AddonMouseEventData>? OnClick { get; set; }
/// <summary>
/// Remove this entry from the bar.
@ -122,10 +117,8 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
/// <inheritdoc cref="IDtrBarEntry.Tooltip" />
public SeString? Tooltip { get; set; }
/// <summary>
/// Gets or sets a action to be invoked when the user clicks on the dtr entry.
/// </summary>
public Action? OnClick { get; set; }
/// <inheritdoc/>
public Action<AddonMouseEventData>? OnClick { get; set; }
/// <inheritdoc/>
public bool HasClickAction => this.OnClick != null;
@ -145,7 +138,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
}
/// <inheritdoc/>
[Api13ToDo("Maybe make this config scoped to internalname?")]
[Api13ToDo("Maybe make this config scoped to internal name?")]
public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false;
/// <summary>
@ -178,16 +171,6 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
/// </summary>
internal LocalPlugin? OwnerPlugin { get; set; }
/// <inheritdoc/>
public bool TriggerClickAction()
{
if (this.OnClick == null)
return false;
this.OnClick.Invoke();
return true;
}
/// <summary>
/// Remove this entry from the bar.
/// You will need to re-acquire it from DtrBar to reuse it.

View file

@ -1,5 +1,7 @@
using System.Runtime.InteropServices;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.NativeWrapper;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Hooking;
using Dalamud.Interface.Utility;
@ -18,8 +20,6 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Common.Component.BGCollision;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using Vector2 = System.Numerics.Vector2;
using Vector3 = System.Numerics.Vector3;
using Vector4 = System.Numerics.Vector4;
@ -167,79 +167,59 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
}
/// <inheritdoc/>
public IntPtr GetUIModule()
public UIModulePtr GetUIModule()
{
var framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance();
if (framework == null)
return IntPtr.Zero;
var uiModule = framework->GetUIModule();
if (uiModule == null)
return IntPtr.Zero;
return (IntPtr)uiModule;
return (nint)UIModule.Instance();
}
/// <inheritdoc/>
public IntPtr GetAddonByName(string name, int index = 1)
public AtkUnitBasePtr GetAddonByName(string name, int index = 1)
{
var atkStage = AtkStage.Instance();
if (atkStage == null)
return IntPtr.Zero;
var unitManager = RaptureAtkUnitManager.Instance();
if (unitManager == null)
return 0;
var unitMgr = atkStage->RaptureAtkUnitManager;
if (unitMgr == null)
return IntPtr.Zero;
var addon = unitMgr->GetAddonByName(name, index);
if (addon == null)
return IntPtr.Zero;
return (IntPtr)addon;
return (nint)unitManager->GetAddonByName(name, index);
}
/// <inheritdoc/>
public IntPtr FindAgentInterface(string addonName)
public AgentInterfacePtr GetAgentById(int id)
{
var agentModule = AgentModule.Instance();
if (agentModule == null || id < 0 || id >= agentModule->Agents.Length)
return 0;
return (nint)agentModule->Agents[id].Value;
}
/// <inheritdoc/>
public AgentInterfacePtr FindAgentInterface(string addonName)
{
var addon = this.GetAddonByName(addonName);
return this.FindAgentInterface(addon);
}
/// <inheritdoc/>
public IntPtr FindAgentInterface(void* addon) => this.FindAgentInterface((IntPtr)addon);
/// <inheritdoc/>
public IntPtr FindAgentInterface(IntPtr addonPtr)
public AgentInterfacePtr FindAgentInterface(AtkUnitBasePtr addon)
{
if (addonPtr == IntPtr.Zero)
return IntPtr.Zero;
if (addon.IsNull)
return 0;
var uiModule = (UIModule*)this.GetUIModule();
if (uiModule == null)
return IntPtr.Zero;
var agentModule = uiModule->GetAgentModule();
var agentModule = AgentModule.Instance();
if (agentModule == null)
return IntPtr.Zero;
var addon = (AtkUnitBase*)addonPtr;
var addonId = addon->ParentId == 0 ? addon->Id : addon->ParentId;
return 0;
var addonId = addon.ParentId == 0 ? addon.Id : addon.ParentId;
if (addonId == 0)
return IntPtr.Zero;
return 0;
var index = 0;
while (true)
foreach (AgentInterface* agent in agentModule->Agents)
{
var agent = agentModule->GetAgentByInternalId((AgentId)index++);
if (agent == uiModule || agent == null)
break;
if (agent->AddonId == addonId)
return new IntPtr(agent);
if (agent != null && agent->AddonId == addonId)
return (nint)agent;
}
return IntPtr.Zero;
return 0;
}
/// <summary>
@ -315,7 +295,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
return ret;
}
private void HandleActionHoverDetour(AgentActionDetail* hoverState, FFXIVClientStructs.FFXIV.Client.UI.Agent.ActionKind actionKind, uint actionId, int a4, byte a5)
private void HandleActionHoverDetour(AgentActionDetail* hoverState, FFXIVClientStructs.FFXIV.Client.UI.Agent.ActionKind actionKind, uint actionId, int a4, bool a5)
{
this.handleActionHoverHook.Original(hoverState, actionKind, actionId, a4, a5);
this.HoveredAction.ActionKind = (HoverActionKind)actionKind;
@ -454,25 +434,25 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui
=> this.gameGuiService.ScreenToWorld(screenPos, out worldPos, rayDistance);
/// <inheritdoc/>
public IntPtr GetUIModule()
public UIModulePtr GetUIModule()
=> this.gameGuiService.GetUIModule();
/// <inheritdoc/>
public IntPtr GetAddonByName(string name, int index = 1)
public AtkUnitBasePtr GetAddonByName(string name, int index = 1)
=> this.gameGuiService.GetAddonByName(name, index);
/// <inheritdoc/>
public IntPtr FindAgentInterface(string addonName)
public AgentInterfacePtr GetAgentById(int id)
=> this.gameGuiService.GetAgentById(id);
/// <inheritdoc/>
public AgentInterfacePtr FindAgentInterface(string addonName)
=> this.gameGuiService.FindAgentInterface(addonName);
/// <inheritdoc/>
public unsafe IntPtr FindAgentInterface(void* addon)
public AgentInterfacePtr FindAgentInterface(AtkUnitBasePtr addon)
=> this.gameGuiService.FindAgentInterface(addon);
/// <inheritdoc/>
public IntPtr FindAgentInterface(IntPtr addonPtr)
=> this.gameGuiService.FindAgentInterface(addonPtr);
private void UiHideToggledForward(object sender, bool toggled) => this.UiHideToggled?.Invoke(sender, toggled);
private void HoveredItemForward(object sender, ulong itemId) => this.HoveredItemChanged?.Invoke(sender, itemId);

View file

@ -72,7 +72,7 @@ internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui
/// <inheritdoc/>
public unsafe void RequestRedraw()
{
var addon = (AddonNamePlate*)this.gameGui.GetAddonByName("NamePlate");
var addon = (AddonNamePlate*)(nint)this.gameGui.GetAddonByName("NamePlate");
if (addon != null)
{
addon->DoFullUpdate = 1;

View file

@ -127,7 +127,7 @@ public enum JobFlags : ulong
RedMage = 1ul << 24,
/// <summary>
/// Blue mage (BLM).
/// Blue mage (BLU).
/// </summary>
BlueMage = 1ul << 25,

View file

@ -1,322 +0,0 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.Command;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.Completion;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Internal;
/// <summary>
/// This class adds dalamud and plugin commands to the chat box's autocompletion.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class Completion : IInternalDisposableService
{
// 0xFF is a magic group number that causes CompletionModule's internals to treat entries
// as raw strings instead of as lookups into an EXD sheet
private const int GroupNumber = 0xFF;
[ServiceManager.ServiceDependency]
private readonly CommandManager commandManager = Service<CommandManager>.Get();
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private readonly Dictionary<string, EntryStrings> cachedCommands = [];
private readonly ConcurrentQueue<string> addedCommands = [];
private EntryStrings? dalamudCategory;
private Hook<CompletionModule.Delegates.GetSelection>? getSelection;
// This is marked volatile since we set and check it from different threads. Instead of using a synchronization
// primitive, a volatile is sufficient since the absolute worst case is that we delay one extra frame to reset
// the list, which is fine
private volatile bool needsClear;
private bool disposed;
private nint wantedVtblPtr;
/// <summary>
/// Initializes a new instance of the <see cref="Completion"/> class.
/// </summary>
[ServiceManager.ServiceConstructor]
internal Completion()
{
this.commandManager.CommandAdded += this.OnCommandAdded;
this.commandManager.CommandRemoved += this.OnCommandRemoved;
this.framework.Update += this.OnUpdate;
}
/// <summary>Finalizes an instance of the <see cref="Completion"/> class.</summary>
~Completion() => this.Dispose(false);
/// <inheritdoc/>
void IInternalDisposableService.DisposeService() => this.Dispose(true);
private static AtkUnitBase* FindOwningAddon(AtkComponentTextInput* component)
{
if (component == null) return null;
var node = (AtkResNode*)component->OwnerNode;
if (node == null) return null;
while (node->ParentNode != null)
node = node->ParentNode;
foreach (var addon in RaptureAtkUnitManager.Instance()->AllLoadedUnitsList.Entries)
{
if (addon.Value->RootNode == node)
return addon;
}
return null;
}
private AtkComponentTextInput* GetActiveTextInput()
{
var mod = RaptureAtkModule.Instance();
if (mod == null) return null;
var basePtr = mod->TextInput.TargetTextInputEventInterface;
if (basePtr == null) return null;
// Once CS has an implementation for multiple inheritance, we can remove this sig from dalamud
// as well as the nasty pointer arithmetic below. But for now, we need to do this manually.
// The AtkTextInputEventInterface* is the secondary base class for AtkComponentTextInput*
// so the pointer is sizeof(AtkComponentInputBase) into the object. We verify that we're looking
// at the object we think we are by confirming the pointed-to vtbl matches the known secondary vtbl for
// AtkComponentTextInput, and if it does, we can shift the pointer back to get the start of our text input
if (this.wantedVtblPtr == 0)
{
this.wantedVtblPtr =
Service<TargetSigScanner>.Get().GetStaticAddressFromSig(
"48 89 01 48 8D 05 ?? ?? ?? ?? 48 89 81 ?? ?? ?? ?? 48 8D 05 ?? ?? ?? ?? 48 89 81 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 8B 48 68",
4);
}
var vtblPtr = *(nint*)basePtr;
if (vtblPtr != this.wantedVtblPtr) return null;
// This needs to be updated if the layout/base order of AtkComponentTextInput changes
return (AtkComponentTextInput*)((AtkComponentInputBase*)basePtr - 1);
}
private bool AllowCompletion(string cmd)
{
// this is one of our commands, let's see if we should allow this to be completed
var component = this.GetActiveTextInput();
// ContainingAddon or ContainingAddon2 aren't always populated, but they
// seem to be in any case where this is actually a completable AtkComponentTextInput
// In the worst case, we can walk the AtkNode tree- but let's try the easy pointers first
var addon = component->ContainingAddon;
if (addon == null) addon = component->ContainingAddon2;
if (addon == null) addon = FindOwningAddon(component);
if (addon == null || addon->NameString != "ChatLog")
{
// we don't know what addon is completing, or we know it isn't ChatLog
// either way, we should just reject this completion
return false;
}
// We're in ChatLog, so check if this is the start of the text input
// AtkComponentTextInput->UnkText1 is the evaluated version of the current text
// so if the command starts with that, then either it's empty or a prefix completion.
// In either case, we're happy to allow completion.
return cmd.StartsWith(component->UnkText1.StringPtr.ExtractText());
}
private void Dispose(bool disposing)
{
if (this.disposed)
return;
if (disposing)
{
this.getSelection?.Disable();
this.getSelection?.Dispose();
this.framework.Update -= this.OnUpdate;
this.commandManager.CommandAdded -= this.OnCommandAdded;
this.commandManager.CommandRemoved -= this.OnCommandRemoved;
this.dalamudCategory?.Dispose();
this.ClearCachedCommands();
}
this.disposed = true;
}
private void OnCommandAdded(object? sender, CommandManager.CommandEventArgs e)
{
if (e.CommandInfo.ShowInHelp)
this.addedCommands.Enqueue(e.Command);
}
private void OnCommandRemoved(object? sender, CommandManager.CommandEventArgs e) => this.needsClear = true;
private void OnUpdate(IFramework fw)
{
var atkModule = RaptureAtkModule.Instance();
if (atkModule == null) return;
var textInput = &atkModule->TextInput;
if (textInput->CompletionModule == null) return;
// Before we change _anything_ we need to check the state of the UI- if the completion list is open
// changes to the underlying data are extremely unsafe, so we'll just wait until the next frame
// worst case, someone tries to complete a command that _just_ got unloaded so it won't do anything
// but that's the same as making a typo, really
if (textInput->CompletionDepth > 0) return;
// Create the category for Dalamud commands.
// This needs to be done here, since we cannot create Utf8Strings before the game
// has initialized (no allocator set up yet).
this.dalamudCategory ??= new EntryStrings("【Dalamud】");
this.LoadCommands(textInput->CompletionModule);
}
private CategoryData* EnsureCategoryData(CompletionModule* module)
{
if (module == null) return null;
if (this.getSelection == null)
{
this.getSelection = Hook<CompletionModule.Delegates.GetSelection>.FromAddress(
(IntPtr)module->VirtualTable->GetSelection,
this.GetSelectionDetour);
this.getSelection.Enable();
}
for (var i = 0; i < module->CategoryNames.Count; i++)
{
if (module->CategoryNames[i].AsReadOnlySeStringSpan().ContainsText("【Dalamud】"u8))
{
return module->CategoryData[i];
}
}
// Create the category since we don't have one
var categoryData = (CategoryData*)Memory.MemoryHelper.GameAllocateDefault((ulong)sizeof(CategoryData));
categoryData->Ctor(GroupNumber, 0xFF);
module->AddCategoryData(GroupNumber, this.dalamudCategory!.Display->StringPtr,
this.dalamudCategory.Match->StringPtr, categoryData);
return categoryData;
}
private void ClearCachedCommands()
{
if (this.cachedCommands.Count == 0)
return;
foreach (var entry in this.cachedCommands.Values)
{
entry.Dispose();
}
this.cachedCommands.Clear();
}
private void LoadCommands(CompletionModule* completionModule)
{
if (completionModule == null) return;
if (completionModule->CategoryNames.Count == 0) return; // We want this data populated first
if (this.needsClear && this.cachedCommands.Count > 0)
{
this.needsClear = false;
completionModule->ClearCompletionData();
this.ClearCachedCommands();
return;
}
var catData = this.EnsureCategoryData(completionModule);
if (catData == null) return;
if (catData->CompletionData.Count == 0)
{
var inputCommands = this.commandManager.Commands.Where(pair => pair.Value.ShowInHelp);
foreach (var (cmd, _) in inputCommands)
AddEntry(cmd);
catData->SortEntries();
return;
}
var needsSort = false;
while (this.addedCommands.TryDequeue(out var cmd))
{
needsSort = true;
AddEntry(cmd);
}
if (needsSort)
catData->SortEntries();
return;
void AddEntry(string cmd)
{
if (this.cachedCommands.ContainsKey(cmd)) return;
var cmdStr = new EntryStrings(cmd);
this.cachedCommands.Add(cmd, cmdStr);
completionModule->AddCompletionEntry(
GroupNumber,
0xFF,
cmdStr.Display->StringPtr,
cmdStr.Match->StringPtr,
0xFF);
}
}
private int GetSelectionDetour(CompletionModule* thisPtr, CategoryData.CompletionDataStruct* dataStructs, int index, Utf8String* outputString, Utf8String* outputDisplayString)
{
var ret = this.getSelection!.Original.Invoke(thisPtr, dataStructs, index, outputString, outputDisplayString);
if (ret != -2 || outputString == null) return ret;
// -2 means it was a plain text final selection, so it might be ours
// Unfortunately, the code that uses this string mangles the color macro for some reason...
// We'll just strip those out since we don't need the color in the chatbox
var txt = outputString->StringPtr.ExtractText();
if (!this.cachedCommands.ContainsKey(txt))
return ret;
if (!this.AllowCompletion(txt))
{
outputString->Clear();
if (outputDisplayString != null) outputDisplayString->Clear();
return ret;
}
outputString->SetString(txt + " ");
return ret;
}
private class EntryStrings(string command) : IDisposable
{
public Utf8String* Display { get; } =
Utf8String.FromSequence(new SeStringBuilder().AddUiForeground(command, 539).BuiltString.EncodeWithNullTerminator());
public Utf8String* Match { get; } = Utf8String.FromString(command);
public void Dispose()
{
this.Display->Dtor(true);
this.Match->Dtor(true);
}
}
}

View file

@ -0,0 +1,279 @@
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.Command;
using Dalamud.Hooking;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.Completion;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Lumina.Text;
namespace Dalamud.Game.Internal;
/// <summary>
/// This class adds Dalamud and plugin commands to the chat box's autocompletion.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class DalamudCompletion : IInternalDisposableService
{
// 0xFF is a magic group number that causes CompletionModule's internals to treat entries
// as raw strings instead of as lookups into an EXD sheet
private const int GroupNumber = 0xFF;
[ServiceManager.ServiceDependency]
private readonly CommandManager commandManager = Service<CommandManager>.Get();
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private readonly Dictionary<string, EntryStrings> cachedCommands = [];
private EntryStrings? dalamudCategory;
private Hook<AtkTextInput.Delegates.OpenCompletion> openSuggestionsHook;
private Hook<CompletionModule.Delegates.GetSelection>? getSelectionHook;
/// <summary>
/// Initializes a new instance of the <see cref="DalamudCompletion"/> class.
/// </summary>
[ServiceManager.ServiceConstructor]
internal DalamudCompletion()
{
this.framework.RunOnTick(this.Setup);
}
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.openSuggestionsHook?.Disable();
this.openSuggestionsHook?.Dispose();
this.getSelectionHook?.Disable();
this.getSelectionHook?.Dispose();
this.dalamudCategory?.Dispose();
this.ClearCachedCommands();
}
private void Setup()
{
var uiModule = UIModule.Instance();
if (uiModule == null || uiModule->FrameCount == 0)
{
this.framework.RunOnTick(this.Setup);
return;
}
this.dalamudCategory = new EntryStrings("【Dalamud】");
this.openSuggestionsHook = Hook<AtkTextInput.Delegates.OpenCompletion>.FromAddress(
(nint)AtkTextInput.MemberFunctionPointers.OpenCompletion,
this.OpenSuggestionsDetour);
this.getSelectionHook = Hook<CompletionModule.Delegates.GetSelection>.FromAddress(
(nint)uiModule->CompletionModule.VirtualTable->GetSelection,
this.GetSelectionDetour);
this.openSuggestionsHook.Enable();
this.getSelectionHook.Enable();
}
private void OpenSuggestionsDetour(AtkTextInput* thisPtr)
{
this.UpdateCompletionData();
this.openSuggestionsHook!.Original(thisPtr);
}
private int GetSelectionDetour(CompletionModule* thisPtr, CategoryData.CompletionDataStruct* dataStructs, int index, Utf8String* outputString, Utf8String* outputDisplayString)
{
var ret = this.getSelectionHook!.Original.Invoke(thisPtr, dataStructs, index, outputString, outputDisplayString);
this.HandleInsert(ret, outputString, outputDisplayString);
return ret;
}
private void UpdateCompletionData()
{
if (!this.TryGetActiveTextInput(out var component, out var addon))
{
if (this.HasDalamudCategory())
this.ResetCompletionData();
return;
}
var uiModule = UIModule.Instance();
if (uiModule == null)
return;
this.ResetCompletionData();
this.ClearCachedCommands();
var currentText = component->UnkText1.StringPtr.ExtractText();
var commands = this.commandManager.Commands
.Where(kv => kv.Value.ShowInHelp && (currentText.Length == 0 || kv.Key.StartsWith(currentText)))
.OrderBy(kv => kv.Key);
if (!commands.Any())
return;
var categoryData = (CategoryData*)IMemorySpace.GetDefaultSpace()->Malloc((ulong)sizeof(CategoryData), 0x08);
categoryData->Ctor(GroupNumber, 0xFF);
uiModule->CompletionModule.AddCategoryData(
GroupNumber,
this.dalamudCategory!.Display->StringPtr,
this.dalamudCategory.Match->StringPtr, categoryData);
foreach (var (cmd, info) in commands)
{
if (!this.cachedCommands.TryGetValue(cmd, out var entryString))
this.cachedCommands.Add(cmd, entryString = new EntryStrings(cmd));
uiModule->CompletionModule.AddCompletionEntry(
GroupNumber,
0xFF,
entryString.Display->StringPtr,
entryString.Match->StringPtr,
0xFF);
}
categoryData->SortEntries();
}
private void HandleInsert(int ret, Utf8String* outputString, Utf8String* outputDisplayString)
{
// -2 means it was a plain text final selection, so it might be ours.
if (ret != -2 || outputString == null)
return;
// Strip out color payloads that we added to the string.
var txt = outputString->StringPtr.ExtractText();
if (!this.cachedCommands.ContainsKey(txt))
return;
if (!this.TryGetActiveTextInput(out _, out _))
{
outputString->Clear();
if (outputDisplayString != null)
outputDisplayString->Clear();
return;
}
outputString->SetString(txt + ' ');
}
private bool TryGetActiveTextInput(out AtkComponentTextInput* component, out AtkUnitBase* addon)
{
component = null;
addon = null;
var raptureAtkModule = RaptureAtkModule.Instance();
if (raptureAtkModule == null)
return false;
var textInputEventInterface = raptureAtkModule->TextInput.TargetTextInputEventInterface;
if (textInputEventInterface == null)
return false;
var ownerNode = textInputEventInterface->GetOwnerNode();
if (ownerNode == null || ownerNode->GetNodeType() != NodeType.Component)
return false;
var componentNode = (AtkComponentNode*)ownerNode;
var componentBase = componentNode->Component;
if (componentBase == null || componentBase->GetComponentType() != ComponentType.TextInput)
return false;
component = (AtkComponentTextInput*)componentBase;
addon = component->ContainingAddon;
if (addon == null)
addon = component->ContainingAddon2;
if (addon == null)
addon = RaptureAtkUnitManager.Instance()->GetAddonByNode((AtkResNode*)component->OwnerNode);
return addon != null && addon->NameString == "ChatLog";
}
private bool HasDalamudCategory()
{
var uiModule = UIModule.Instance();
if (uiModule == null)
return false;
for (var i = 0; i < uiModule->CompletionModule.CategoryNames.Count; i++)
{
if (uiModule->CompletionModule.CategoryNames[i].AsReadOnlySeStringSpan().ContainsText("【Dalamud】"u8))
{
return true;
}
}
return false;
}
private void ResetCompletionData()
{
var uiModule = UIModule.Instance();
if (uiModule == null)
return;
uiModule->CompletionModule.ClearCompletionData();
// This happens in UIModule.Update. Just repeat it to fill CompletionData back up with defaults.
uiModule->CompletionModule.Update(
&uiModule->CompletionSheetName,
&uiModule->CompletionOpenIconMacro,
&uiModule->CompletionCloseIconMacro,
0);
}
private void ClearCachedCommands()
{
foreach (var entry in this.cachedCommands.Values)
{
entry.Dispose();
}
this.cachedCommands.Clear();
}
private class EntryStrings : IDisposable
{
public EntryStrings(string command)
{
var rssb = SeStringBuilder.SharedPool.Get();
this.Display = Utf8String.FromSequence(rssb
.PushColorType(539)
.Append(command)
.PopColorType()
.GetViewAsSpan());
SeStringBuilder.SharedPool.Return(rssb);
this.Match = Utf8String.FromString(command);
}
public Utf8String* Display { get; }
public Utf8String* Match { get; }
public void Dispose()
{
this.Display->Dtor(true);
this.Match->Dtor(true);
}
}
}

View file

@ -1,7 +1,9 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.Data;
using Dalamud.Game.Inventory.Records;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game;
@ -160,6 +162,29 @@ public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
}
}
/// <summary>
/// Gets a list of materia entries for this item. Exists as a user-friendly interface to <see cref="Materia"/> and
/// <see cref="MateriaGrade"/>.
/// </summary>
public IReadOnlyList<MateriaEntry> MateriaEntries
{
get
{
if (ItemUtil.IsEventItem(this.BaseItemId) || this.IsMateriaUsedForDate)
return [];
var result = new List<MateriaEntry>();
for (byte i = 0; i < this.InternalItem.GetMateriaCount(); i++)
{
var entry = new MateriaEntry(this.InternalItem.GetMateriaId(i), this.InternalItem.GetMateriaGrade(i));
if (entry.IsValid())
result.Add(entry);
}
return result;
}
}
/// <summary>
/// Gets the address of native inventory item in the game.<br />
/// Can be 0 if this instance of <see cref="GameInventoryItem"/> does not point to a valid set of container type and slot.<br />

View file

@ -0,0 +1,42 @@
using Dalamud.Data;
using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace Dalamud.Game.Inventory.Records;
/// <summary>
/// A record to hold easy information about a given piece of Materia.
/// </summary>
public record MateriaEntry
{
/// <summary>
/// Initializes a new instance of the <see cref="MateriaEntry"/> class.
/// </summary>
/// <param name="typeId">The ID of the materia.</param>
/// <param name="gradeValue">The grade of the materia.</param>
public MateriaEntry(ushort typeId, byte gradeValue)
{
this.Type = LuminaUtils.CreateRef<Materia>(typeId);
this.Grade = LuminaUtils.CreateRef<MateriaGrade>(gradeValue);
}
/// <summary>
/// Gets the Lumina row for this Materia.
/// </summary>
public RowRef<Materia> Type { get; }
/// <summary>
/// Gets the Lumina row for this Materia's grade.
/// </summary>
public RowRef<MateriaGrade> Grade { get; }
/// <summary>
/// Checks if this MateriaEntry is valid.
/// </summary>
/// <returns>True if valid, false otherwise.</returns>
internal bool IsValid()
{
return this.Type.IsValid && this.Grade.IsValid;
}
}

View file

@ -0,0 +1,96 @@
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
namespace Dalamud.Game.NativeWrapper;
/// <summary>
/// A readonly wrapper for AgentInterface.
/// </summary>
/// <param name="address">The address to the AgentInterface.</param>
[StructLayout(LayoutKind.Explicit, Size = 0x08)]
public readonly unsafe struct AgentInterfacePtr(nint address) : IEquatable<AgentInterfacePtr>
{
/// <summary>
/// The address to the AgentInterface.
/// </summary>
[FieldOffset(0x00)]
public readonly nint Address = address;
/// <summary>
/// Gets a value indicating whether the underlying pointer is a nullptr.
/// </summary>
public readonly bool IsNull => this.Address == 0;
/// <summary>
/// Gets a value indicating whether the agents addon is visible.
/// </summary>
public readonly AtkUnitBasePtr Addon
{
get
{
if (this.IsNull)
return 0;
var raptureAtkUnitManager = RaptureAtkUnitManager.Instance();
if (raptureAtkUnitManager == null)
return 0;
return (nint)raptureAtkUnitManager->GetAddonById(this.AddonId);
}
}
/// <summary>
/// Gets a value indicating whether the agent is active.
/// </summary>
public readonly ushort AddonId => (ushort)(this.IsNull ? 0 : this.Struct->GetAddonId());
/// <summary>
/// Gets a value indicating whether the agent is active.
/// </summary>
public readonly bool IsAgentActive => !this.IsNull && this.Struct->IsAgentActive();
/// <summary>
/// Gets a value indicating whether the agents addon is ready.
/// </summary>
public readonly bool IsAddonReady => !this.IsNull && this.Struct->IsAddonReady();
/// <summary>
/// Gets a value indicating whether the agents addon is visible.
/// </summary>
public readonly bool IsAddonShown => !this.IsNull && this.Struct->IsAddonShown();
/// <summary>
/// Gets the AgentInterface*.
/// </summary>
/// <remarks> Internal use only. </remarks>
internal readonly AgentInterface* Struct => (AgentInterface*)this.Address;
public static implicit operator nint(AgentInterfacePtr wrapper) => wrapper.Address;
public static implicit operator AgentInterfacePtr(nint address) => new(address);
public static implicit operator AgentInterfacePtr(void* ptr) => new((nint)ptr);
public static bool operator ==(AgentInterfacePtr left, AgentInterfacePtr right) => left.Address == right.Address;
public static bool operator !=(AgentInterfacePtr left, AgentInterfacePtr right) => left.Address != right.Address;
/// <summary>
/// Focuses the AtkUnitBase.
/// </summary>
/// <returns> <c>true</c> when the addon was focused, <c>false</c> otherwise. </returns>
public readonly bool FocusAddon() => this.IsNull && this.Struct->FocusAddon();
/// <summary>Determines whether the specified AgentInterfacePtr is equal to the current AgentInterfacePtr.</summary>
/// <param name="other">The AgentInterfacePtr to compare with the current AgentInterfacePtr.</param>
/// <returns><c>true</c> if the specified AgentInterfacePtr is equal to the current AgentInterfacePtr; otherwise, <c>false</c>.</returns>
public readonly bool Equals(AgentInterfacePtr other) => this.Address == other.Address;
/// <inheritdoc cref="object.Equals(object?)"/>
public override readonly bool Equals(object obj) => obj is AgentInterfacePtr wrapper && this.Equals(wrapper);
/// <inheritdoc cref="object.GetHashCode()"/>
public override readonly int GetHashCode() => ((nuint)this.Address).GetHashCode();
}

View file

@ -0,0 +1,171 @@
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Component.GUI;
using FFXIVClientStructs.Interop;
namespace Dalamud.Game.NativeWrapper;
/// <summary>
/// A readonly wrapper for AtkUnitBase.
/// </summary>
/// <param name="address">The address to the AtkUnitBase.</param>
[StructLayout(LayoutKind.Explicit, Size = 0x08)]
public readonly unsafe struct AtkUnitBasePtr(nint address) : IEquatable<AtkUnitBasePtr>
{
/// <summary>
/// The address to the AtkUnitBase.
/// </summary>
[FieldOffset(0x00)]
public readonly nint Address = address;
/// <summary>
/// Gets a value indicating whether the underlying pointer is a nullptr.
/// </summary>
public readonly bool IsNull => this.Address == 0;
/// <summary>
/// Gets a value indicating whether the OnSetup function has been called.
/// </summary>
public readonly bool IsReady => !this.IsNull && this.Struct->IsReady;
/// <summary>
/// Gets a value indicating whether the AtkUnitBase is visible.
/// </summary>
public readonly bool IsVisible => !this.IsNull && this.Struct->IsVisible;
/// <summary>
/// Gets the name.
/// </summary>
public readonly string Name => this.IsNull ? string.Empty : this.Struct->NameString;
/// <summary>
/// Gets the id.
/// </summary>
public readonly ushort Id => this.IsNull ? (ushort)0 : this.Struct->Id;
/// <summary>
/// Gets the parent id.
/// </summary>
public readonly ushort ParentId => this.IsNull ? (ushort)0 : this.Struct->ParentId;
/// <summary>
/// Gets the host id.
/// </summary>
public readonly ushort HostId => this.IsNull ? (ushort)0 : this.Struct->HostId;
/// <summary>
/// Gets the scale.
/// </summary>
public readonly float Scale => this.IsNull ? 0f : this.Struct->Scale;
/// <summary>
/// Gets the x-position.
/// </summary>
public readonly short X => this.IsNull ? (short)0 : this.Struct->X;
/// <summary>
/// Gets the y-position.
/// </summary>
public readonly short Y => this.IsNull ? (short)0 : this.Struct->Y;
/// <summary>
/// Gets the width.
/// </summary>
public readonly float Width => this.IsNull ? 0f : this.Struct->GetScaledWidth(false);
/// <summary>
/// Gets the height.
/// </summary>
public readonly float Height => this.IsNull ? 0f : this.Struct->GetScaledHeight(false);
/// <summary>
/// Gets the scaled width.
/// </summary>
public readonly float ScaledWidth => this.IsNull ? 0f : this.Struct->GetScaledWidth(true);
/// <summary>
/// Gets the scaled height.
/// </summary>
public readonly float ScaledHeight => this.IsNull ? 0f : this.Struct->GetScaledHeight(true);
/// <summary>
/// Gets the position.
/// </summary>
public readonly Vector2 Position => new(this.X, this.Y);
/// <summary>
/// Gets the size.
/// </summary>
public readonly Vector2 Size => new(this.Width, this.Height);
/// <summary>
/// Gets the scaled size.
/// </summary>
public readonly Vector2 ScaledSize => new(this.ScaledWidth, this.ScaledHeight);
/// <summary>
/// Gets the number of <see cref="AtkValue"/> entries.
/// </summary>
public readonly int AtkValuesCount => this.Struct->AtkValuesCount;
/// <summary>
/// Gets an enumerable collection of <see cref="AtkValuePtr"/> of the addons current AtkValues.
/// </summary>
/// <returns>
/// An <see cref="IEnumerable{T}"/> of <see cref="AtkValuePtr"/> corresponding to the addons AtkValues.
/// </returns>
public IEnumerable<AtkValuePtr> AtkValues
{
get
{
for (var i = 0; i < this.AtkValuesCount; i++)
{
AtkValuePtr ptr;
unsafe
{
ptr = new AtkValuePtr((nint)this.Struct->AtkValuesSpan.GetPointer(i));
}
yield return ptr;
}
}
}
/// <summary>
/// Gets the AtkUnitBase*.
/// </summary>
/// <remarks> Internal use only. </remarks>
internal readonly AtkUnitBase* Struct => (AtkUnitBase*)this.Address;
public static implicit operator nint(AtkUnitBasePtr wrapper) => wrapper.Address;
public static implicit operator AtkUnitBasePtr(nint address) => new(address);
public static implicit operator AtkUnitBasePtr(void* ptr) => new((nint)ptr);
public static bool operator ==(AtkUnitBasePtr left, AtkUnitBasePtr right) => left.Address == right.Address;
public static bool operator !=(AtkUnitBasePtr left, AtkUnitBasePtr right) => left.Address != right.Address;
/// <summary>
/// Focuses the AtkUnitBase.
/// </summary>
public readonly void Focus()
{
if (!this.IsNull)
this.Struct->Focus();
}
/// <summary>Determines whether the specified AtkUnitBasePtr is equal to the current AtkUnitBasePtr.</summary>
/// <param name="other">The AtkUnitBasePtr to compare with the current AtkUnitBasePtr.</param>
/// <returns><c>true</c> if the specified AtkUnitBasePtr is equal to the current AtkUnitBasePtr; otherwise, <c>false</c>.</returns>
public readonly bool Equals(AtkUnitBasePtr other) => this.Address == other.Address;
/// <inheritdoc cref="object.Equals(object?)"/>
public override readonly bool Equals(object obj) => obj is AtkUnitBasePtr wrapper && this.Equals(wrapper);
/// <inheritdoc cref="object.GetHashCode()"/>
public override readonly int GetHashCode() => ((nuint)this.Address).GetHashCode();
}

View file

@ -0,0 +1,113 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.NativeWrapper;
/// <summary>
/// A readonly wrapper for AtkValue.
/// </summary>
/// <param name="address">The address to the AtkValue.</param>
[StructLayout(LayoutKind.Explicit, Size = 0x08)]
public readonly unsafe struct AtkValuePtr(nint address) : IEquatable<AtkValuePtr>
{
/// <summary>
/// The address to the AtkValue.
/// </summary>
[FieldOffset(0x00)]
public readonly nint Address = address;
/// <summary>
/// Gets a value indicating whether the underlying pointer is a nullptr.
/// </summary>
public readonly bool IsNull => this.Address == 0;
/// <summary>
/// Gets the value type.
/// </summary>
public readonly AtkValueType ValueType => (AtkValueType)this.Struct->Type;
/// <summary>
/// Gets the AtkValue*.
/// </summary>
/// <remarks> Internal use only. </remarks>
internal readonly AtkValue* Struct => (AtkValue*)this.Address;
public static implicit operator nint(AtkValuePtr wrapper) => wrapper.Address;
public static implicit operator AtkValuePtr(nint address) => new(address);
public static implicit operator AtkValuePtr(void* ptr) => new((nint)ptr);
public static bool operator ==(AtkValuePtr left, AtkValuePtr right) => left.Address == right.Address;
public static bool operator !=(AtkValuePtr left, AtkValuePtr right) => left.Address != right.Address;
/// <summary>
/// Gets the value of the underlying <see cref="AtkValue"/> as a boxed object, based on its <see cref="AtkValueType"/>.
/// </summary>
/// <returns>
/// The boxed value represented by this <see cref="AtkValuePtr"/>, or <c>null</c> if the value is null or undefined.
/// </returns>
/// <exception cref="NotImplementedException">
/// Thrown if the value type is not currently handled by this implementation.
/// </exception>
public unsafe object? GetValue()
{
if (this.Struct == null)
return null;
return this.ValueType switch
{
AtkValueType.Undefined or AtkValueType.Null => null,
AtkValueType.Bool => this.Struct->Bool,
AtkValueType.Int => this.Struct->Int,
AtkValueType.Int64 => this.Struct->Int64,
AtkValueType.UInt => this.Struct->UInt,
AtkValueType.UInt64 => this.Struct->UInt64,
AtkValueType.Float => this.Struct->Float,
AtkValueType.String or AtkValueType.String8 or AtkValueType.ManagedString => this.Struct->String.HasValue ? this.Struct->String.AsReadOnlySeString() : default,
AtkValueType.WideString => this.Struct->WideString == null ? string.Empty : new string(this.Struct->WideString),
AtkValueType.Pointer => (nint)this.Struct->Pointer,
_ => throw new NotImplementedException($"AtkValueType {this.ValueType} is currently not supported"),
};
}
/// <summary>
/// Attempts to retrieve the value as a strongly-typed object if the underlying type matches.
/// </summary>
/// <typeparam name="T">The expected value type to extract.</typeparam>
/// <param name="result">
/// When this method returns <c>true</c>, contains the extracted value of type <typeparamref name="T"/>.
/// Otherwise, contains the default value of type <typeparamref name="T"/>.
/// </param>
/// <returns>
/// <c>true</c> if the value was successfully extracted and matched <typeparamref name="T"/>; otherwise, <c>false</c>.
/// </returns>
public unsafe bool TryGet<T>([NotNullWhen(true)] out T? result) where T : struct
{
object? value = this.GetValue();
if (value is T typed)
{
result = typed;
return true;
}
result = default;
return false;
}
/// <summary>Determines whether the specified AtkValuePtr is equal to the current AtkValuePtr.</summary>
/// <param name="other">The AtkValuePtr to compare with the current AtkValuePtr.</param>
/// <returns><c>true</c> if the specified AtkValuePtr is equal to the current AtkValuePtr; otherwise, <c>false</c>.</returns>
public readonly bool Equals(AtkValuePtr other) => this.Address == other.Address || this.Struct->EqualTo(other.Struct);
/// <inheritdoc cref="object.Equals(object?)"/>
public override readonly bool Equals(object obj) => obj is AtkValuePtr wrapper && this.Equals(wrapper);
/// <inheritdoc cref="object.GetHashCode()"/>
public override readonly int GetHashCode() => ((nuint)this.Address).GetHashCode();
}

View file

@ -0,0 +1,87 @@
namespace Dalamud.Game.NativeWrapper;
/// <summary>
/// Represents the data type of the AtkValue.
/// </summary>
public enum AtkValueType
{
/// <summary>
/// The value is undefined or invalid.
/// </summary>
Undefined = 0,
/// <summary>
/// The value is null.
/// </summary>
Null = 0x1,
/// <summary>
/// The value is a boolean.
/// </summary>
Bool = 0x2,
/// <summary>
/// The value is a 32-bit signed integer.
/// </summary>
Int = 0x3,
/// <summary>
/// The value is a 64-bit signed integer.
/// </summary>
Int64 = 0x4,
/// <summary>
/// The value is a 32-bit unsigned integer.
/// </summary>
UInt = 0x5,
/// <summary>
/// The value is a 64-bit unsigned integer.
/// </summary>
UInt64 = 0x6,
/// <summary>
/// The value is a 32-bit floating-point number.
/// </summary>
Float = 0x7,
/// <summary>
/// The value points to a null-terminated 8-bit character string (ASCII or UTF-8).
/// </summary>
String = 0x8,
/// <summary>
/// The value points to a null-terminated 16-bit character string (UTF-16 / wide string).
/// </summary>
WideString = 0x9,
/// <summary>
/// The value points to a constant null-terminated 8-bit character string (const char*).
/// </summary>
String8 = 0xA,
/// <summary>
/// The value is a vector.
/// </summary>
Vector = 0xB,
/// <summary>
/// The value is a pointer.
/// </summary>
Pointer = 0xC,
/// <summary>
/// The value is pointing to an array of AtkValue entries.
/// </summary>
AtkValues = 0xD,
/// <summary>
/// The value is a managed string. See <see cref="String"/>.
/// </summary>
ManagedString = 0x28,
/// <summary>
/// The value is a managed vector. See <see cref="Vector"/>.
/// </summary>
ManagedVector = 0x2B,
}

View file

@ -0,0 +1,51 @@
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.UI;
namespace Dalamud.Game.NativeWrapper;
/// <summary>
/// A readonly wrapper for UIModule.
/// </summary>
/// <param name="address">The address to the UIModule.</param>
[StructLayout(LayoutKind.Explicit, Size = 0x08)]
public readonly unsafe struct UIModulePtr(nint address) : IEquatable<UIModulePtr>
{
/// <summary>
/// The address to the UIModule.
/// </summary>
[FieldOffset(0x00)]
public readonly nint Address = address;
/// <summary>
/// Gets a value indicating whether the underlying pointer is a nullptr.
/// </summary>
public readonly bool IsNull => this.Address == 0;
/// <summary>
/// Gets the UIModule*.
/// </summary>
/// <remarks> Internal use only. </remarks>
internal readonly UIModule* Struct => (UIModule*)this.Address;
public static implicit operator nint(UIModulePtr wrapper) => wrapper.Address;
public static implicit operator UIModulePtr(nint address) => new(address);
public static implicit operator UIModulePtr(void* ptr) => new((nint)ptr);
public static bool operator ==(UIModulePtr left, UIModulePtr right) => left.Address == right.Address;
public static bool operator !=(UIModulePtr left, UIModulePtr right) => left.Address != right.Address;
/// <summary>Determines whether the specified UIModulePtr is equal to the current UIModulePtr.</summary>
/// <param name="other">The UIModulePtr to compare with the current UIModulePtr.</param>
/// <returns><c>true</c> if the specified UIModulePtr is equal to the current UIModulePtr; otherwise, <c>false</c>.</returns>
public readonly bool Equals(UIModulePtr other) => this.Address == other.Address;
/// <inheritdoc cref="object.Equals(object?)"/>
public override readonly bool Equals(object obj) => obj is UIModulePtr wrapper && this.Equals(wrapper);
/// <inheritdoc cref="object.GetHashCode()"/>
public override readonly int GetHashCode() => ((nuint)this.Address).GetHashCode();
}

View file

@ -17,7 +17,7 @@ namespace Dalamud.Game.Network;
/// This class handles interacting with game network events.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class GameNetwork : IInternalDisposableService, IGameNetwork
internal sealed unsafe class GameNetwork : IInternalDisposableService
{
private readonly GameNetworkAddressResolver address;
private readonly Hook<PacketDispatcher.Delegates.OnReceivePacket> processZonePacketDownHook;
@ -51,11 +51,23 @@ internal sealed unsafe class GameNetwork : IInternalDisposableService, IGameNetw
this.processZonePacketUpHook.Enable();
}
/// <summary>
/// The delegate type of a network message event.
/// </summary>
/// <param name="dataPtr">The pointer to the raw data.</param>
/// <param name="opCode">The operation ID code.</param>
/// <param name="sourceActorId">The source actor ID.</param>
/// <param name="targetActorId">The taret actor ID.</param>
/// <param name="direction">The direction of the packed.</param>
public delegate void OnNetworkMessageDelegate(nint dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate byte ProcessZonePacketUpDelegate(IntPtr a1, IntPtr dataPtr, IntPtr a3, byte a4);
/// <inheritdoc/>
public event IGameNetwork.OnNetworkMessageDelegate? NetworkMessage;
/// <summary>
/// Event that is called when a network message is sent/received.
/// </summary>
public event OnNetworkMessageDelegate? NetworkMessage;
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
@ -136,39 +148,3 @@ internal sealed unsafe class GameNetwork : IInternalDisposableService, IGameNetw
return this.processZonePacketUpHook.Original(a1, dataPtr, a3, a4);
}
}
/// <summary>
/// Plugin-scoped version of a AddonLifecycle service.
/// </summary>
[PluginInterface]
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia<IGameNetwork>]
#pragma warning restore SA1015
internal class GameNetworkPluginScoped : IInternalDisposableService, IGameNetwork
{
[ServiceManager.ServiceDependency]
private readonly GameNetwork gameNetworkService = Service<GameNetwork>.Get();
/// <summary>
/// Initializes a new instance of the <see cref="GameNetworkPluginScoped"/> class.
/// </summary>
internal GameNetworkPluginScoped()
{
this.gameNetworkService.NetworkMessage += this.NetworkMessageForward;
}
/// <inheritdoc/>
public event IGameNetwork.OnNetworkMessageDelegate? NetworkMessage;
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.gameNetworkService.NetworkMessage -= this.NetworkMessageForward;
this.NetworkMessage = null;
}
private void NetworkMessageForward(nint dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction)
=> this.NetworkMessage?.Invoke(dataPtr, opCode, sourceActorId, targetActorId, direction);
}

View file

@ -275,7 +275,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
if (uploaderId == 0)
{
var playerState = PlayerState.Instance();
if (playerState->IsLoaded == 1)
if (playerState->IsLoaded)
{
uploaderId = playerState->ContentId;
}

View file

@ -16,6 +16,6 @@ internal class NetworkHandlersAddressResolver : BaseAddressResolver
{
this.CustomTalkEventResponsePacketHandler =
scanner.ScanText(
"48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 49 8B D9 41 0F B6 F8 0F B7 F2 8B E9 E8 ?? ?? ?? ?? 48 8B C8 44 0F B6 CF 0F B6 44 24 ?? 44 0F B7 C6 88 44 24 ?? 8B D5 48 89 5C 24"); // unnamed in CS
"48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 49 8B D9 41 0F B6 F8 0F B7 F2 8B E9 E8 ?? ?? ?? ?? 44 0F B6 54 24 ?? 44 0F B6 CF 44 88 54 24 ?? 44 0F B7 C6 8B D5"); // unnamed in CS
}
}

View file

@ -55,4 +55,37 @@ internal sealed class WinSockHandlers : IInternalDisposableService
return socket;
}
/// <summary>
/// Native ws2_32 functions.
/// </summary>
private static class NativeFunctions
{
/// <summary>
/// See https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-setsockopt.
/// The setsockopt function sets a socket option.
/// </summary>
/// <param name="socket">
/// A descriptor that identifies a socket.
/// </param>
/// <param name="level">
/// The level at which the option is defined (for example, SOL_SOCKET).
/// </param>
/// <param name="optName">
/// The socket option for which the value is to be set (for example, SO_BROADCAST). The optname parameter must be a
/// socket option defined within the specified level, or behavior is undefined.
/// </param>
/// <param name="optVal">
/// A pointer to the buffer in which the value for the requested option is specified.
/// </param>
/// <param name="optLen">
/// The size, in bytes, of the buffer pointed to by the optval parameter.
/// </param>
/// <returns>
/// If no error occurs, setsockopt returns zero. Otherwise, a value of SOCKET_ERROR is returned, and a specific error
/// code can be retrieved by calling WSAGetLastError.
/// </returns>
[DllImport("ws2_32.dll", CallingConvention = CallingConvention.Winapi, EntryPoint = "setsockopt")]
public static extern int SetSockOpt(IntPtr socket, SocketOptionLevel level, SocketOptionName optName, ref IntPtr optVal, int optLen);
}
}

View file

@ -21,6 +21,8 @@ namespace Dalamud.Game;
/// </summary>
public class SigScanner : IDisposable, ISigScanner
{
private static byte[]? fileBytes;
private readonly FileInfo? cacheFile;
private nint moduleCopyPtr;
@ -51,12 +53,16 @@ public class SigScanner : IDisposable, ISigScanner
this.Is32BitProcess = !Environment.Is64BitProcess;
this.IsCopy = doCopy;
if (this.IsCopy)
{
this.SetupCopiedSegments();
fileBytes ??= File.ReadAllBytes(module.FileName);
}
// Limit the search space to .text section.
this.SetupSearchSpace(module);
if (this.IsCopy)
this.SetupCopiedSegments();
Log.Verbose($"Module base: 0x{this.TextSectionBase.ToInt64():X}");
Log.Verbose($"Module size: 0x{this.TextSectionSize:X}");
@ -494,6 +500,18 @@ public class SigScanner : IDisposable, ISigScanner
case 0x747865742E: // .text
this.TextSectionOffset = Marshal.ReadInt32(sectionCursor, 12);
this.TextSectionSize = Marshal.ReadInt32(sectionCursor, 8);
if (this.IsCopy)
{
var pointerToRawData = Marshal.ReadInt32(sectionCursor, 20);
Marshal.Copy(
fileBytes.AsSpan(pointerToRawData, this.TextSectionSize).ToArray(),
0,
this.moduleCopyPtr + (nint)this.TextSectionOffset,
this.TextSectionSize);
}
break;
case 0x617461642E: // .data
this.DataSectionOffset = Marshal.ReadInt32(sectionCursor, 12);

View file

@ -3,7 +3,6 @@ using Dalamud.Utility;
using Lumina.Extensions;
using ItemKind = Dalamud.Game.Text.SeStringHandling.Payloads.ItemPayload.ItemKind;
using LSheets = Lumina.Excel.Sheets;
namespace Dalamud.Game.Text.Evaluator.Internal;
@ -222,8 +221,8 @@ internal class SheetRedirectResolver : IServiceType
sheetName = nameof(LSheets.AkatsukiNoteString);
colIndex = 0;
if (this.dataManager.Excel.GetSubrowSheet<LSheets.AkatsukiNote>().TryGetRow(rowId, out var row))
rowId = (uint)row[0].Unknown2;
if (this.dataManager.Excel.GetSubrowSheet<LSheets.AkatsukiNote>().TryGetSubrow(rowId, 0, out var row))
rowId = row.ListName.RowId;
break;
}
}

View file

@ -40,8 +40,6 @@ using StatusSheet = Lumina.Excel.Sheets.Status;
namespace Dalamud.Game.Text.Evaluator;
#pragma warning disable SeStringEvaluator
/// <summary>
/// Evaluator for SeStrings.
/// </summary>
@ -317,6 +315,9 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
case MacroCode.Sheet:
return this.TryResolveSheet(in context, payload);
case MacroCode.SheetSub:
return this.TryResolveSheetSub(in context, payload);
case MacroCode.String:
return this.TryResolveString(in context, payload);
@ -759,6 +760,65 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
return true;
}
private bool TryResolveSheetSub(in SeStringContext context, in ReadOnlySePayloadSpan payload)
{
var enu = payload.GetEnumerator();
if (!enu.MoveNext() || !enu.Current.TryGetString(out var eSheetNameStr))
return false;
if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var eRowIdValue))
return false;
if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var eSubrowIdValue))
return false;
if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var eColIndexValue))
return false;
var secondaryRowId = this.GetSubrowSheetIntValue(context.Language, eSheetNameStr.ExtractText(), eRowIdValue, (ushort)eSubrowIdValue, eColIndexValue);
if (secondaryRowId == -1)
return false;
if (!enu.MoveNext() || !enu.Current.TryGetString(out var eSecondarySheetNameStr))
return false;
if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var secondaryColIndex))
return false;
var text = this.FormatSheetValue(context.Language, eSecondarySheetNameStr.ExtractText(), (uint)secondaryRowId, secondaryColIndex, 0);
if (text.IsEmpty)
return false;
this.CreateSheetLink(context, eSecondarySheetNameStr.ExtractText(), text, eRowIdValue, eSubrowIdValue);
return true;
}
private int GetSubrowSheetIntValue(ClientLanguage language, string sheetName, uint rowId, ushort subrowId, uint colIndex)
{
if (!this.dataManager.Excel.SheetNames.Contains(sheetName))
return -1;
if (!this.dataManager.GetSubrowExcelSheet<RawSubrow>(language, sheetName)
.TryGetSubrow(rowId, subrowId, out var row))
return -1;
if (colIndex >= row.Columns.Count)
return -1;
var column = row.Columns[(int)colIndex];
return column.Type switch
{
ExcelColumnDataType.Int8 => row.ReadInt8(column.Offset),
ExcelColumnDataType.UInt8 => row.ReadUInt8(column.Offset),
ExcelColumnDataType.Int16 => row.ReadInt16(column.Offset),
ExcelColumnDataType.UInt16 => row.ReadUInt16(column.Offset),
ExcelColumnDataType.Int32 => row.ReadInt32(column.Offset),
_ => -1,
};
}
private ReadOnlySeString FormatSheetValue(ClientLanguage language, string sheetName, uint rowId, uint colIndex, uint colParam)
{
if (!this.dataManager.Excel.SheetNames.Contains(sheetName))

View file

@ -71,6 +71,7 @@ public readonly struct SeStringParameter
public static implicit operator SeStringParameter(ReadOnlySeStringSpan value) => new(new ReadOnlySeString(value));
[Obsolete("Switch to using ReadOnlySeString instead of Lumina's SeString.", true)]
public static implicit operator SeStringParameter(LSeString value) => new(new ReadOnlySeString(value.RawData));
public static implicit operator SeStringParameter(DSeString value) => new(new ReadOnlySeString(value.Encode()));

View file

@ -72,33 +72,6 @@ public class ItemPayload : Payload
{
}
/// <summary>
/// Kinds of items that can be fetched from this payload.
/// </summary>
[Api13ToDo("Move this out of ItemPayload. It's used in other classes too.")]
public enum ItemKind : uint
{
/// <summary>
/// Normal items.
/// </summary>
Normal,
/// <summary>
/// Collectible Items.
/// </summary>
Collectible = 500_000,
/// <summary>
/// High-Quality items.
/// </summary>
Hq = 1_000_000,
/// <summary>
/// Event/Key items.
/// </summary>
EventItem = 2_000_000,
}
/// <inheritdoc/>
public override PayloadType Type => PayloadType.Item;

View file

@ -118,6 +118,7 @@ public class SeString
/// </summary>
/// <param name="str">string to convert.</param>
/// <returns>Equivalent SeString.</returns>
[Obsolete("Switch to using ReadOnlySeString instead of Lumina's SeString.", true)]
public static explicit operator SeString(Lumina.Text.SeString str) => str.ToDalamudString();
/// <summary>
@ -181,7 +182,7 @@ public class SeString
/// <param name="displayNameOverride">An optional name override to display, instead of the actual item name.</param>
/// <returns>An SeString containing all the payloads necessary to display an item link in the chat log.</returns>
public static SeString CreateItemLink(uint itemId, bool isHq, string? displayNameOverride = null) =>
CreateItemLink(itemId, isHq ? ItemPayload.ItemKind.Hq : ItemPayload.ItemKind.Normal, displayNameOverride);
CreateItemLink(itemId, isHq ? ItemKind.Hq : ItemKind.Normal, displayNameOverride);
/// <summary>
/// Creates an SeString representing an entire Payload chain that can be used to link an item in the chat log.
@ -190,7 +191,7 @@ public class SeString
/// <param name="kind">The kind of item to link.</param>
/// <param name="displayNameOverride">An optional name override to display, instead of the actual item name.</param>
/// <returns>An SeString containing all the payloads necessary to display an item link in the chat log.</returns>
public static SeString CreateItemLink(uint itemId, ItemPayload.ItemKind kind = ItemPayload.ItemKind.Normal, string? displayNameOverride = null)
public static SeString CreateItemLink(uint itemId, ItemKind kind = ItemKind.Normal, string? displayNameOverride = null)
{
var clientState = Service<ClientState.ClientState>.Get();
var seStringEvaluator = Service<SeStringEvaluator>.Get();

View file

@ -1,6 +1,7 @@
using System.Collections.Generic;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Utility;
namespace Dalamud.Game.Text.SeStringHandling;
@ -126,7 +127,7 @@ public class SeStringBuilder
/// <param name="kind">Kind of item to encode.</param>
/// <param name="itemNameOverride">Override for the item's name.</param>
/// <returns>The current builder.</returns>
public SeStringBuilder AddItemLink(uint itemId, ItemPayload.ItemKind kind = ItemPayload.ItemKind.Normal, string? itemNameOverride = null) =>
public SeStringBuilder AddItemLink(uint itemId, ItemKind kind = ItemKind.Normal, string? itemNameOverride = null) =>
this.Append(SeString.CreateItemLink(itemId, kind, itemNameOverride));
/// <summary>

View file

@ -201,11 +201,11 @@ public abstract class Hook<T> : IDalamudHook where T : Delegate
if (EnvironmentConfiguration.DalamudForceMinHook)
useMinHook = true;
var moduleHandle = NativeFunctions.GetModuleHandleW(moduleName);
if (moduleHandle == IntPtr.Zero)
using var moduleHandle = Windows.Win32.PInvoke.GetModuleHandle(moduleName);
if (moduleHandle.IsInvalid)
throw new Exception($"Could not get a handle to module {moduleName}");
var procAddress = NativeFunctions.GetProcAddress(moduleHandle, exportName);
var procAddress = (nint)Windows.Win32.PInvoke.GetProcAddress(moduleHandle, exportName);
if (procAddress == IntPtr.Zero)
throw new Exception($"Could not get the address of {moduleName}::{exportName}");

View file

@ -1,10 +1,11 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using System.Runtime.InteropServices;
using Dalamud.Memory;
using JetBrains.Annotations;
using Windows.Win32.System.Memory;
using Win32Exception = System.ComponentModel.Win32Exception;
namespace Dalamud.Hooking.Internal;
@ -12,7 +13,7 @@ namespace Dalamud.Hooking.Internal;
/// Manages a hook with MinHook.
/// </summary>
/// <typeparam name="T">Delegate type to represents a function prototype. This must be the same prototype as original function do.</typeparam>
internal class FunctionPointerVariableHook<T> : Hook<T>
internal unsafe class FunctionPointerVariableHook<T> : Hook<T>
where T : Delegate
{
private readonly nint pfnDetour;
@ -55,11 +56,11 @@ internal class FunctionPointerVariableHook<T> : Hook<T>
// Note: WINE seemingly tries to clean up all heap allocations on process exit.
// We want our allocation to be kept there forever, until no running thread remains.
// Therefore we're using VirtualAlloc instead of HeapCreate/HeapAlloc.
var pfnThunkBytes = (byte*)NativeFunctions.VirtualAlloc(
0,
var pfnThunkBytes = (byte*)Windows.Win32.PInvoke.VirtualAlloc(
null,
12,
NativeFunctions.AllocationType.Reserve | NativeFunctions.AllocationType.Commit,
MemoryProtection.ExecuteReadWrite);
VIRTUAL_ALLOCATION_TYPE.MEM_RESERVE | VIRTUAL_ALLOCATION_TYPE.MEM_COMMIT,
PAGE_PROTECTION_FLAGS.PAGE_EXECUTE_READWRITE);
if (pfnThunkBytes == null)
{
throw new OutOfMemoryException("Failed to allocate memory for import hooks.");
@ -78,10 +79,10 @@ internal class FunctionPointerVariableHook<T> : Hook<T>
this.ppfnThunkJumpTarget = this.pfnThunk + 2;
if (!NativeFunctions.VirtualProtect(
this.Address,
if (!Windows.Win32.PInvoke.VirtualProtect(
this.Address.ToPointer(),
(UIntPtr)Marshal.SizeOf<IntPtr>(),
MemoryProtection.ExecuteReadWrite,
PAGE_PROTECTION_FLAGS.PAGE_EXECUTE_READWRITE,
out var oldProtect))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
@ -93,7 +94,7 @@ internal class FunctionPointerVariableHook<T> : Hook<T>
Marshal.WriteIntPtr(this.Address, this.pfnThunk);
// This really should not fail, but then even if it does, whatever.
NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf<IntPtr>(), oldProtect, out _);
Windows.Win32.PInvoke.VirtualProtect(this.Address.ToPointer(), (UIntPtr)Marshal.SizeOf<IntPtr>(), oldProtect, out _);
// Add afterwards, so the hookIdent starts at 0.
indexList.Add(this);

View file

@ -1,10 +1,9 @@
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
namespace Dalamud.Interface.Components;
/// <summary>

View file

@ -1,9 +1,8 @@
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
namespace Dalamud.Interface.Components;
/// <summary>

View file

@ -1,9 +1,7 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility.Raii;
using FFXIVClientStructs.FFXIV.Common.Math;
using ImGuiNET;
namespace Dalamud.Interface.Components;
/// <summary>
@ -45,7 +43,7 @@ public static partial class ImGuiComponents
{
using (ImRaii.TextWrapPos(ImGui.GetFontSize() * 35.0f))
{
ImGui.TextUnformatted(helpText);
ImGui.Text(helpText);
}
}
}

View file

@ -1,10 +1,9 @@
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
namespace Dalamud.Interface.Components;
/// <summary>

View file

@ -2,10 +2,9 @@ using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using ImGuiNET;
namespace Dalamud.Interface.Components;
/// <summary>

View file

@ -1,4 +1,4 @@
using ImGuiNET;
using Dalamud.Bindings.ImGui;
namespace Dalamud.Interface.Components;
@ -12,6 +12,6 @@ public static partial class ImGuiComponents
/// </summary>
public static void Test()
{
ImGui.Text("You are viewing the test component. The test was a success.");
ImGui.Text("You are viewing the test component. The test was a success."u8);
}
}

View file

@ -1,7 +1,6 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
namespace Dalamud.Interface.Components;
/// <summary>
@ -30,7 +29,7 @@ public static partial class ImGuiComponents
{
using (ImRaii.Tooltip())
{
ImGui.TextUnformatted(hint);
ImGui.Text(hint);
}
}
}

View file

@ -1,6 +1,6 @@
using System.Numerics;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
namespace Dalamud.Interface.Components;

View file

@ -1,11 +1,10 @@
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Internal;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using ImGuiNET;
using Serilog;
namespace Dalamud.Interface.DragDrop;
@ -32,7 +31,7 @@ internal partial class DragDropManager : IInternalDisposableService, IDragDropMa
Service<InterfaceManager.InterfaceManagerWithScene>.GetAsync()
.ContinueWith(t =>
{
this.windowHandlePtr = t.Result.Manager.WindowHandlePtr;
this.windowHandlePtr = t.Result.Manager.GameWindowHandle;
this.Enable();
});
}
@ -103,7 +102,7 @@ internal partial class DragDropManager : IInternalDisposableService, IDragDropMa
}
/// <inheritdoc cref="IDragDropManager.CreateImGuiSource(string, Func{IDragDropManager, bool}, Func{IDragDropManager, bool})"/>
public void CreateImGuiSource(string label, Func<IDragDropManager, bool> validityCheck, Func<IDragDropManager, bool> tooltipBuilder)
public unsafe void CreateImGuiSource(string label, Func<IDragDropManager, bool> validityCheck, Func<IDragDropManager, bool> tooltipBuilder)
{
if (!this.IsDragging && !this.IsDropping())
{
@ -115,7 +114,7 @@ internal partial class DragDropManager : IInternalDisposableService, IDragDropMa
return;
}
ImGui.SetDragDropPayload(label, nint.Zero, 0);
ImGui.SetDragDropPayload(label, null, 0);
if (this.CheckTooltipFrame(out var frame) && tooltipBuilder(this))
{
this.lastTooltipFrame = frame;
@ -136,7 +135,7 @@ internal partial class DragDropManager : IInternalDisposableService, IDragDropMa
unsafe
{
if (ImGui.AcceptDragDropPayload(label, ImGuiDragDropFlags.AcceptBeforeDelivery).NativePtr != null && this.IsDropping())
if (ImGui.AcceptDragDropPayload(label, ImGuiDragDropFlags.AcceptBeforeDelivery).Handle != null && this.IsDropping())
{
this.lastDropFrame = -2;
files = this.Files;

View file

@ -4,8 +4,8 @@ using System.Linq;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
using Dalamud.Bindings.ImGui;
using Dalamud.Utility;
using ImGuiNET;
using Serilog;
namespace Dalamud.Interface.DragDrop;

View file

@ -1,12 +1,9 @@
using System.Collections.Generic;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Storage.Assets;
using ImGuiNET;
using Newtonsoft.Json;
using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.FontIdentifier;

View file

@ -1,11 +1,8 @@
using System.Collections.Generic;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.ManagedFontAtlas;
using ImGuiNET;
using Newtonsoft.Json;
using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.FontIdentifier;

View file

@ -1,12 +1,9 @@
using System.Collections.Generic;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ManagedFontAtlas;
using ImGuiNET;
using Newtonsoft.Json;
using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.FontIdentifier;

View file

@ -1,7 +1,6 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.ManagedFontAtlas;
using ImGuiNET;
namespace Dalamud.Interface.FontIdentifier;
/// <summary>

View file

@ -1,7 +1,6 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.ManagedFontAtlas;
using ImGuiNET;
namespace Dalamud.Interface.FontIdentifier;
/// <summary>

View file

@ -3,11 +3,9 @@ using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Text;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.Utility;
using ImGuiNET;
using Newtonsoft.Json;
namespace Dalamud.Interface.FontIdentifier;

View file

@ -2,13 +2,10 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Utility;
using ImGuiNET;
using Newtonsoft.Json;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
@ -38,7 +35,7 @@ public sealed class SystemFontId : IFontId
this.EnglishName = name;
else if (this.LocaleNames.TryGetValue("en", out name))
this.EnglishName = name;
else
else
this.EnglishName = this.LocaleNames.Values.First();
}

View file

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Numerics;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
namespace Dalamud.Interface.GameFonts;

View file

@ -0,0 +1,4 @@
namespace Dalamud.Interface.ImGuiBackend.Delegates;
/// <summary>Delegate to be called when ImGui should be used to layout now.</summary>
public delegate void ImGuiBuildUiDelegate();

View file

@ -0,0 +1,4 @@
namespace Dalamud.Interface.ImGuiBackend.Delegates;
/// <summary>Delegate to be called on new input frame.</summary>
public delegate void ImGuiNewInputFrameDelegate();

View file

@ -0,0 +1,4 @@
namespace Dalamud.Interface.ImGuiBackend.Delegates;
/// <summary>Delegate to be called on new render frame.</summary>
public delegate void ImGuiNewRenderFrameDelegate();

View file

@ -0,0 +1,253 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.Bindings.ImGui;
using Dalamud.Bindings.ImGuizmo;
using Dalamud.Bindings.ImPlot;
using Dalamud.Interface.ImGuiBackend.Delegates;
using Dalamud.Interface.ImGuiBackend.Helpers;
using Dalamud.Interface.ImGuiBackend.InputHandler;
using Dalamud.Interface.ImGuiBackend.Renderers;
using Dalamud.Utility;
using Serilog;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
namespace Dalamud.Interface.ImGuiBackend;
/// <summary>
/// Backend for ImGui, using <see cref="Dx11Renderer"/> and <see cref="Win32InputHandler"/>.
/// </summary>
[SuppressMessage(
"StyleCop.CSharp.LayoutRules",
"SA1519:Braces should not be omitted from multi-line child statement",
Justification = "Multiple fixed/using scopes")]
internal sealed unsafe class Dx11Win32Backend : IWin32Backend
{
private readonly Dx11Renderer imguiRenderer;
private readonly Win32InputHandler imguiInput;
private ComPtr<IDXGISwapChain> swapChainPossiblyWrapped;
private ComPtr<IDXGISwapChain> swapChain;
private ComPtr<ID3D11Device> device;
private ComPtr<ID3D11DeviceContext> deviceContext;
private int targetWidth;
private int targetHeight;
/// <summary>
/// Initializes a new instance of the <see cref="Dx11Win32Backend"/> class.
/// </summary>
/// <param name="swapChain">The pointer to an instance of <see cref="IDXGISwapChain"/>. The reference is copied.</param>
public Dx11Win32Backend(IDXGISwapChain* swapChain)
{
try
{
this.swapChainPossiblyWrapped = new(swapChain);
this.swapChain = new(swapChain);
fixed (ComPtr<IDXGISwapChain>* ppSwapChain = &this.swapChain)
ReShadePeeler.PeelSwapChain(ppSwapChain);
fixed (Guid* guid = &IID.IID_ID3D11Device)
fixed (ID3D11Device** pp = &this.device.GetPinnableReference())
this.swapChain.Get()->GetDevice(guid, (void**)pp).ThrowOnError();
fixed (ID3D11DeviceContext** pp = &this.deviceContext.GetPinnableReference())
this.device.Get()->GetImmediateContext(pp);
using var buffer = default(ComPtr<ID3D11Resource>);
fixed (Guid* guid = &IID.IID_ID3D11Resource)
this.swapChain.Get()->GetBuffer(0, guid, (void**)buffer.GetAddressOf()).ThrowOnError();
var desc = default(DXGI_SWAP_CHAIN_DESC);
this.swapChain.Get()->GetDesc(&desc).ThrowOnError();
this.targetWidth = (int)desc.BufferDesc.Width;
this.targetHeight = (int)desc.BufferDesc.Height;
this.WindowHandle = desc.OutputWindow;
var ctx = ImGui.CreateContext();
ImGuizmo.SetImGuiContext(ctx);
ImPlot.SetImGuiContext(ctx);
ImPlot.CreateContext();
ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.DockingEnable | ImGuiConfigFlags.ViewportsEnable;
this.imguiRenderer = new(this.SwapChain, this.Device, this.DeviceContext);
this.imguiInput = new(this.WindowHandle);
}
catch
{
this.Dispose();
throw;
}
}
/// <summary>
/// Finalizes an instance of the <see cref="Dx11Win32Backend"/> class.
/// </summary>
~Dx11Win32Backend() => this.ReleaseUnmanagedResources();
/// <inheritdoc/>
public event ImGuiBuildUiDelegate? BuildUi;
/// <inheritdoc/>
public event ImGuiNewInputFrameDelegate? NewInputFrame;
/// <inheritdoc/>
public event ImGuiNewRenderFrameDelegate? NewRenderFrame;
/// <inheritdoc/>
public bool UpdateCursor
{
get => this.imguiInput.UpdateCursor;
set => this.imguiInput.UpdateCursor = value;
}
/// <inheritdoc/>
public string? IniPath
{
get => this.imguiInput.IniPath;
set => this.imguiInput.IniPath = value;
}
/// <inheritdoc/>
public IImGuiInputHandler InputHandler => this.imguiInput;
/// <inheritdoc/>
public IImGuiRenderer Renderer => this.imguiRenderer;
/// <summary>
/// Gets the pointer to an instance of <see cref="IDXGISwapChain"/>.
/// </summary>
public IDXGISwapChain* SwapChain => this.swapChain;
/// <summary>
/// Gets the pointer to an instance of <see cref="ID3D11Device"/>.
/// </summary>
public ID3D11Device* Device => this.device;
/// <summary>
/// Gets the pointer to an instance of <see cref="ID3D11Device"/>, in <see cref="nint"/>.
/// </summary>
public nint DeviceHandle => (nint)this.device.Get();
/// <summary>
/// Gets the pointer to an instance of <see cref="ID3D11DeviceContext"/>.
/// </summary>
public ID3D11DeviceContext* DeviceContext => this.deviceContext;
/// <summary>
/// Gets the window handle.
/// </summary>
public HWND WindowHandle { get; }
/// <inheritdoc/>
public void Dispose()
{
this.ReleaseUnmanagedResources();
GC.SuppressFinalize(this);
}
/// <inheritdoc/>
public nint? ProcessWndProcW(HWND hWnd, uint msg, WPARAM wParam, LPARAM lParam) =>
this.imguiInput.ProcessWndProcW(hWnd, msg, wParam, lParam);
/// <inheritdoc/>
public void Render()
{
this.imguiRenderer.OnNewFrame();
this.NewRenderFrame?.Invoke();
this.imguiInput.NewFrame(this.targetWidth, this.targetHeight);
this.NewInputFrame?.Invoke();
ImGui.NewFrame();
ImGuizmo.BeginFrame();
this.BuildUi?.Invoke();
ImGui.Render();
this.imguiRenderer.RenderDrawData(ImGui.GetDrawData());
ImGui.UpdatePlatformWindows();
ImGui.RenderPlatformWindowsDefault();
}
/// <inheritdoc/>
public void OnPreResize() => this.imguiRenderer.OnPreResize();
/// <inheritdoc/>
public void OnPostResize(int newWidth, int newHeight)
{
this.imguiRenderer.OnPostResize(newWidth, newHeight);
this.targetWidth = newWidth;
this.targetHeight = newHeight;
}
/// <inheritdoc/>
public void InvalidateFonts() => this.imguiRenderer.RebuildFontTexture();
/// <inheritdoc/>
public bool IsImGuiCursor(nint cursorHandle) => this.imguiInput.IsImGuiCursor(cursorHandle);
/// <inheritdoc/>
public bool IsAttachedToPresentationTarget(nint targetHandle) =>
AreIUnknownEqual(this.swapChain.Get(), (IUnknown*)targetHandle)
|| AreIUnknownEqual(this.swapChainPossiblyWrapped.Get(), (IUnknown*)targetHandle);
/// <inheritdoc/>
public bool IsMainViewportFullScreen()
{
BOOL fullscreen;
this.swapChain.Get()->GetFullscreenState(&fullscreen, null);
return fullscreen;
}
private static bool AreIUnknownEqual<T1, T2>(T1* punk1, T2* punk2)
where T1 : unmanaged, IUnknown.Interface
where T2 : unmanaged, IUnknown.Interface
{
// https://learn.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-queryinterface(refiid_void)
// For any given COM object (also known as a COM component), a specific query for the IUnknown interface on any
// of the object's interfaces must always return the same pointer value.
if (punk1 is null || punk2 is null)
return false;
fixed (Guid* iid = &IID.IID_IUnknown)
{
using var u1 = default(ComPtr<IUnknown>);
if (punk1->QueryInterface(iid, (void**)u1.GetAddressOf()).FAILED)
return false;
using var u2 = default(ComPtr<IUnknown>);
if (punk2->QueryInterface(iid, (void**)u2.GetAddressOf()).FAILED)
return false;
return u1.Get() == u2.Get();
}
}
private void ReleaseUnmanagedResources()
{
if (this.device.IsEmpty())
return;
this.imguiRenderer?.Dispose();
this.imguiInput?.Dispose();
ImPlot.DestroyContext();
ImGui.DestroyContext();
this.swapChain.Dispose();
this.deviceContext.Dispose();
this.device.Dispose();
this.swapChainPossiblyWrapped.Dispose();
}
}

View file

@ -0,0 +1,655 @@
using System.Runtime.InteropServices;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
namespace Dalamud.Interface.ImGuiBackend.Helpers.D3D11;
/// <summary>
/// Captures states of a <see cref="ID3D11DeviceContext"/>.
/// </summary>
internal unsafe struct D3D11DeviceContextStateBackup : IDisposable
{
private InputAssemblerState inputAssemblerState;
private RasterizerState rasterizerState;
private OutputMergerState outputMergerState;
private VertexShaderState vertexShaderState;
private HullShaderState hullShaderState;
private DomainShaderState domainShaderState;
private GeometryShaderState geometryShaderState;
private PixelShaderState pixelShaderState;
private ComputeShaderState computeShaderState;
/// <summary>
/// Initializes a new instance of the <see cref="D3D11DeviceContextStateBackup"/> struct,
/// by capturing all states of a <see cref="ID3D11DeviceContext"/>.
/// </summary>
/// <param name="featureLevel">The feature level.</param>
/// <param name="ctx">The device context.</param>
public D3D11DeviceContextStateBackup(D3D_FEATURE_LEVEL featureLevel, ID3D11DeviceContext* ctx)
{
this.inputAssemblerState = InputAssemblerState.From(ctx);
this.rasterizerState = RasterizerState.From(ctx);
this.outputMergerState = OutputMergerState.From(featureLevel, ctx);
this.vertexShaderState = VertexShaderState.From(ctx);
this.hullShaderState = HullShaderState.From(ctx);
this.domainShaderState = DomainShaderState.From(ctx);
this.geometryShaderState = GeometryShaderState.From(ctx);
this.pixelShaderState = PixelShaderState.From(ctx);
this.computeShaderState = ComputeShaderState.From(featureLevel, ctx);
}
/// <inheritdoc/>
public void Dispose()
{
this.inputAssemblerState.Dispose();
this.rasterizerState.Dispose();
this.outputMergerState.Dispose();
this.vertexShaderState.Dispose();
this.hullShaderState.Dispose();
this.domainShaderState.Dispose();
this.geometryShaderState.Dispose();
this.pixelShaderState.Dispose();
this.computeShaderState.Dispose();
}
/// <summary>
/// Captures Input Assembler states of a <see cref="ID3D11DeviceContext"/>.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct InputAssemblerState : IDisposable
{
private const int BufferCount = TerraFX.Interop.DirectX.D3D11.D3D11_IA_VERTEX_INPUT_RESOURCE_SLOT_COUNT;
private ComPtr<ID3D11DeviceContext> context;
private ComPtr<ID3D11InputLayout> layout;
private ComPtr<ID3D11Buffer> indexBuffer;
private DXGI_FORMAT indexFormat;
private uint indexOffset;
private D3D_PRIMITIVE_TOPOLOGY topology;
private fixed ulong buffers[BufferCount];
private fixed uint strides[BufferCount];
private fixed uint offsets[BufferCount];
/// <summary>
/// Creates a new instance of <see cref="InputAssemblerState"/> from <paramref name="ctx"/>.
/// </summary>
/// <param name="ctx">The device context.</param>
/// <returns>The captured state.</returns>
public static InputAssemblerState From(ID3D11DeviceContext* ctx)
{
var state = default(InputAssemblerState);
state.context.Attach(ctx);
ctx->AddRef();
ctx->IAGetInputLayout(state.layout.GetAddressOf());
ctx->IAGetPrimitiveTopology(&state.topology);
ctx->IAGetIndexBuffer(state.indexBuffer.GetAddressOf(), &state.indexFormat, &state.indexOffset);
ctx->IAGetVertexBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers, state.strides, state.offsets);
return state;
}
/// <inheritdoc/>
public void Dispose()
{
var ctx = this.context.Get();
if (ctx is null)
return;
fixed (InputAssemblerState* pThis = &this)
{
ctx->IASetInputLayout(pThis->layout);
ctx->IASetPrimitiveTopology(pThis->topology);
ctx->IASetIndexBuffer(pThis->indexBuffer, pThis->indexFormat, pThis->indexOffset);
ctx->IASetVertexBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers, pThis->strides, pThis->offsets);
pThis->context.Dispose();
pThis->layout.Dispose();
pThis->indexBuffer.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11Buffer>>(pThis->buffers, BufferCount))
b.Dispose();
}
}
}
/// <summary>
/// Captures Rasterizer states of a <see cref="ID3D11DeviceContext"/>.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct RasterizerState : IDisposable
{
private const int Count = TerraFX.Interop.DirectX.D3D11.D3D11_VIEWPORT_AND_SCISSORRECT_MAX_INDEX;
private ComPtr<ID3D11DeviceContext> context;
private ComPtr<ID3D11RasterizerState> state;
private fixed byte viewports[24 * Count];
private fixed ulong scissorRects[16 * Count];
/// <summary>
/// Creates a new instance of <see cref="RasterizerState"/> from <paramref name="ctx"/>.
/// </summary>
/// <param name="ctx">The device context.</param>
/// <returns>The captured state.</returns>
public static RasterizerState From(ID3D11DeviceContext* ctx)
{
var state = default(RasterizerState);
state.context.Attach(ctx);
ctx->AddRef();
ctx->RSGetState(state.state.GetAddressOf());
uint n = Count;
ctx->RSGetViewports(&n, (D3D11_VIEWPORT*)state.viewports);
n = Count;
ctx->RSGetScissorRects(&n, (RECT*)state.scissorRects);
return state;
}
/// <inheritdoc/>
public void Dispose()
{
var ctx = this.context.Get();
if (ctx is null)
return;
fixed (RasterizerState* pThis = &this)
{
ctx->RSSetState(pThis->state);
ctx->RSSetViewports(Count, (D3D11_VIEWPORT*)pThis->viewports);
ctx->RSSetScissorRects(Count, (RECT*)pThis->scissorRects);
pThis->context.Dispose();
pThis->state.Dispose();
}
}
}
/// <summary>
/// Captures Output Merger states of a <see cref="ID3D11DeviceContext"/>.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct OutputMergerState : IDisposable
{
private const int RtvCount = TerraFX.Interop.DirectX.D3D11.D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT;
private const int UavCountMax = TerraFX.Interop.DirectX.D3D11.D3D11_1_UAV_SLOT_COUNT;
private ComPtr<ID3D11DeviceContext> context;
private ComPtr<ID3D11BlendState> blendState;
private fixed float blendFactor[4];
private uint sampleMask;
private uint stencilRef;
private ComPtr<ID3D11DepthStencilState> depthStencilState;
private fixed ulong rtvs[RtvCount]; // ID3D11RenderTargetView*[RtvCount]
private ComPtr<ID3D11DepthStencilView> dsv;
private fixed ulong uavs[UavCountMax]; // ID3D11UnorderedAccessView*[UavCount]
private int uavCount;
/// <summary>
/// Creates a new instance of <see cref="OutputMergerState"/> from <paramref name="ctx"/>.
/// </summary>
/// <param name="featureLevel">The feature level.</param>
/// <param name="ctx">The device context.</param>
/// <returns>The captured state.</returns>
public static OutputMergerState From(D3D_FEATURE_LEVEL featureLevel, ID3D11DeviceContext* ctx)
{
var state = default(OutputMergerState);
state.uavCount = featureLevel >= D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1
? TerraFX.Interop.DirectX.D3D11.D3D11_1_UAV_SLOT_COUNT
: TerraFX.Interop.DirectX.D3D11.D3D11_PS_CS_UAV_REGISTER_COUNT;
state.context.Attach(ctx);
ctx->AddRef();
ctx->OMGetBlendState(state.blendState.GetAddressOf(), state.blendFactor, &state.sampleMask);
ctx->OMGetDepthStencilState(state.depthStencilState.GetAddressOf(), &state.stencilRef);
ctx->OMGetRenderTargetsAndUnorderedAccessViews(
RtvCount,
(ID3D11RenderTargetView**)state.rtvs,
state.dsv.GetAddressOf(),
0,
(uint)state.uavCount,
(ID3D11UnorderedAccessView**)state.uavs);
return state;
}
/// <inheritdoc/>
public void Dispose()
{
var ctx = this.context.Get();
if (ctx is null)
return;
fixed (OutputMergerState* pThis = &this)
{
ctx->OMSetBlendState(pThis->blendState, pThis->blendFactor, pThis->sampleMask);
ctx->OMSetDepthStencilState(pThis->depthStencilState, pThis->stencilRef);
var rtvc = (uint)RtvCount;
while (rtvc > 0 && pThis->rtvs[rtvc - 1] == 0)
rtvc--;
var uavlb = rtvc;
while (uavlb < this.uavCount && pThis->uavs[uavlb] == 0)
uavlb++;
var uavc = (uint)this.uavCount;
while (uavc > uavlb && pThis->uavs[uavc - 1] == 0)
uavlb--;
uavc -= uavlb;
ctx->OMSetRenderTargetsAndUnorderedAccessViews(
rtvc,
(ID3D11RenderTargetView**)pThis->rtvs,
pThis->dsv,
uavc == 0 ? 0 : uavlb,
uavc,
uavc == 0 ? null : (ID3D11UnorderedAccessView**)pThis->uavs,
null);
this.context.Reset();
this.blendState.Reset();
this.depthStencilState.Reset();
this.dsv.Reset();
foreach (ref var b in new Span<ComPtr<ID3D11RenderTargetView>>(pThis->rtvs, RtvCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11UnorderedAccessView>>(pThis->uavs, this.uavCount))
b.Dispose();
}
}
}
/// <summary>
/// Captures Vertex Shader states of a <see cref="ID3D11DeviceContext"/>.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct VertexShaderState : IDisposable
{
private const int BufferCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT;
private const int SamplerCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT;
private const int ResourceCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT;
private const int ClassInstanceCount = 256; // According to msdn
private ComPtr<ID3D11DeviceContext> context;
private ComPtr<ID3D11VertexShader> shader;
private fixed ulong insts[ClassInstanceCount];
private fixed ulong buffers[BufferCount];
private fixed ulong samplers[SamplerCount];
private fixed ulong resources[ResourceCount];
private uint instCount;
/// <summary>
/// Creates a new instance of <see cref="VertexShaderState"/> from <paramref name="ctx"/>.
/// </summary>
/// <param name="ctx">The device context.</param>
/// <returns>The captured state.</returns>
public static VertexShaderState From(ID3D11DeviceContext* ctx)
{
var state = default(VertexShaderState);
state.context.Attach(ctx);
ctx->AddRef();
state.instCount = ClassInstanceCount;
ctx->VSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount);
ctx->VSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers);
ctx->VSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers);
ctx->VSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources);
return state;
}
/// <inheritdoc/>
public void Dispose()
{
var ctx = this.context.Get();
if (ctx is null)
return;
fixed (VertexShaderState* pThis = &this)
{
ctx->VSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount);
ctx->VSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers);
ctx->VSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers);
ctx->VSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources);
foreach (ref var b in new Span<ComPtr<ID3D11Buffer>>(pThis->buffers, BufferCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11SamplerState>>(pThis->samplers, SamplerCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ShaderResourceView>>(pThis->resources, ResourceCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ClassInstance>>(pThis->insts, (int)pThis->instCount))
b.Dispose();
pThis->context.Dispose();
pThis->shader.Dispose();
}
}
}
/// <summary>
/// Captures Hull Shader states of a <see cref="ID3D11DeviceContext"/>.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct HullShaderState : IDisposable
{
private const int BufferCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT;
private const int SamplerCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT;
private const int ResourceCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT;
private const int ClassInstanceCount = 256; // According to msdn
private ComPtr<ID3D11DeviceContext> context;
private ComPtr<ID3D11HullShader> shader;
private fixed ulong insts[ClassInstanceCount];
private fixed ulong buffers[BufferCount];
private fixed ulong samplers[SamplerCount];
private fixed ulong resources[ResourceCount];
private uint instCount;
/// <summary>
/// Creates a new instance of <see cref="HullShaderState"/> from <paramref name="ctx"/>.
/// </summary>
/// <param name="ctx">The device context.</param>
/// <returns>The captured state.</returns>
public static HullShaderState From(ID3D11DeviceContext* ctx)
{
var state = default(HullShaderState);
state.context.Attach(ctx);
ctx->AddRef();
state.instCount = ClassInstanceCount;
ctx->HSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount);
ctx->HSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers);
ctx->HSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers);
ctx->HSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources);
return state;
}
/// <inheritdoc/>
public void Dispose()
{
var ctx = this.context.Get();
if (ctx is null)
return;
fixed (HullShaderState* pThis = &this)
{
ctx->HSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount);
ctx->HSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers);
ctx->HSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers);
ctx->HSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources);
foreach (ref var b in new Span<ComPtr<ID3D11Buffer>>(pThis->buffers, BufferCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11SamplerState>>(pThis->samplers, SamplerCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ShaderResourceView>>(pThis->resources, ResourceCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ClassInstance>>(pThis->insts, (int)pThis->instCount))
b.Dispose();
pThis->context.Dispose();
pThis->shader.Dispose();
}
}
}
/// <summary>
/// Captures Domain Shader states of a <see cref="ID3D11DeviceContext"/>.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DomainShaderState : IDisposable
{
private const int BufferCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT;
private const int SamplerCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT;
private const int ResourceCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT;
private const int ClassInstanceCount = 256; // According to msdn
private ComPtr<ID3D11DeviceContext> context;
private ComPtr<ID3D11DomainShader> shader;
private fixed ulong insts[ClassInstanceCount];
private fixed ulong buffers[BufferCount];
private fixed ulong samplers[SamplerCount];
private fixed ulong resources[ResourceCount];
private uint instCount;
/// <summary>
/// Creates a new instance of <see cref="DomainShaderState"/> from <paramref name="ctx"/>.
/// </summary>
/// <param name="ctx">The device context.</param>
/// <returns>The captured state.</returns>
public static DomainShaderState From(ID3D11DeviceContext* ctx)
{
var state = default(DomainShaderState);
state.context.Attach(ctx);
ctx->AddRef();
state.instCount = ClassInstanceCount;
ctx->DSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount);
ctx->DSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers);
ctx->DSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers);
ctx->DSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources);
return state;
}
/// <inheritdoc/>
public void Dispose()
{
var ctx = this.context.Get();
if (ctx is null)
return;
fixed (DomainShaderState* pThis = &this)
{
ctx->DSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount);
ctx->DSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers);
ctx->DSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers);
ctx->DSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources);
foreach (ref var b in new Span<ComPtr<ID3D11Buffer>>(pThis->buffers, BufferCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11SamplerState>>(pThis->samplers, SamplerCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ShaderResourceView>>(pThis->resources, ResourceCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ClassInstance>>(pThis->insts, (int)pThis->instCount))
b.Dispose();
pThis->context.Dispose();
pThis->shader.Dispose();
}
}
}
/// <summary>
/// Captures Geometry Shader states of a <see cref="ID3D11DeviceContext"/>.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct GeometryShaderState : IDisposable
{
private const int BufferCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT;
private const int SamplerCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT;
private const int ResourceCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT;
private const int ClassInstanceCount = 256; // According to msdn
private ComPtr<ID3D11DeviceContext> context;
private ComPtr<ID3D11GeometryShader> shader;
private fixed ulong insts[ClassInstanceCount];
private fixed ulong buffers[BufferCount];
private fixed ulong samplers[SamplerCount];
private fixed ulong resources[ResourceCount];
private uint instCount;
/// <summary>
/// Creates a new instance of <see cref="GeometryShaderState"/> from <paramref name="ctx"/>.
/// </summary>
/// <param name="ctx">The device context.</param>
/// <returns>The captured state.</returns>
public static GeometryShaderState From(ID3D11DeviceContext* ctx)
{
var state = default(GeometryShaderState);
state.context.Attach(ctx);
ctx->AddRef();
state.instCount = ClassInstanceCount;
ctx->GSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount);
ctx->GSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers);
ctx->GSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers);
ctx->GSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources);
return state;
}
/// <inheritdoc/>
public void Dispose()
{
var ctx = this.context.Get();
if (ctx is null)
return;
fixed (GeometryShaderState* pThis = &this)
{
ctx->GSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount);
ctx->GSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers);
ctx->GSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers);
ctx->GSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources);
foreach (ref var b in new Span<ComPtr<ID3D11Buffer>>(pThis->buffers, BufferCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11SamplerState>>(pThis->samplers, SamplerCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ShaderResourceView>>(pThis->resources, ResourceCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ClassInstance>>(pThis->insts, (int)pThis->instCount))
b.Dispose();
pThis->context.Dispose();
pThis->shader.Dispose();
}
}
}
/// <summary>
/// Captures Pixel Shader states of a <see cref="ID3D11DeviceContext"/>.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct PixelShaderState : IDisposable
{
private const int BufferCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT;
private const int SamplerCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT;
private const int ResourceCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT;
private const int ClassInstanceCount = 256; // According to msdn
private ComPtr<ID3D11DeviceContext> context;
private ComPtr<ID3D11PixelShader> shader;
private fixed ulong insts[ClassInstanceCount];
private fixed ulong buffers[BufferCount];
private fixed ulong samplers[SamplerCount];
private fixed ulong resources[ResourceCount];
private uint instCount;
/// <summary>
/// Creates a new instance of <see cref="PixelShaderState"/> from <paramref name="ctx"/>.
/// </summary>
/// <param name="ctx">The device context.</param>
/// <returns>The captured state.</returns>
public static PixelShaderState From(ID3D11DeviceContext* ctx)
{
var state = default(PixelShaderState);
state.context.Attach(ctx);
ctx->AddRef();
state.instCount = ClassInstanceCount;
ctx->PSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount);
ctx->PSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers);
ctx->PSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers);
ctx->PSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources);
return state;
}
/// <inheritdoc/>
public void Dispose()
{
var ctx = this.context.Get();
if (ctx is null)
return;
fixed (PixelShaderState* pThis = &this)
{
ctx->PSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount);
ctx->PSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers);
ctx->PSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers);
ctx->PSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources);
foreach (ref var b in new Span<ComPtr<ID3D11Buffer>>(pThis->buffers, BufferCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11SamplerState>>(pThis->samplers, SamplerCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ShaderResourceView>>(pThis->resources, ResourceCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ClassInstance>>(pThis->insts, (int)pThis->instCount))
b.Dispose();
pThis->context.Dispose();
pThis->shader.Dispose();
}
}
}
/// <summary>
/// Captures Compute Shader states of a <see cref="ID3D11DeviceContext"/>.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct ComputeShaderState : IDisposable
{
private const int BufferCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT;
private const int SamplerCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT;
private const int ResourceCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT;
private const int InstanceCount = 256; // According to msdn
private const int UavCountMax = TerraFX.Interop.DirectX.D3D11.D3D11_1_UAV_SLOT_COUNT;
private ComPtr<ID3D11DeviceContext> context;
private ComPtr<ID3D11ComputeShader> shader;
private fixed ulong insts[InstanceCount]; // ID3D11ClassInstance*[BufferCount]
private fixed ulong buffers[BufferCount]; // ID3D11Buffer*[BufferCount]
private fixed ulong samplers[SamplerCount]; // ID3D11SamplerState*[SamplerCount]
private fixed ulong resources[ResourceCount]; // ID3D11ShaderResourceView*[ResourceCount]
private fixed ulong uavs[UavCountMax]; // ID3D11UnorderedAccessView*[UavCountMax]
private uint instCount;
private int uavCount;
/// <summary>
/// Creates a new instance of <see cref="ComputeShaderState"/> from <paramref name="ctx"/>.
/// </summary>
/// <param name="featureLevel">The feature level.</param>
/// <param name="ctx">The device context.</param>
/// <returns>The captured state.</returns>
public static ComputeShaderState From(D3D_FEATURE_LEVEL featureLevel, ID3D11DeviceContext* ctx)
{
var state = default(ComputeShaderState);
state.uavCount = featureLevel >= D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1
? TerraFX.Interop.DirectX.D3D11.D3D11_1_UAV_SLOT_COUNT
: TerraFX.Interop.DirectX.D3D11.D3D11_PS_CS_UAV_REGISTER_COUNT;
state.context.Attach(ctx);
ctx->AddRef();
state.instCount = InstanceCount;
ctx->CSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount);
ctx->CSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers);
ctx->CSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers);
ctx->CSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources);
ctx->CSGetUnorderedAccessViews(0, (uint)state.uavCount, (ID3D11UnorderedAccessView**)state.uavs);
return state;
}
/// <inheritdoc/>
public void Dispose()
{
var ctx = this.context.Get();
if (ctx is null)
return;
fixed (ComputeShaderState* pThis = &this)
{
ctx->CSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount);
ctx->CSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers);
ctx->CSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers);
ctx->CSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources);
ctx->CSSetUnorderedAccessViews(0, (uint)this.uavCount, (ID3D11UnorderedAccessView**)pThis->uavs, null);
foreach (ref var b in new Span<ComPtr<ID3D11Buffer>>(pThis->buffers, BufferCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11SamplerState>>(pThis->samplers, SamplerCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ShaderResourceView>>(pThis->resources, ResourceCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ClassInstance>>(pThis->insts, (int)pThis->instCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11UnorderedAccessView>>(pThis->uavs, this.uavCount))
b.Dispose();
pThis->context.Dispose();
pThis->shader.Dispose();
}
}
}
}

View file

@ -0,0 +1,26 @@
using System.Text;
using Dalamud.Utility;
using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.ImGuiBackend.Helpers.D3D11;
/// <summary>Utility extension methods for D3D11 objects.</summary>
internal static class Extensions
{
/// <summary>Sets the name for debugging.</summary>
/// <param name="child">D3D11 object.</param>
/// <param name="name">Debug name.</param>
/// <typeparam name="T">Object type.</typeparam>
public static unsafe void SetDebugName<T>(ref this T child, string name)
where T : unmanaged, ID3D11DeviceChild.Interface
{
var len = Encoding.UTF8.GetByteCount(name);
var buf = stackalloc byte[len + 1];
Encoding.UTF8.GetBytes(name, new(buf, len + 1));
buf[len] = 0;
fixed (Guid* pId = &DirectX.WKPDID_D3DDebugObjectName)
child.SetPrivateData(pId, (uint)(len + 1), buf).ThrowOnError();
}
}

View file

@ -0,0 +1,165 @@
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using Dalamud.Bindings.ImGui;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
namespace Dalamud.Interface.ImGuiBackend.Helpers;
/// <summary>
/// Helpers for using ImGui Viewports.
/// </summary>
internal static class ImGuiViewportHelpers
{
/// <summary>
/// Delegate to be called when a window should be created.
/// </summary>
/// <param name="viewport">An instance of <see cref="ImGuiViewportPtr"/>.</param>
public delegate void CreateWindowDelegate(ImGuiViewportPtr viewport);
/// <summary>
/// Delegate to be called when a window should be destroyed.
/// </summary>
/// <param name="viewport">An instance of <see cref="ImGuiViewportPtr"/>.</param>
public delegate void DestroyWindowDelegate(ImGuiViewportPtr viewport);
/// <summary>
/// Delegate to be called when a window should be resized.
/// </summary>
/// <param name="viewport">An instance of <see cref="ImGuiViewportPtr"/>.</param>
/// <param name="size">Size of the new window.</param>
public delegate void SetWindowSizeDelegate(ImGuiViewportPtr viewport, Vector2 size);
/// <summary>
/// Delegate to be called when a window should be rendered.
/// </summary>
/// <param name="viewport">An instance of <see cref="ImGuiViewportPtr"/>.</param>
/// <param name="v">Custom user-provided argument from <see cref="ImGui.RenderPlatformWindowsDefault()"/>.</param>
public delegate void RenderWindowDelegate(ImGuiViewportPtr viewport, nint v);
/// <summary>
/// Delegate to be called when buffers for the window should be swapped.
/// </summary>
/// <param name="viewport">An instance of <see cref="ImGuiViewportPtr"/>.</param>
/// <param name="v">Custom user-provided argument from <see cref="ImGui.RenderPlatformWindowsDefault()"/>.</param>
public delegate void SwapBuffersDelegate(ImGuiViewportPtr viewport, nint v);
/// <summary>
/// Delegate to be called when the window should be showed.
/// </summary>
/// <param name="viewport">An instance of <see cref="ImGuiViewportPtr"/>.</param>
public delegate void ShowWindowDelegate(ImGuiViewportPtr viewport);
/// <summary>
/// Delegate to be called when the window should be updated.
/// </summary>
/// <param name="viewport">An instance of <see cref="ImGuiViewportPtr"/>.</param>
public delegate void UpdateWindowDelegate(ImGuiViewportPtr viewport);
/// <summary>
/// Delegate to be called when the window position is queried.
/// </summary>
/// <param name="returnStorage">The return value storage.</param>
/// <param name="viewport">An instance of <see cref="ImGuiViewportPtr"/>.</param>
/// <returns>Same value with <paramref name="returnStorage"/>.</returns>
public unsafe delegate Vector2* GetWindowPosDelegate(Vector2* returnStorage, ImGuiViewportPtr viewport);
/// <summary>
/// Delegate to be called when the window should be moved.
/// </summary>
/// <param name="viewport">An instance of <see cref="ImGuiViewportPtr"/>.</param>
/// <param name="pos">The new position.</param>
public delegate void SetWindowPosDelegate(ImGuiViewportPtr viewport, Vector2 pos);
/// <summary>
/// Delegate to be called when the window size is queried.
/// </summary>
/// <param name="returnStorage">The return value storage.</param>
/// <param name="viewport">An instance of <see cref="ImGuiViewportPtr"/>.</param>
/// <returns>Same value with <paramref name="returnStorage"/>.</returns>
public unsafe delegate Vector2* GetWindowSizeDelegate(Vector2* returnStorage, ImGuiViewportPtr viewport);
/// <summary>
/// Delegate to be called when the window should be given focus.
/// </summary>
/// <param name="viewport">An instance of <see cref="ImGuiViewportPtr"/>.</param>
public delegate void SetWindowFocusDelegate(ImGuiViewportPtr viewport);
/// <summary>
/// Delegate to be called when the window is focused.
/// </summary>
/// <param name="viewport">An instance of <see cref="ImGuiViewportPtr"/>.</param>
/// <returns>Whether the window is focused.</returns>
public delegate bool GetWindowFocusDelegate(ImGuiViewportPtr viewport);
/// <summary>
/// Delegate to be called when whether the window is minimized is queried.
/// </summary>
/// <param name="viewport">An instance of <see cref="ImGuiViewportPtr"/>.</param>
/// <returns>Whether the window is minimized.</returns>
public delegate bool GetWindowMinimizedDelegate(ImGuiViewportPtr viewport);
/// <summary>
/// Delegate to be called when the window title should be changed.
/// </summary>
/// <param name="viewport">An instance of <see cref="ImGuiViewportPtr"/>.</param>
/// <param name="title">The new title.</param>
public delegate void SetWindowTitleDelegate(ImGuiViewportPtr viewport, string title);
/// <summary>
/// Delegate to be called when the window alpha should be changed.
/// </summary>
/// <param name="viewport">An instance of <see cref="ImGuiViewportPtr"/>.</param>
/// <param name="alpha">The new alpha.</param>
public delegate void SetWindowAlphaDelegate(ImGuiViewportPtr viewport, float alpha);
/// <summary>
/// Delegate to be called when the IME input position should be changed.
/// </summary>
/// <param name="viewport">An instance of <see cref="ImGuiViewportPtr"/>.</param>
/// <param name="pos">The new position.</param>
public delegate void SetImeInputPosDelegate(ImGuiViewportPtr viewport, Vector2 pos);
/// <summary>
/// Delegate to be called when the window's DPI scale value is queried.
/// </summary>
/// <param name="viewport">An instance of <see cref="ImGuiViewportPtr"/>.</param>
/// <returns>The DPI scale.</returns>
public delegate float GetWindowDpiScaleDelegate(ImGuiViewportPtr viewport);
/// <summary>
/// Delegate to be called when viewport is changed.
/// </summary>
/// <param name="viewport">An instance of <see cref="ImGuiViewportPtr"/>.</param>
public delegate void ChangedViewportDelegate(ImGuiViewportPtr viewport);
/// <summary>
/// Disables ImGui from disabling alpha for Viewport window backgrounds.
/// </summary>
public static unsafe void EnableViewportWindowBackgroundAlpha()
{
// TODO: patch imgui.cpp:6126, which disables background transparency for extra viewport windows
var offset = 0x00007FFB6ADA632C - 0x00007FFB6AD60000;
offset += Process.GetCurrentProcess().Modules.Cast<ProcessModule>().First(x => x.ModuleName == "cimgui.dll")
.BaseAddress;
var b = (byte*)offset;
uint old;
if (!VirtualProtect(b, 1, PAGE.PAGE_EXECUTE_READWRITE, &old))
{
throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error())
?? throw new InvalidOperationException($"{nameof(VirtualProtect)} failed.");
}
*b = 0xEB;
if (!VirtualProtect(b, 1, old, &old))
{
throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error())
?? throw new InvalidOperationException($"{nameof(VirtualProtect)} failed.");
}
}
}

View file

@ -0,0 +1,230 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
namespace Dalamud.Interface.ImGuiBackend.Helpers;
/// <summary>
/// Peels ReShade off stuff.
/// </summary>
[SuppressMessage(
"StyleCop.CSharp.LayoutRules",
"SA1519:Braces should not be omitted from multi-line child statement",
Justification = "Multiple fixed blocks")]
internal static unsafe class ReShadePeeler
{
/// <summary>
/// Peels <see cref="IDXGISwapChain"/> if it is wrapped by ReShade.
/// </summary>
/// <param name="comptr">[inout] The COM pointer to an instance of <see cref="IDXGISwapChain"/>.</param>
/// <typeparam name="T">A COM type that is or extends <see cref="IDXGISwapChain"/>.</typeparam>
/// <returns><c>true</c> if peeled.</returns>
public static bool PeelSwapChain<T>(ComPtr<T>* comptr)
where T : unmanaged, IDXGISwapChain.Interface =>
PeelIUnknown(comptr, sizeof(IDXGISwapChain.Vtbl<IDXGISwapChain>));
private static bool PeelIUnknown<T>(ComPtr<T>* comptr, nint vtblSize)
where T : unmanaged, IUnknown.Interface
{
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 = 8; i <= 0x20; i += 8)
{
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<IUnknown>);
punk.Attach((IUnknown*)pObjectBehind);
// Is the IUnknown object also the type we want?
using var comptr2 = default(ComPtr<T>);
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)
continue;
var dosh = (IMAGE_DOS_HEADER*)processModule.BaseAddress;
var nth = (IMAGE_NT_HEADERS64*)(processModule.BaseAddress + dosh->e_lfanew);
if (ptr >= processModule.BaseAddress + nth->OptionalHeader.SizeOfImage)
continue;
fixed (byte* pfn0 = "CreateDXGIFactory"u8)
fixed (byte* pfn1 = "D2D1CreateDevice"u8)
fixed (byte* pfn2 = "D3D10CreateDevice"u8)
fixed (byte* pfn3 = "D3D11CreateDevice"u8)
fixed (byte* pfn4 = "D3D12CreateDevice"u8)
fixed (byte* pfn5 = "glBegin"u8)
fixed (byte* pfn6 = "vkCreateDevice"u8)
{
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn0) == 0)
continue;
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn1) == 0)
continue;
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn2) == 0)
continue;
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn3) == 0)
continue;
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn4) == 0)
continue;
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn5) == 0)
continue;
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn6) == 0)
continue;
}
var fileInfo = FileVersionInfo.GetVersionInfo(processModule.FileName);
if (fileInfo.FileDescription == null)
continue;
if (!fileInfo.FileDescription.Contains("GShade") && !fileInfo.FileDescription.Contains("ReShade"))
continue;
return true;
}
return false;
}
private static bool IsReShadedComObject<T>(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);
}
}

View file

@ -0,0 +1,64 @@
using Dalamud.Interface.ImGuiBackend.Delegates;
using Dalamud.Interface.ImGuiBackend.InputHandler;
using Dalamud.Interface.ImGuiBackend.Renderers;
namespace Dalamud.Interface.ImGuiBackend;
/// <summary>Backend for ImGui.</summary>
internal interface IImGuiBackend : IDisposable
{
/// <summary>User methods invoked every ImGui frame to construct custom UIs.</summary>
event ImGuiBuildUiDelegate? BuildUi;
/// <summary>User methods invoked every ImGui frame on handling inputs.</summary>
event ImGuiNewInputFrameDelegate? NewInputFrame;
/// <summary>User methods invoked every ImGui frame on handling renders.</summary>
event ImGuiNewRenderFrameDelegate? NewRenderFrame;
/// <summary>Gets or sets a value indicating whether the cursor should be overridden with the ImGui cursor.
/// </summary>
bool UpdateCursor { get; set; }
/// <summary>Gets or sets the path of ImGui configuration .ini file.</summary>
string? IniPath { get; set; }
/// <summary>Gets the device handle.</summary>
nint DeviceHandle { get; }
/// <summary>Gets the input handler.</summary>
IImGuiInputHandler InputHandler { get; }
/// <summary>Gets the renderer.</summary>
IImGuiRenderer Renderer { get; }
/// <summary>Performs a render cycle.</summary>
void Render();
/// <summary>Handles stuff before resizing happens.</summary>
void OnPreResize();
/// <summary>Handles stuff after resizing happens.</summary>
/// <param name="newWidth">The new width.</param>
/// <param name="newHeight">The new height.</param>
void OnPostResize(int newWidth, int newHeight);
/// <summary>Invalidates fonts immediately.</summary>
/// <remarks>Call this while handling <see cref="NewRenderFrame"/>.</remarks>
void InvalidateFonts();
/// <summary>Determines if <paramref name="cursorHandle"/> is owned by this.</summary>
/// <param name="cursorHandle">The cursor.</param>
/// <returns>Whether it is the case.</returns>
bool IsImGuiCursor(nint cursorHandle);
/// <summary>Determines if this instance of <see cref="IImGuiBackend"/> is rendering to
/// <paramref name="targetHandle"/>. </summary>
/// <param name="targetHandle">The present target handle.</param>
/// <returns>Whether it is the case.</returns>
bool IsAttachedToPresentationTarget(nint targetHandle);
/// <summary>Determines if the main viewport is full screen. </summary>
/// <returns>Whether it is the case.</returns>
bool IsMainViewportFullScreen();
}

View file

@ -0,0 +1,15 @@
using TerraFX.Interop.Windows;
namespace Dalamud.Interface.ImGuiBackend;
/// <summary><see cref="IImGuiBackend"/> with Win32 support.</summary>
internal interface IWin32Backend : IImGuiBackend
{
/// <summary>Processes window messages.</summary>
/// <param name="hWnd">Handle of the window.</param>
/// <param name="msg">Type of window message.</param>
/// <param name="wParam">wParam.</param>
/// <param name="lParam">lParam.</param>
/// <returns>Return value.</returns>
public nint? ProcessWndProcW(HWND hWnd, uint msg, WPARAM wParam, LPARAM lParam);
}

Some files were not shown because too many files have changed in this diff Show more