mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
Compare commits
66 commits
5ea8ae7941
...
a5b57106be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5b57106be | ||
|
|
8dcbd52c22 | ||
|
|
1b5fbaa82e | ||
|
|
9bce0d33a6 | ||
|
|
879c210cc6 | ||
|
|
1fe2d54128 | ||
|
|
bfd592abbe | ||
|
|
df0bfc18c3 | ||
|
|
3fbc24904a | ||
|
|
5bb212bfaa | ||
|
|
f055af7f7b | ||
|
|
a917ebd856 | ||
|
|
0e6dae9f64 | ||
|
|
874745651b | ||
|
|
ead1c705a4 | ||
|
|
a31dda7865 | ||
|
|
d7e04ad4ff | ||
|
|
c661faea6b | ||
|
|
d4f1636dd2 | ||
|
|
196a5ef709 | ||
|
|
5e192ef39b | ||
|
|
947518b3d6 | ||
|
|
2cef75bbbe | ||
|
|
8ab7b59ae4 | ||
|
|
7b286c427c | ||
|
|
0d8f577576 | ||
|
|
01d8fc0c7e | ||
|
|
71927a8bf6 | ||
|
|
6a69a6e197 | ||
|
|
cc91916574 | ||
|
|
b7dda599fb | ||
|
|
63b7ecf0d7 | ||
|
|
e4eca842d3 | ||
|
|
c79fa96505 | ||
|
|
ba0cf4c990 | ||
|
|
9a49a9588b | ||
|
|
19a3926051 | ||
|
|
4937a2f4bd | ||
|
|
78ed4a2b01 | ||
|
|
62b9c1f2a1 | ||
|
|
a2e923b051 | ||
|
|
de396e70f8 | ||
|
|
7a8f01f418 | ||
|
|
9d0879148c | ||
|
|
778c82fad2 | ||
|
|
7f2ed9adb6 | ||
|
|
53b94caeb7 | ||
|
|
d1dc81318a | ||
|
|
a48eead85e | ||
|
|
d1bed3ebc5 | ||
|
|
23e7c164d8 | ||
|
|
8a9b47c7a4 | ||
|
|
520e3ea028 | ||
|
|
dd70c5b8ee | ||
|
|
2b2f628096 | ||
|
|
6340afb692 | ||
|
|
928fbba489 | ||
|
|
7bc921f543 | ||
|
|
a37a13e0ba | ||
|
|
e0eff2fe74 | ||
|
|
7d76d27555 | ||
|
|
4e87b4b007 | ||
|
|
544f8b28bf | ||
|
|
e5451c37af | ||
|
|
40e63f2d9a | ||
|
|
c19ea6ace3 |
89 changed files with 2659 additions and 1187 deletions
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
|
|
@ -23,7 +23,7 @@ jobs:
|
|||
uses: microsoft/setup-msbuild@v1.0.2
|
||||
- uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: '9.0.200'
|
||||
dotnet-version: '10.0.100'
|
||||
- name: Define VERSION
|
||||
run: |
|
||||
$env:COMMIT = $env:GITHUB_SHA.Substring(0, 7)
|
||||
|
|
|
|||
|
|
@ -87,7 +87,6 @@
|
|||
"CompileDalamudCrashHandler",
|
||||
"CompileImGuiNatives",
|
||||
"CompileInjector",
|
||||
"CompileInjectorBoot",
|
||||
"Restore",
|
||||
"SetCILogging",
|
||||
"Test"
|
||||
|
|
@ -115,7 +114,6 @@
|
|||
"CompileDalamudCrashHandler",
|
||||
"CompileImGuiNatives",
|
||||
"CompileInjector",
|
||||
"CompileInjectorBoot",
|
||||
"Restore",
|
||||
"SetCILogging",
|
||||
"Test"
|
||||
|
|
|
|||
|
|
@ -108,6 +108,11 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
|
|||
config.LogName = json.value("LogName", config.LogName);
|
||||
config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory);
|
||||
config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory);
|
||||
|
||||
if (json.contains("TempDirectory") && !json["TempDirectory"].is_null()) {
|
||||
config.TempDirectory = json.value("TempDirectory", config.TempDirectory);
|
||||
}
|
||||
|
||||
config.Language = json.value("Language", config.Language);
|
||||
config.Platform = json.value("Platform", config.Platform);
|
||||
config.GameVersion = json.value("GameVersion", config.GameVersion);
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ struct DalamudStartInfo {
|
|||
std::string ConfigurationPath;
|
||||
std::string LogPath;
|
||||
std::string LogName;
|
||||
std::string TempDirectory;
|
||||
std::string PluginDirectory;
|
||||
std::string AssetDirectory;
|
||||
ClientLanguage Language = ClientLanguage::English;
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ static DalamudExpected<void> append_injector_launch_args(std::vector<std::wstrin
|
|||
args.emplace_back(L"--logname=\"" + unicode::convert<std::wstring>(g_startInfo.LogName) + L"\"");
|
||||
args.emplace_back(L"--dalamud-plugin-directory=\"" + unicode::convert<std::wstring>(g_startInfo.PluginDirectory) + L"\"");
|
||||
args.emplace_back(L"--dalamud-asset-directory=\"" + unicode::convert<std::wstring>(g_startInfo.AssetDirectory) + L"\"");
|
||||
args.emplace_back(L"--dalamud-temp-directory=\"" + unicode::convert<std::wstring>(g_startInfo.TempDirectory) + L"\"");
|
||||
args.emplace_back(std::format(L"--dalamud-client-language={}", static_cast<int>(g_startInfo.Language)));
|
||||
args.emplace_back(std::format(L"--dalamud-delay-initialize={}", g_startInfo.DelayInitializeMs));
|
||||
// NoLoadPlugins/NoLoadThirdPartyPlugins: supplied from DalamudCrashHandler
|
||||
|
|
@ -268,7 +269,7 @@ LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex)
|
|||
|
||||
if (!is_ffxiv_address(L"ffxiv_dx11.exe", ex->ContextRecord->Rip) &&
|
||||
!is_ffxiv_address(L"cimgui.dll", ex->ContextRecord->Rip))
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
return exception_handler(ex);
|
||||
|
|
@ -297,7 +298,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
|
|||
if (HANDLE hReadPipeRaw, hWritePipeRaw; CreatePipe(&hReadPipeRaw, &hWritePipeRaw, nullptr, 65536))
|
||||
{
|
||||
hWritePipe.emplace(hWritePipeRaw, &CloseHandle);
|
||||
|
||||
|
||||
if (HANDLE hReadPipeInheritableRaw; DuplicateHandle(GetCurrentProcess(), hReadPipeRaw, GetCurrentProcess(), &hReadPipeInheritableRaw, 0, TRUE, DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE))
|
||||
{
|
||||
hReadPipeInheritable.emplace(hReadPipeInheritableRaw, &CloseHandle);
|
||||
|
|
@ -315,9 +316,9 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
|
|||
}
|
||||
|
||||
// additional information
|
||||
STARTUPINFOEXW siex{};
|
||||
STARTUPINFOEXW siex{};
|
||||
PROCESS_INFORMATION pi{};
|
||||
|
||||
|
||||
siex.StartupInfo.cb = sizeof siex;
|
||||
siex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW;
|
||||
siex.StartupInfo.wShowWindow = g_startInfo.CrashHandlerShow ? SW_SHOW : SW_HIDE;
|
||||
|
|
@ -385,7 +386,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
|
|||
argstr.push_back(L' ');
|
||||
}
|
||||
argstr.pop_back();
|
||||
|
||||
|
||||
if (!handles.empty() && !UpdateProcThreadAttribute(siex.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &handles[0], std::span(handles).size_bytes(), nullptr, nullptr))
|
||||
{
|
||||
logging::W("Failed to launch DalamudCrashHandler.exe: UpdateProcThreadAttribute error 0x{:x}", GetLastError());
|
||||
|
|
@ -400,7 +401,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
|
|||
TRUE, // Set handle inheritance to FALSE
|
||||
EXTENDED_STARTUPINFO_PRESENT, // lpStartupInfo actually points to a STARTUPINFOEX(W)
|
||||
nullptr, // Use parent's environment block
|
||||
nullptr, // Use parent's starting directory
|
||||
nullptr, // Use parent's starting directory
|
||||
&siex.StartupInfo, // Pointer to STARTUPINFO structure
|
||||
&pi // Pointer to PROCESS_INFORMATION structure (removed extra parentheses)
|
||||
))
|
||||
|
|
@ -416,7 +417,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
|
|||
}
|
||||
|
||||
CloseHandle(pi.hThread);
|
||||
|
||||
|
||||
g_crashhandler_process = pi.hProcess;
|
||||
g_crashhandler_pipe_write = hWritePipe->release();
|
||||
logging::I("Launched DalamudCrashHandler.exe: PID {}", pi.dwProcessId);
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@ public record DalamudStartInfo
|
|||
/// </summary>
|
||||
public string? ConfigurationPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the directory for temporary files. This directory needs to exist and be writable to the user.
|
||||
/// It should also be predictable and easy for launchers to find.
|
||||
/// </summary>
|
||||
public string? TempDirectory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path of the log files.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,111 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{8874326B-E755-4D13-90B4-59AB263A3E6B}</ProjectGuid>
|
||||
<RootNamespace>Dalamud_Injector_Boot</RootNamespace>
|
||||
<Configuration Condition=" '$(Configuration)'=='' ">Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
<TargetName>Dalamud.Injector</TargetName>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
<OutDir>..\bin\$(Configuration)\</OutDir>
|
||||
<IntDir>obj\$(Configuration)\</IntDir>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp23</LanguageStandard>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
|
||||
<PreprocessorDefinitions>CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<EnableUAC>false</EnableUAC>
|
||||
<AdditionalLibraryDirectories>..\lib\CoreCLR;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<ProgramDatabaseFile>$(OutDir)$(TargetName).Boot.pdb</ProgramDatabaseFile>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>false</IntrinsicFunctions>
|
||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<EnableCOMDATFolding>false</EnableCOMDATFolding>
|
||||
<OptimizeReferences>false</OptimizeReferences>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ItemGroup>
|
||||
<Content Include="..\lib\CoreCLR\nethost\nethost.dll">
|
||||
<Link>nethost.dll</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="dalamud.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="resources.rc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\Dalamud.Boot\logging.cpp" />
|
||||
<ClCompile Include="..\Dalamud.Boot\unicode.cpp" />
|
||||
<ClCompile Include="..\lib\CoreCLR\boot.cpp" />
|
||||
<ClCompile Include="..\lib\CoreCLR\CoreCLR.cpp" />
|
||||
<ClCompile Include="main.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\Dalamud.Boot\logging.h" />
|
||||
<ClInclude Include="..\Dalamud.Boot\unicode.h" />
|
||||
<ClInclude Include="..\lib\CoreCLR\CoreCLR.h" />
|
||||
<ClInclude Include="..\lib\CoreCLR\core\coreclr_delegates.h" />
|
||||
<ClInclude Include="..\lib\CoreCLR\core\hostfxr.h" />
|
||||
<ClInclude Include="..\lib\CoreCLR\nethost\nethost.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
</ItemGroup>
|
||||
<Target Name="RemoveExtraFiles" AfterTargets="PostBuildEvent">
|
||||
<Delete Files="$(OutDir)$(TargetName).lib" />
|
||||
<Delete Files="$(OutDir)$(TargetName).exp" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Header Files">
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Resource Files">
|
||||
<UniqueIdentifier>{4faac519-3a73-4b2b-96e7-fb597f02c0be}</UniqueIdentifier>
|
||||
<Extensions>ico;rc</Extensions>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="dalamud.ico">
|
||||
<Filter>Resource Files</Filter>
|
||||
</Image>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="resources.rc">
|
||||
<Filter>Resource Files</Filter>
|
||||
</ResourceCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="main.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\lib\CoreCLR\boot.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\lib\CoreCLR\CoreCLR.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\Dalamud.Boot\logging.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\Dalamud.Boot\unicode.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\lib\CoreCLR\CoreCLR.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\lib\CoreCLR\nethost\nethost.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\lib\CoreCLR\core\hostfxr.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\lib\CoreCLR\core\coreclr_delegates.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\Dalamud.Boot\logging.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="pch.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\Dalamud.Boot\unicode.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
#define WIN32_LEAN_AND_MEAN
|
||||
|
||||
#include <filesystem>
|
||||
#include <Windows.h>
|
||||
#include <shellapi.h>
|
||||
#include "..\Dalamud.Boot\logging.h"
|
||||
#include "..\lib\CoreCLR\CoreCLR.h"
|
||||
#include "..\lib\CoreCLR\boot.h"
|
||||
|
||||
int wmain(int argc, wchar_t** argv)
|
||||
{
|
||||
// Take care: don't redirect stderr/out here, we need to write our pid to stdout for XL to read
|
||||
//logging::start_file_logging("dalamud.injector.boot.log", false);
|
||||
logging::I("Dalamud Injector, (c) 2021 XIVLauncher Contributors");
|
||||
logging::I("Built at : " __DATE__ "@" __TIME__);
|
||||
|
||||
wchar_t _module_path[MAX_PATH];
|
||||
GetModuleFileNameW(NULL, _module_path, sizeof _module_path / 2);
|
||||
std::filesystem::path fs_module_path(_module_path);
|
||||
|
||||
std::wstring runtimeconfig_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.runtimeconfig.json").c_str());
|
||||
std::wstring module_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.dll").c_str());
|
||||
|
||||
// =========================================================================== //
|
||||
|
||||
void* entrypoint_vfn;
|
||||
const auto result = InitializeClrAndGetEntryPoint(
|
||||
GetModuleHandleW(nullptr),
|
||||
false,
|
||||
runtimeconfig_path,
|
||||
module_path,
|
||||
L"Dalamud.Injector.EntryPoint, Dalamud.Injector",
|
||||
L"Main",
|
||||
L"Dalamud.Injector.EntryPoint+MainDelegate, Dalamud.Injector",
|
||||
&entrypoint_vfn);
|
||||
|
||||
if (FAILED(result))
|
||||
return result;
|
||||
|
||||
typedef int (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(int, wchar_t**);
|
||||
custom_component_entry_point_fn entrypoint_fn = reinterpret_cast<custom_component_entry_point_fn>(entrypoint_vfn);
|
||||
|
||||
logging::I("Running Dalamud Injector...");
|
||||
const auto ret = entrypoint_fn(argc, argv);
|
||||
logging::I("Done!");
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
#pragma once
|
||||
|
|
@ -1 +0,0 @@
|
|||
MAINICON ICON "dalamud.ico"
|
||||
|
|
@ -13,12 +13,13 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Output">
|
||||
<OutputType>Library</OutputType>
|
||||
<OutputType>Exe</OutputType>
|
||||
<OutputPath>..\bin\$(Configuration)\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
|
||||
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
|
||||
<ApplicationIcon>dalamud.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Documentation">
|
||||
|
|
|
|||
|
|
@ -25,34 +25,20 @@ namespace Dalamud.Injector
|
|||
/// <summary>
|
||||
/// Entrypoint to the program.
|
||||
/// </summary>
|
||||
public sealed class EntryPoint
|
||||
public sealed class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// A delegate used during initialization of the CLR from Dalamud.Injector.Boot.
|
||||
/// </summary>
|
||||
/// <param name="argc">Count of arguments.</param>
|
||||
/// <param name="argvPtr">char** string arguments.</param>
|
||||
/// <returns>Return value (HRESULT).</returns>
|
||||
public delegate int MainDelegate(int argc, IntPtr argvPtr);
|
||||
|
||||
/// <summary>
|
||||
/// Start the Dalamud injector.
|
||||
/// </summary>
|
||||
/// <param name="argc">Count of arguments.</param>
|
||||
/// <param name="argvPtr">byte** string arguments.</param>
|
||||
/// <param name="argsArray">Command line arguments.</param>
|
||||
/// <returns>Return value (HRESULT).</returns>
|
||||
public static int Main(int argc, IntPtr argvPtr)
|
||||
public static int Main(string[] argsArray)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<string> args = new(argc);
|
||||
|
||||
unsafe
|
||||
{
|
||||
var argv = (IntPtr*)argvPtr;
|
||||
for (var i = 0; i < argc; i++)
|
||||
args.Add(Marshal.PtrToStringUni(argv[i]));
|
||||
}
|
||||
// API14 TODO: Refactor
|
||||
var args = argsArray.ToList();
|
||||
args.Insert(0, Assembly.GetExecutingAssembly().Location);
|
||||
|
||||
Init(args);
|
||||
args.Remove("-v"); // Remove "verbose" flag
|
||||
|
|
@ -305,6 +291,7 @@ namespace Dalamud.Injector
|
|||
var configurationPath = startInfo.ConfigurationPath;
|
||||
var pluginDirectory = startInfo.PluginDirectory;
|
||||
var assetDirectory = startInfo.AssetDirectory;
|
||||
var tempDirectory = startInfo.TempDirectory;
|
||||
var delayInitializeMs = startInfo.DelayInitializeMs;
|
||||
var logName = startInfo.LogName;
|
||||
var logPath = startInfo.LogPath;
|
||||
|
|
@ -335,6 +322,10 @@ namespace Dalamud.Injector
|
|||
{
|
||||
assetDirectory = args[i][key.Length..];
|
||||
}
|
||||
else if (args[i].StartsWith(key = "--dalamud-temp-directory="))
|
||||
{
|
||||
tempDirectory = args[i][key.Length..];
|
||||
}
|
||||
else if (args[i].StartsWith(key = "--dalamud-delay-initialize="))
|
||||
{
|
||||
delayInitializeMs = int.Parse(args[i][key.Length..]);
|
||||
|
|
@ -447,6 +438,7 @@ namespace Dalamud.Injector
|
|||
startInfo.ConfigurationPath = configurationPath;
|
||||
startInfo.PluginDirectory = pluginDirectory;
|
||||
startInfo.AssetDirectory = assetDirectory;
|
||||
startInfo.TempDirectory = tempDirectory;
|
||||
startInfo.Language = clientLanguage;
|
||||
startInfo.Platform = platform;
|
||||
startInfo.DelayInitializeMs = delayInitializeMs;
|
||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
108
Dalamud.Test/Rpc/DalamudUriTests.cs
Normal file
108
Dalamud.Test/Rpc/DalamudUriTests.cs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
using Dalamud.Networking.Rpc.Model;
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace Dalamud.Test.Rpc
|
||||
{
|
||||
public class DalamudUriTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("https://www.google.com/", false)]
|
||||
[InlineData("dalamud://PluginInstaller/Dalamud.FindAnything", true)]
|
||||
public void ValidatesScheme(string uri, bool valid)
|
||||
{
|
||||
Action act = () => { _ = DalamudUri.FromUri(uri); };
|
||||
|
||||
var ex = Record.Exception(act);
|
||||
if (valid)
|
||||
{
|
||||
Assert.Null(ex);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.NotNull(ex);
|
||||
Assert.IsType<ArgumentOutOfRangeException>(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("dalamud://PluginInstaller/Dalamud.FindAnything", "plugininstaller")]
|
||||
[InlineData("dalamud://Plugin/Dalamud.FindAnything/OpenWindow", "plugin")]
|
||||
[InlineData("dalamud://Test", "test")]
|
||||
public void ExtractsNamespace(string uri, string expectedNamespace)
|
||||
{
|
||||
var dalamudUri = DalamudUri.FromUri(uri);
|
||||
Assert.Equal(expectedNamespace, dalamudUri.Namespace);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("dalamud://foo/bar/baz/qux/?cow=moo", "/bar/baz/qux/")]
|
||||
[InlineData("dalamud://foo/bar/baz/qux?cow=moo", "/bar/baz/qux")]
|
||||
[InlineData("dalamud://foo/bar/baz", "/bar/baz")]
|
||||
[InlineData("dalamud://foo/bar", "/bar")]
|
||||
[InlineData("dalamud://foo/bar/", "/bar/")]
|
||||
[InlineData("dalamud://foo/", "/")]
|
||||
public void ExtractsPath(string uri, string expectedPath)
|
||||
{
|
||||
var dalamudUri = DalamudUri.FromUri(uri);
|
||||
Assert.Equal(expectedPath, dalamudUri.Path);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("dalamud://foo/bar/baz/qux/?cow=moo#frag", "/bar/baz/qux/?cow=moo#frag")]
|
||||
[InlineData("dalamud://foo/bar/baz/qux/?cow=moo", "/bar/baz/qux/?cow=moo")]
|
||||
[InlineData("dalamud://foo/bar/baz/qux?cow=moo", "/bar/baz/qux?cow=moo")]
|
||||
[InlineData("dalamud://foo/bar/baz", "/bar/baz")]
|
||||
[InlineData("dalamud://foo/bar?cow=moo", "/bar?cow=moo")]
|
||||
[InlineData("dalamud://foo/bar", "/bar")]
|
||||
[InlineData("dalamud://foo/bar/?cow=moo", "/bar/?cow=moo")]
|
||||
[InlineData("dalamud://foo/bar/", "/bar/")]
|
||||
[InlineData("dalamud://foo/?cow=moo#chicken", "/?cow=moo#chicken")]
|
||||
[InlineData("dalamud://foo/?cow=moo", "/?cow=moo")]
|
||||
[InlineData("dalamud://foo/", "/")]
|
||||
public void ExtractsData(string uri, string expectedData)
|
||||
{
|
||||
var dalamudUri = DalamudUri.FromUri(uri);
|
||||
|
||||
Assert.Equal(expectedData, dalamudUri.Data);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("dalamud://foo/bar", 0)]
|
||||
[InlineData("dalamud://foo/bar?cow=moo", 1)]
|
||||
[InlineData("dalamud://foo/bar?cow=moo&wolf=awoo", 2)]
|
||||
[InlineData("dalamud://foo/bar?cow=moo&wolf=awoo&cat", 3)]
|
||||
public void ExtractsQueryParams(string uri, int queryCount)
|
||||
{
|
||||
var dalamudUri = DalamudUri.FromUri(uri);
|
||||
Assert.Equal(queryCount, dalamudUri.QueryParams.Count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("dalamud://foo/bar/baz/qux/meh/?foo=bar", 5, true)]
|
||||
[InlineData("dalamud://foo/bar/baz/qux/meh/", 5, true)]
|
||||
[InlineData("dalamud://foo/bar/baz/qux/meh", 5)]
|
||||
[InlineData("dalamud://foo/bar/baz/qux", 4)]
|
||||
[InlineData("dalamud://foo/bar/baz", 3)]
|
||||
[InlineData("dalamud://foo/bar/", 2)]
|
||||
[InlineData("dalamud://foo/bar", 2)]
|
||||
public void ExtractsSegments(string uri, int segmentCount, bool finalSegmentEndsWithSlash = false)
|
||||
{
|
||||
var dalamudUri = DalamudUri.FromUri(uri);
|
||||
var segments = dalamudUri.Segments;
|
||||
|
||||
// First segment must always be `/`
|
||||
Assert.Equal("/", segments[0]);
|
||||
|
||||
Assert.Equal(segmentCount, segments.Length);
|
||||
|
||||
if (finalSegmentEndsWithSlash)
|
||||
{
|
||||
Assert.EndsWith("/", segments.Last());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Dalamud.sln
12
Dalamud.sln
|
|
@ -1,4 +1,4 @@
|
|||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.1.32319.34
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
|
|
@ -27,8 +27,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Boot", "Dalamud.Boo
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Injector", "Dalamud.Injector\Dalamud.Injector.csproj", "{5B832F73-5F54-4ADC-870F-D0095EF72C9A}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Injector.Boot", "Dalamud.Injector.Boot\Dalamud.Injector.Boot.vcxproj", "{8874326B-E755-4D13-90B4-59AB263A3E6B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Test", "Dalamud.Test\Dalamud.Test.csproj", "{C8004563-1806-4329-844F-0EF6274291FC}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{E15BDA6D-E881-4482-94BA-BE5527E917FF}"
|
||||
|
|
@ -49,8 +47,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator", "lib\FFX
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator.Runtime", "lib\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj", "{A6AA1C3F-9470-4922-9D3F-D4549657AB22}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Injector", "Injector", "{19775C83-7117-4A5F-AA00-18889F46A490}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{8F079208-C227-4D96-9427-2BEBE0003944}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cimgui", "external\cimgui\cimgui.vcxproj", "{8430077C-F736-4246-A052-8EA1CECE844E}"
|
||||
|
|
@ -103,10 +99,6 @@ Global
|
|||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.Build.0 = Release|x64
|
||||
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.Build.0 = Release|x64
|
||||
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
|
|
@ -188,8 +180,6 @@ Global
|
|||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A} = {19775C83-7117-4A5F-AA00-18889F46A490}
|
||||
{8874326B-E755-4D13-90B4-59AB263A3E6B} = {19775C83-7117-4A5F-AA00-18889F46A490}
|
||||
{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}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@
|
|||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
<PackageReference Include="Serilog.Sinks.File" />
|
||||
<PackageReference Include="sqlite-net-pcl" />
|
||||
<PackageReference Include="StreamJsonRpc" />
|
||||
<PackageReference Include="StyleCop.Analyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
namespace Dalamud.Game.Addon.Events;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game.Addon.Events;
|
||||
|
||||
/// <summary>
|
||||
/// AddonEventManager memory address resolver.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game.Addon.Lifecycle;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -63,47 +63,37 @@ public interface IAetheryteEntry
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class representing an aetheryte entry available to the game.
|
||||
/// This struct represents an aetheryte entry available to the game.
|
||||
/// </summary>
|
||||
internal sealed class AetheryteEntry : IAetheryteEntry
|
||||
/// <param name="data">Data read from the Aetheryte List.</param>
|
||||
internal readonly struct AetheryteEntry(TeleportInfo data) : IAetheryteEntry
|
||||
{
|
||||
private readonly TeleportInfo data;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AetheryteEntry"/> class.
|
||||
/// </summary>
|
||||
/// <param name="data">Data read from the Aetheryte List.</param>
|
||||
internal AetheryteEntry(TeleportInfo data)
|
||||
{
|
||||
this.data = data;
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public uint AetheryteId => data.AetheryteId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint AetheryteId => this.data.AetheryteId;
|
||||
public uint TerritoryId => data.TerritoryId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint TerritoryId => this.data.TerritoryId;
|
||||
public byte SubIndex => data.SubIndex;
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte SubIndex => this.data.SubIndex;
|
||||
public byte Ward => data.Ward;
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte Ward => this.data.Ward;
|
||||
public byte Plot => data.Plot;
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte Plot => this.data.Plot;
|
||||
public uint GilCost => data.GilCost;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint GilCost => this.data.GilCost;
|
||||
public bool IsFavourite => data.IsFavourite;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsFavourite => this.data.IsFavourite;
|
||||
public bool IsSharedHouse => data.IsSharedHouse;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsSharedHouse => this.data.IsSharedHouse;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsApartment => this.data.IsApartment;
|
||||
public bool IsApartment => data.IsApartment;
|
||||
|
||||
/// <inheritdoc />
|
||||
public RowRef<Lumina.Excel.Sheets.Aetheryte> AetheryteData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Aetheryte>(this.AetheryteId);
|
||||
|
|
|
|||
|
|
@ -88,10 +88,7 @@ internal sealed partial class AetheryteList
|
|||
/// <inheritdoc/>
|
||||
public IEnumerator<IAetheryteEntry> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < this.Length; i++)
|
||||
{
|
||||
yield return this[i];
|
||||
}
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -99,4 +96,30 @@ internal sealed partial class AetheryteList
|
|||
{
|
||||
return this.GetEnumerator();
|
||||
}
|
||||
|
||||
private struct Enumerator(AetheryteList aetheryteList) : IEnumerator<IAetheryteEntry>
|
||||
{
|
||||
private int index = 0;
|
||||
|
||||
public IAetheryteEntry Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => this.Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (this.index == aetheryteList.Length) return false;
|
||||
this.Current = aetheryteList[this.index];
|
||||
this.index++;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using Dalamud.IoC.Internal;
|
|||
using Dalamud.Plugin.Services;
|
||||
|
||||
using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy;
|
||||
using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember;
|
||||
using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Buddy;
|
||||
|
|
@ -23,7 +24,7 @@ namespace Dalamud.Game.ClientState.Buddy;
|
|||
#pragma warning restore SA1015
|
||||
internal sealed partial class BuddyList : IServiceType, IBuddyList
|
||||
{
|
||||
private const uint InvalidObjectID = 0xE0000000;
|
||||
private const uint InvalidEntityId = 0xE0000000;
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly PlayerState playerState = Service<PlayerState>.Get();
|
||||
|
|
@ -84,37 +85,37 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr GetCompanionBuddyMemberAddress()
|
||||
public unsafe nint GetCompanionBuddyMemberAddress()
|
||||
{
|
||||
return (IntPtr)this.BuddyListStruct->CompanionInfo.Companion;
|
||||
return (nint)this.BuddyListStruct->CompanionInfo.Companion;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr GetPetBuddyMemberAddress()
|
||||
public unsafe nint GetPetBuddyMemberAddress()
|
||||
{
|
||||
return (IntPtr)this.BuddyListStruct->PetInfo.Pet;
|
||||
return (nint)this.BuddyListStruct->PetInfo.Pet;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr GetBattleBuddyMemberAddress(int index)
|
||||
public unsafe nint GetBattleBuddyMemberAddress(int index)
|
||||
{
|
||||
if (index < 0 || index >= 3)
|
||||
return IntPtr.Zero;
|
||||
return 0;
|
||||
|
||||
return (IntPtr)Unsafe.AsPointer(ref this.BuddyListStruct->BattleBuddies[index]);
|
||||
return (nint)Unsafe.AsPointer(ref this.BuddyListStruct->BattleBuddies[index]);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IBuddyMember? CreateBuddyMemberReference(IntPtr address)
|
||||
public unsafe IBuddyMember? CreateBuddyMemberReference(nint address)
|
||||
{
|
||||
if (address == IntPtr.Zero)
|
||||
if (address == 0)
|
||||
return null;
|
||||
|
||||
if (!this.playerState.IsLoaded)
|
||||
if (this.playerState.ContentId == 0)
|
||||
return null;
|
||||
|
||||
var buddy = new BuddyMember(address);
|
||||
if (buddy.ObjectId == InvalidObjectID)
|
||||
var buddy = new BuddyMember((CSBuddyMember*)address);
|
||||
if (buddy.EntityId == InvalidEntityId)
|
||||
return null;
|
||||
|
||||
return buddy;
|
||||
|
|
@ -132,12 +133,35 @@ internal sealed partial class BuddyList
|
|||
/// <inheritdoc/>
|
||||
public IEnumerator<IBuddyMember> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < this.Length; i++)
|
||||
{
|
||||
yield return this[i];
|
||||
}
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
private struct Enumerator(BuddyList buddyList) : IEnumerator<IBuddyMember>
|
||||
{
|
||||
private int index = 0;
|
||||
|
||||
public IBuddyMember Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => this.Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (this.index == buddyList.Length) return false;
|
||||
this.Current = buddyList[this.index];
|
||||
this.index++;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,24 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
|
||||
using Lumina.Excel;
|
||||
|
||||
using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Buddy;
|
||||
|
||||
/// <summary>
|
||||
/// Interface representing represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
|
||||
/// </summary>
|
||||
public interface IBuddyMember
|
||||
public interface IBuddyMember : IEquatable<IBuddyMember>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the address of the buddy in memory.
|
||||
/// </summary>
|
||||
IntPtr Address { get; }
|
||||
nint Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the object ID of this buddy.
|
||||
|
|
@ -67,42 +71,34 @@ public interface IBuddyMember
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
|
||||
/// This struct represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
|
||||
/// </summary>
|
||||
internal unsafe class BuddyMember : IBuddyMember
|
||||
/// <param name="ptr">A pointer to the BuddyMember.</param>
|
||||
internal readonly unsafe struct BuddyMember(CSBuddyMember* ptr) : IBuddyMember
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BuddyMember"/> class.
|
||||
/// </summary>
|
||||
/// <param name="address">Buddy address.</param>
|
||||
internal BuddyMember(IntPtr address)
|
||||
{
|
||||
this.Address = address;
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public nint Address => (nint)ptr;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IntPtr Address { get; }
|
||||
public uint ObjectId => this.EntityId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint ObjectId => this.Struct->EntityId;
|
||||
public uint EntityId => ptr->EntityId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint EntityId => this.Struct->EntityId;
|
||||
public IGameObject? GameObject => this.objectTable.SearchById(this.EntityId);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IGameObject? GameObject => this.objectTable.SearchById(this.ObjectId);
|
||||
public uint CurrentHP => ptr->CurrentHealth;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint CurrentHP => this.Struct->CurrentHealth;
|
||||
public uint MaxHP => ptr->MaxHealth;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint MaxHP => this.Struct->MaxHealth;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint DataID => this.Struct->DataId;
|
||||
public uint DataID => ptr->DataId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public RowRef<Lumina.Excel.Sheets.Mount> MountData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Mount>(this.DataID);
|
||||
|
|
@ -113,5 +109,25 @@ internal unsafe class BuddyMember : IBuddyMember
|
|||
/// <inheritdoc />
|
||||
public RowRef<Lumina.Excel.Sheets.DawnGrowMember> TrustData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.DawnGrowMember>(this.DataID);
|
||||
|
||||
private FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember*)this.Address;
|
||||
public static bool operator ==(BuddyMember x, BuddyMember y) => x.Equals(y);
|
||||
|
||||
public static bool operator !=(BuddyMember x, BuddyMember y) => !(x == y);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(IBuddyMember? other)
|
||||
{
|
||||
return this.EntityId == other.EntityId;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is BuddyMember fate && this.Equals(fate);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return this.EntityId.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game.ClientState;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
|
||||
using Dalamud.Data;
|
||||
|
|
@ -7,10 +8,12 @@ using Dalamud.Memory;
|
|||
|
||||
using Lumina.Excel;
|
||||
|
||||
using CSFateContext = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Fates;
|
||||
|
||||
/// <summary>
|
||||
/// Interface representing an fate entry that can be seen in the current area.
|
||||
/// Interface representing a fate entry that can be seen in the current area.
|
||||
/// </summary>
|
||||
public interface IFate : IEquatable<IFate>
|
||||
{
|
||||
|
|
@ -112,129 +115,96 @@ public interface IFate : IEquatable<IFate>
|
|||
/// <summary>
|
||||
/// Gets the address of this Fate in memory.
|
||||
/// </summary>
|
||||
IntPtr Address { get; }
|
||||
nint Address { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class represents an FFXIV Fate.
|
||||
/// This struct represents a Fate.
|
||||
/// </summary>
|
||||
internal unsafe partial class Fate
|
||||
/// <param name="ptr">A pointer to the FateContext.</param>
|
||||
internal readonly unsafe struct Fate(CSFateContext* ptr) : IFate
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Fate"/> class.
|
||||
/// </summary>
|
||||
/// <param name="address">The address of this fate in memory.</param>
|
||||
internal Fate(IntPtr address)
|
||||
{
|
||||
this.Address = address;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IntPtr Address { get; }
|
||||
|
||||
private FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext*)this.Address;
|
||||
|
||||
public static bool operator ==(Fate fate1, Fate fate2)
|
||||
{
|
||||
if (fate1 is null || fate2 is null)
|
||||
return Equals(fate1, fate2);
|
||||
|
||||
return fate1.Equals(fate2);
|
||||
}
|
||||
|
||||
public static bool operator !=(Fate fate1, Fate fate2) => !(fate1 == fate2);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this Fate is still valid in memory.
|
||||
/// </summary>
|
||||
/// <param name="fate">The fate to check.</param>
|
||||
/// <returns>True or false.</returns>
|
||||
public static bool IsValid(Fate fate)
|
||||
{
|
||||
if (fate == null)
|
||||
return false;
|
||||
|
||||
var playerState = Service<PlayerState>.Get();
|
||||
return playerState.IsLoaded == true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this actor is still valid in memory.
|
||||
/// </summary>
|
||||
/// <returns>True or false.</returns>
|
||||
public bool IsValid() => IsValid(this);
|
||||
public nint Address => (nint)ptr;
|
||||
|
||||
/// <inheritdoc/>
|
||||
bool IEquatable<IFate>.Equals(IFate other) => this.FateId == other?.FateId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object obj) => ((IEquatable<IFate>)this).Equals(obj as IFate);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => this.FateId.GetHashCode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class represents an FFXIV Fate.
|
||||
/// </summary>
|
||||
internal unsafe partial class Fate : IFate
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public ushort FateId => this.Struct->FateId;
|
||||
public ushort FateId => ptr->FateId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public RowRef<Lumina.Excel.Sheets.Fate> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Fate>(this.FateId);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int StartTimeEpoch => this.Struct->StartTimeEpoch;
|
||||
public int StartTimeEpoch => ptr->StartTimeEpoch;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public short Duration => this.Struct->Duration;
|
||||
public short Duration => ptr->Duration;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public long TimeRemaining => this.StartTimeEpoch + this.Duration - DateTimeOffset.Now.ToUnixTimeSeconds();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SeString Name => MemoryHelper.ReadSeString(&this.Struct->Name);
|
||||
public SeString Name => MemoryHelper.ReadSeString(&ptr->Name);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SeString Description => MemoryHelper.ReadSeString(&this.Struct->Description);
|
||||
public SeString Description => MemoryHelper.ReadSeString(&ptr->Description);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SeString Objective => MemoryHelper.ReadSeString(&this.Struct->Objective);
|
||||
public SeString Objective => MemoryHelper.ReadSeString(&ptr->Objective);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FateState State => (FateState)this.Struct->State;
|
||||
public FateState State => (FateState)ptr->State;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte HandInCount => this.Struct->HandInCount;
|
||||
public byte HandInCount => ptr->HandInCount;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte Progress => this.Struct->Progress;
|
||||
public byte Progress => ptr->Progress;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool HasBonus => this.Struct->IsBonus;
|
||||
public bool HasBonus => ptr->IsBonus;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint IconId => this.Struct->IconId;
|
||||
public uint IconId => ptr->IconId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte Level => this.Struct->Level;
|
||||
public byte Level => ptr->Level;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte MaxLevel => this.Struct->MaxLevel;
|
||||
public byte MaxLevel => ptr->MaxLevel;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Vector3 Position => this.Struct->Location;
|
||||
public Vector3 Position => ptr->Location;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public float Radius => this.Struct->Radius;
|
||||
public float Radius => ptr->Radius;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint MapIconId => this.Struct->MapIconId;
|
||||
public uint MapIconId => ptr->MapIconId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the territory this <see cref="Fate"/> is located in.
|
||||
/// </summary>
|
||||
public RowRef<Lumina.Excel.Sheets.TerritoryType> TerritoryType => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(this.Struct->MapMarkers[0].MapMarkerData.TerritoryTypeId);
|
||||
public RowRef<Lumina.Excel.Sheets.TerritoryType> TerritoryType => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(ptr->MapMarkers[0].MapMarkerData.TerritoryTypeId);
|
||||
|
||||
public static bool operator ==(Fate x, Fate y) => x.Equals(y);
|
||||
|
||||
public static bool operator !=(Fate x, Fate y) => !(x == y);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(IFate? other)
|
||||
{
|
||||
return this.FateId == other.FateId;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is Fate fate && this.Equals(fate);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return this.FateId.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using Dalamud.IoC;
|
|||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using CSFateContext = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext;
|
||||
using CSFateManager = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Fates;
|
||||
|
|
@ -26,7 +27,7 @@ internal sealed partial class FateTable : IServiceType, IFateTable
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr Address => (nint)CSFateManager.Instance();
|
||||
public unsafe nint Address => (nint)CSFateManager.Instance();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe int Length
|
||||
|
|
@ -69,29 +70,29 @@ internal sealed partial class FateTable : IServiceType, IFateTable
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr GetFateAddress(int index)
|
||||
public unsafe nint GetFateAddress(int index)
|
||||
{
|
||||
if (index >= this.Length)
|
||||
return IntPtr.Zero;
|
||||
return 0;
|
||||
|
||||
var fateManager = CSFateManager.Instance();
|
||||
if (fateManager == null)
|
||||
return IntPtr.Zero;
|
||||
return 0;
|
||||
|
||||
return (IntPtr)fateManager->Fates[index].Value;
|
||||
return (nint)fateManager->Fates[index].Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IFate? CreateFateReference(IntPtr offset)
|
||||
public unsafe IFate? CreateFateReference(IntPtr address)
|
||||
{
|
||||
if (offset == IntPtr.Zero)
|
||||
if (address == 0)
|
||||
return null;
|
||||
|
||||
var playerState = Service<PlayerState>.Get();
|
||||
if (!playerState.IsLoaded)
|
||||
var clientState = Service<ClientState>.Get();
|
||||
if (clientState.LocalContentId == 0)
|
||||
return null;
|
||||
|
||||
return new Fate(offset);
|
||||
return new Fate((CSFateContext*)address);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,12 +107,35 @@ internal sealed partial class FateTable
|
|||
/// <inheritdoc/>
|
||||
public IEnumerator<IFate> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < this.Length; i++)
|
||||
{
|
||||
yield return this[i];
|
||||
}
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
private struct Enumerator(FateTable fateTable) : IEnumerator<IFate>
|
||||
{
|
||||
private int index = 0;
|
||||
|
||||
public IFate Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => this.Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (this.index == fateTable.Length) return false;
|
||||
this.Current = fateTable[this.index];
|
||||
this.index++;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ using Dalamud.Utility;
|
|||
|
||||
using FFXIVClientStructs.Interop;
|
||||
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
|
||||
using CSGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
||||
using CSGameObjectManager = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObjectManager;
|
||||
|
||||
|
|
@ -37,8 +35,6 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
|
|||
|
||||
private readonly CachedEntry[] cachedObjectTable;
|
||||
|
||||
private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4];
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private unsafe ObjectTable()
|
||||
{
|
||||
|
|
@ -48,9 +44,6 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
|
|||
this.cachedObjectTable = new CachedEntry[objectTableLength];
|
||||
for (var i = 0; i < this.cachedObjectTable.Length; i++)
|
||||
this.cachedObjectTable[i] = new(nativeObjectTable.GetPointer(i));
|
||||
|
||||
for (var i = 0; i < this.frameworkThreadEnumerators.Length; i++)
|
||||
this.frameworkThreadEnumerators[i] = new(this, i);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -243,30 +236,14 @@ internal sealed partial class ObjectTable
|
|||
public IEnumerator<IGameObject> GetEnumerator()
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
|
||||
// If we're on the framework thread, see if there's an already allocated enumerator available for use.
|
||||
foreach (ref var x in this.frameworkThreadEnumerators.AsSpan())
|
||||
{
|
||||
if (x is not null)
|
||||
{
|
||||
var t = x;
|
||||
x = null;
|
||||
t.Reset();
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
// No reusable enumerator is available; allocate a new temporary one.
|
||||
return new Enumerator(this, -1);
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
private sealed class Enumerator(ObjectTable owner, int slotId) : IEnumerator<IGameObject>, IResettable
|
||||
private struct Enumerator(ObjectTable owner) : IEnumerator<IGameObject>
|
||||
{
|
||||
private ObjectTable? owner = owner;
|
||||
|
||||
private int index = -1;
|
||||
|
||||
public IGameObject Current { get; private set; } = null!;
|
||||
|
|
@ -278,7 +255,7 @@ internal sealed partial class ObjectTable
|
|||
if (this.index == objectTableLength)
|
||||
return false;
|
||||
|
||||
var cache = this.owner!.cachedObjectTable.AsSpan();
|
||||
var cache = owner.cachedObjectTable.AsSpan();
|
||||
for (this.index++; this.index < objectTableLength; this.index++)
|
||||
{
|
||||
if (cache[this.index].Update() is { } ao)
|
||||
|
|
@ -295,17 +272,6 @@ internal sealed partial class ObjectTable
|
|||
|
||||
public void Dispose()
|
||||
{
|
||||
if (this.owner is not { } o)
|
||||
return;
|
||||
|
||||
if (slotId != -1)
|
||||
o.frameworkThreadEnumerators[slotId] = this;
|
||||
}
|
||||
|
||||
public bool TryReset()
|
||||
{
|
||||
this.Reset();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using Dalamud.IoC.Internal;
|
|||
using Dalamud.Plugin.Services;
|
||||
|
||||
using CSGroupManager = FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager;
|
||||
using CSPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Party;
|
||||
|
||||
|
|
@ -43,20 +44,20 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
|
|||
public bool IsAlliance => this.GroupManagerStruct->MainGroup.AllianceFlags > 0;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr GroupManagerAddress => (nint)CSGroupManager.Instance();
|
||||
public unsafe nint GroupManagerAddress => (nint)CSGroupManager.Instance();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr GroupListAddress => (IntPtr)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]);
|
||||
public nint GroupListAddress => (nint)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr AllianceListAddress => (IntPtr)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]);
|
||||
public nint AllianceListAddress => (nint)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public long PartyId => this.GroupManagerStruct->MainGroup.PartyId;
|
||||
|
||||
private static int PartyMemberSize { get; } = Marshal.SizeOf<FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember>();
|
||||
private static int PartyMemberSize { get; } = Marshal.SizeOf<CSPartyMember>();
|
||||
|
||||
private FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager* GroupManagerStruct => (FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager*)this.GroupManagerAddress;
|
||||
private CSGroupManager* GroupManagerStruct => (CSGroupManager*)this.GroupManagerAddress;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IPartyMember? this[int index]
|
||||
|
|
@ -81,39 +82,45 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr GetPartyMemberAddress(int index)
|
||||
public nint GetPartyMemberAddress(int index)
|
||||
{
|
||||
if (index < 0 || index >= GroupLength)
|
||||
return IntPtr.Zero;
|
||||
return 0;
|
||||
|
||||
return this.GroupListAddress + (index * PartyMemberSize);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IPartyMember? CreatePartyMemberReference(IntPtr address)
|
||||
public IPartyMember? CreatePartyMemberReference(nint address)
|
||||
{
|
||||
if (address == IntPtr.Zero || !this.playerState.IsLoaded)
|
||||
if (this.playerState.ContentId == 0)
|
||||
return null;
|
||||
|
||||
return new PartyMember(address);
|
||||
if (address == 0)
|
||||
return null;
|
||||
|
||||
return new PartyMember((CSPartyMember*)address);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr GetAllianceMemberAddress(int index)
|
||||
public nint GetAllianceMemberAddress(int index)
|
||||
{
|
||||
if (index < 0 || index >= AllianceLength)
|
||||
return IntPtr.Zero;
|
||||
return 0;
|
||||
|
||||
return this.AllianceListAddress + (index * PartyMemberSize);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IPartyMember? CreateAllianceMemberReference(IntPtr address)
|
||||
public IPartyMember? CreateAllianceMemberReference(nint address)
|
||||
{
|
||||
if (address == IntPtr.Zero || !this.playerState.IsLoaded)
|
||||
if (this.playerState.ContentId == 0)
|
||||
return null;
|
||||
|
||||
return new PartyMember(address);
|
||||
if (address == 0)
|
||||
return null;
|
||||
|
||||
return new PartyMember((CSPartyMember*)address);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -128,18 +135,44 @@ internal sealed partial class PartyList
|
|||
/// <inheritdoc/>
|
||||
public IEnumerator<IPartyMember> GetEnumerator()
|
||||
{
|
||||
// Normally using Length results in a recursion crash, however we know the party size via ptr.
|
||||
for (var i = 0; i < this.Length; i++)
|
||||
{
|
||||
var member = this[i];
|
||||
|
||||
if (member == null)
|
||||
break;
|
||||
|
||||
yield return member;
|
||||
}
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
private struct Enumerator(PartyList partyList) : IEnumerator<IPartyMember>
|
||||
{
|
||||
private int index = 0;
|
||||
|
||||
public IPartyMember Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => this.Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (this.index == partyList.Length) return false;
|
||||
|
||||
for (; this.index < partyList.Length; this.index++)
|
||||
{
|
||||
var partyMember = partyList[this.index];
|
||||
if (partyMember != null)
|
||||
{
|
||||
this.Current = partyMember;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,27 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Game.ClientState.Statuses;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Memory;
|
||||
|
||||
using Lumina.Excel;
|
||||
|
||||
using CSPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Party;
|
||||
|
||||
/// <summary>
|
||||
/// Interface representing a party member.
|
||||
/// </summary>
|
||||
public interface IPartyMember
|
||||
public interface IPartyMember : IEquatable<IPartyMember>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the address of this party member in memory.
|
||||
/// </summary>
|
||||
IntPtr Address { get; }
|
||||
nint Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of buffs or debuffs applied to this party member.
|
||||
|
|
@ -108,69 +109,81 @@ public interface IPartyMember
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class represents a party member in the group manager.
|
||||
/// This struct represents a party member in the group manager.
|
||||
/// </summary>
|
||||
internal unsafe class PartyMember : IPartyMember
|
||||
/// <param name="ptr">A pointer to the PartyMember.</param>
|
||||
internal unsafe readonly struct PartyMember(CSPartyMember* ptr) : IPartyMember
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PartyMember"/> class.
|
||||
/// </summary>
|
||||
/// <param name="address">Address of the party member.</param>
|
||||
internal PartyMember(IntPtr address)
|
||||
{
|
||||
this.Address = address;
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public nint Address => (nint)ptr;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr Address { get; }
|
||||
public StatusList Statuses => new(&ptr->StatusManager);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public StatusList Statuses => new(&this.Struct->StatusManager);
|
||||
public Vector3 Position => ptr->Position;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Vector3 Position => this.Struct->Position;
|
||||
public long ContentId => (long)ptr->ContentId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public long ContentId => (long)this.Struct->ContentId;
|
||||
public uint ObjectId => ptr->EntityId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint ObjectId => this.Struct->EntityId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint EntityId => this.Struct->EntityId;
|
||||
public uint EntityId => ptr->EntityId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IGameObject? GameObject => Service<ObjectTable>.Get().SearchById(this.EntityId);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint CurrentHP => this.Struct->CurrentHP;
|
||||
public uint CurrentHP => ptr->CurrentHP;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint MaxHP => this.Struct->MaxHP;
|
||||
public uint MaxHP => ptr->MaxHP;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ushort CurrentMP => this.Struct->CurrentMP;
|
||||
public ushort CurrentMP => ptr->CurrentMP;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ushort MaxMP => this.Struct->MaxMP;
|
||||
public ushort MaxMP => ptr->MaxMP;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public RowRef<Lumina.Excel.Sheets.TerritoryType> Territory => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(this.Struct->TerritoryType);
|
||||
public RowRef<Lumina.Excel.Sheets.TerritoryType> Territory => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(ptr->TerritoryType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public RowRef<Lumina.Excel.Sheets.World> World => LuminaUtils.CreateRef<Lumina.Excel.Sheets.World>(this.Struct->HomeWorld);
|
||||
public RowRef<Lumina.Excel.Sheets.World> World => LuminaUtils.CreateRef<Lumina.Excel.Sheets.World>(ptr->HomeWorld);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SeString Name => SeString.Parse(this.Struct->Name);
|
||||
public SeString Name => SeString.Parse(ptr->Name);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte Sex => this.Struct->Sex;
|
||||
public byte Sex => ptr->Sex;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public RowRef<Lumina.Excel.Sheets.ClassJob> ClassJob => LuminaUtils.CreateRef<Lumina.Excel.Sheets.ClassJob>(this.Struct->ClassJob);
|
||||
public RowRef<Lumina.Excel.Sheets.ClassJob> ClassJob => LuminaUtils.CreateRef<Lumina.Excel.Sheets.ClassJob>(ptr->ClassJob);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte Level => this.Struct->Level;
|
||||
public byte Level => ptr->Level;
|
||||
|
||||
private FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember*)this.Address;
|
||||
public static bool operator ==(PartyMember x, PartyMember y) => x.Equals(y);
|
||||
|
||||
public static bool operator !=(PartyMember x, PartyMember y) => !(x == y);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(IPartyMember? other)
|
||||
{
|
||||
return this.EntityId == other.EntityId;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is PartyMember fate && this.Equals(fate);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return this.EntityId.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,61 +1,49 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
|
||||
using Lumina.Excel;
|
||||
|
||||
using CSStatus = FFXIVClientStructs.FFXIV.Client.Game.Status;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Statuses;
|
||||
|
||||
/// <summary>
|
||||
/// This class represents a status effect an actor is afflicted by.
|
||||
/// Interface representing a status.
|
||||
/// </summary>
|
||||
public unsafe class Status
|
||||
public interface IStatus : IEquatable<IStatus>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Status"/> class.
|
||||
/// </summary>
|
||||
/// <param name="address">Status address.</param>
|
||||
internal Status(IntPtr address)
|
||||
{
|
||||
this.Address = address;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the status in memory.
|
||||
/// </summary>
|
||||
public IntPtr Address { get; }
|
||||
nint Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the status ID of this status.
|
||||
/// </summary>
|
||||
public uint StatusId => this.Struct->StatusId;
|
||||
uint StatusId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the GameData associated with this status.
|
||||
/// </summary>
|
||||
public RowRef<Lumina.Excel.Sheets.Status> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Status>(this.Struct->StatusId);
|
||||
RowRef<Lumina.Excel.Sheets.Status> GameData { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter value of the status.
|
||||
/// </summary>
|
||||
public ushort Param => this.Struct->Param;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stack count of this status.
|
||||
/// Only valid if this is a non-food status.
|
||||
/// </summary>
|
||||
[Obsolete($"Replaced with {nameof(Param)}", true)]
|
||||
public byte StackCount => (byte)this.Struct->Param;
|
||||
ushort Param { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the time remaining of this status.
|
||||
/// </summary>
|
||||
public float RemainingTime => this.Struct->RemainingTime;
|
||||
float RemainingTime { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source ID of this status.
|
||||
/// </summary>
|
||||
public uint SourceId => this.Struct->SourceObject.ObjectId;
|
||||
uint SourceId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source actor associated with this status.
|
||||
|
|
@ -63,7 +51,55 @@ public unsafe class Status
|
|||
/// <remarks>
|
||||
/// This iterates the actor table, it should be used with care.
|
||||
/// </remarks>
|
||||
IGameObject? SourceObject { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This struct represents a status effect an actor is afflicted by.
|
||||
/// </summary>
|
||||
/// <param name="ptr">A pointer to the Status.</param>
|
||||
internal unsafe readonly struct Status(CSStatus* ptr) : IStatus
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public nint Address => (nint)ptr;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint StatusId => ptr->StatusId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public RowRef<Lumina.Excel.Sheets.Status> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Status>(ptr->StatusId);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ushort Param => ptr->Param;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public float RemainingTime => ptr->RemainingTime;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint SourceId => ptr->SourceObject.ObjectId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IGameObject? SourceObject => Service<ObjectTable>.Get().SearchById(this.SourceId);
|
||||
|
||||
private FFXIVClientStructs.FFXIV.Client.Game.Status* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Status*)this.Address;
|
||||
public static bool operator ==(Status x, Status y) => x.Equals(y);
|
||||
|
||||
public static bool operator !=(Status x, Status y) => !(x == y);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(IStatus? other)
|
||||
{
|
||||
return this.StatusId == other.StatusId && this.SourceId == other.SourceId && this.Param == other.Param && this.RemainingTime == other.RemainingTime;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is Status fate && this.Equals(fate);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this.StatusId, this.SourceId, this.Param, this.RemainingTime);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Game.Player;
|
||||
using CSStatus = FFXIVClientStructs.FFXIV.Client.Game.Status;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Statuses;
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ public sealed unsafe partial class StatusList
|
|||
/// Initializes a new instance of the <see cref="StatusList"/> class.
|
||||
/// </summary>
|
||||
/// <param name="address">Address of the status list.</param>
|
||||
internal StatusList(IntPtr address)
|
||||
internal StatusList(nint address)
|
||||
{
|
||||
this.Address = address;
|
||||
}
|
||||
|
|
@ -26,14 +26,14 @@ public sealed unsafe partial class StatusList
|
|||
/// </summary>
|
||||
/// <param name="pointer">Pointer to the status list.</param>
|
||||
internal unsafe StatusList(void* pointer)
|
||||
: this((IntPtr)pointer)
|
||||
: this((nint)pointer)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the status list in memory.
|
||||
/// </summary>
|
||||
public IntPtr Address { get; }
|
||||
public nint Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the amount of status effect slots the actor has.
|
||||
|
|
@ -49,7 +49,7 @@ public sealed unsafe partial class StatusList
|
|||
/// </summary>
|
||||
/// <param name="index">Status Index.</param>
|
||||
/// <returns>The status at the specified index.</returns>
|
||||
public Status? this[int index]
|
||||
public IStatus? this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
|
|
@ -66,7 +66,7 @@ public sealed unsafe partial class StatusList
|
|||
/// </summary>
|
||||
/// <param name="address">The address of the status list in memory.</param>
|
||||
/// <returns>The status object containing the requested data.</returns>
|
||||
public static StatusList? CreateStatusListReference(IntPtr address)
|
||||
public static StatusList? CreateStatusListReference(nint address)
|
||||
{
|
||||
if (address == IntPtr.Zero)
|
||||
return null;
|
||||
|
|
@ -74,8 +74,12 @@ public sealed unsafe partial class StatusList
|
|||
// The use case for CreateStatusListReference and CreateStatusReference to be static is so
|
||||
// fake status lists can be generated. Since they aren't exposed as services, it's either
|
||||
// here or somewhere else.
|
||||
var playerState = Service<PlayerState>.Get();
|
||||
if (!playerState.IsLoaded)
|
||||
var clientState = Service<ClientState>.Get();
|
||||
|
||||
if (clientState.LocalContentId == 0)
|
||||
return null;
|
||||
|
||||
if (address == 0)
|
||||
return null;
|
||||
|
||||
return new StatusList(address);
|
||||
|
|
@ -86,16 +90,15 @@ public sealed unsafe partial class StatusList
|
|||
/// </summary>
|
||||
/// <param name="address">The address of the status effect in memory.</param>
|
||||
/// <returns>The status object containing the requested data.</returns>
|
||||
public static Status? CreateStatusReference(IntPtr address)
|
||||
public static IStatus? CreateStatusReference(nint address)
|
||||
{
|
||||
if (address == IntPtr.Zero)
|
||||
return null;
|
||||
|
||||
var playerState = Service<PlayerState>.Get();
|
||||
if (!playerState.IsLoaded)
|
||||
if (address == 0)
|
||||
return null;
|
||||
|
||||
return new Status(address);
|
||||
return new Status((CSStatus*)address);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -103,22 +106,22 @@ public sealed unsafe partial class StatusList
|
|||
/// </summary>
|
||||
/// <param name="index">The index of the status.</param>
|
||||
/// <returns>The memory address of the status.</returns>
|
||||
public IntPtr GetStatusAddress(int index)
|
||||
public nint GetStatusAddress(int index)
|
||||
{
|
||||
if (index < 0 || index >= this.Length)
|
||||
return IntPtr.Zero;
|
||||
return 0;
|
||||
|
||||
return (IntPtr)Unsafe.AsPointer(ref this.Struct->Status[index]);
|
||||
return (nint)Unsafe.AsPointer(ref this.Struct->Status[index]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This collection represents the status effects an actor is afflicted by.
|
||||
/// </summary>
|
||||
public sealed partial class StatusList : IReadOnlyCollection<Status>, ICollection
|
||||
public sealed partial class StatusList : IReadOnlyCollection<IStatus>, ICollection
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
int IReadOnlyCollection<Status>.Count => this.Length;
|
||||
int IReadOnlyCollection<IStatus>.Count => this.Length;
|
||||
|
||||
/// <inheritdoc/>
|
||||
int ICollection.Count => this.Length;
|
||||
|
|
@ -130,17 +133,9 @@ public sealed partial class StatusList : IReadOnlyCollection<Status>, ICollectio
|
|||
object ICollection.SyncRoot => this;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerator<Status> GetEnumerator()
|
||||
public IEnumerator<IStatus> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < this.Length; i++)
|
||||
{
|
||||
var status = this[i];
|
||||
|
||||
if (status == null || status.StatusId == 0)
|
||||
continue;
|
||||
|
||||
yield return status;
|
||||
}
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -155,4 +150,39 @@ public sealed partial class StatusList : IReadOnlyCollection<Status>, ICollectio
|
|||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
private struct Enumerator(StatusList statusList) : IEnumerator<IStatus>
|
||||
{
|
||||
private int index = 0;
|
||||
|
||||
public IStatus Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => this.Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (this.index == statusList.Length) return false;
|
||||
|
||||
for (; this.index < statusList.Length; this.index++)
|
||||
{
|
||||
var status = statusList[this.index];
|
||||
if (status != null && status.StatusId != 0)
|
||||
{
|
||||
this.Current = status;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Structs;
|
||||
|
||||
/// <summary>
|
||||
/// Native memory representation of a FFXIV status effect.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct StatusEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// The effect ID.
|
||||
/// </summary>
|
||||
public short EffectId;
|
||||
|
||||
/// <summary>
|
||||
/// How many stacks are present.
|
||||
/// </summary>
|
||||
public byte StackCount;
|
||||
|
||||
/// <summary>
|
||||
/// Additional parameters.
|
||||
/// </summary>
|
||||
public byte Param;
|
||||
|
||||
/// <summary>
|
||||
/// The duration remaining.
|
||||
/// </summary>
|
||||
public float Duration;
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the actor that caused this effect.
|
||||
/// </summary>
|
||||
public int OwnerId;
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
namespace Dalamud.Game.Config;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// Game config system address resolver.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game.DutyState;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game.Gui;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game.Gui.NamePlate;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game.Network;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
namespace Dalamud.Game.Network.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game.Network.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Internal address resolver for the network handlers.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ using System.Runtime.CompilerServices;
|
|||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using Iced.Intel;
|
||||
using Newtonsoft.Json;
|
||||
using Serilog;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
using System.Diagnostics;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game;
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ using System.Diagnostics.CodeAnalysis;
|
|||
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1116:SplitParametersMustStartOnLineAfterDeclaration", Justification = "Reviewed.")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "This would be nice, but a big refactor")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:FileNameMustMatchTypeName", Justification = "I don't like this one so much")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1108:BlockStatementsMustNotContainEmbeddedComments", Justification = "I like having comments in blocks")]
|
||||
|
||||
// ImRAII stuff
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed.", Scope = "namespaceanddescendants", Target = "Dalamud.Interface.Utility.Raii")]
|
||||
|
|
|
|||
|
|
@ -299,11 +299,12 @@ internal sealed partial class Win32InputHandler
|
|||
|
||||
private static void ViewportFlagsToWin32Styles(ImGuiViewportFlags flags, out int style, out int exStyle)
|
||||
{
|
||||
style = (int)(flags.HasFlag(ImGuiViewportFlags.NoDecoration) ? WS.WS_POPUP : WS.WS_OVERLAPPEDWINDOW);
|
||||
exStyle =
|
||||
(int)(flags.HasFlag(ImGuiViewportFlags.NoTaskBarIcon) ? WS.WS_EX_TOOLWINDOW : (uint)WS.WS_EX_APPWINDOW);
|
||||
style = (flags & ImGuiViewportFlags.NoDecoration) != 0 ? unchecked((int)WS.WS_POPUP) : WS.WS_OVERLAPPEDWINDOW;
|
||||
exStyle = (flags & ImGuiViewportFlags.NoTaskBarIcon) != 0 ? WS.WS_EX_TOOLWINDOW : WS.WS_EX_APPWINDOW;
|
||||
exStyle |= WS.WS_EX_NOREDIRECTIONBITMAP;
|
||||
if (flags.HasFlag(ImGuiViewportFlags.TopMost))
|
||||
if ((flags & ImGuiViewportFlags.TopMost) != 0)
|
||||
exStyle |= WS.WS_EX_TOPMOST;
|
||||
if ((flags & ImGuiViewportFlags.NoInputs) != 0)
|
||||
exStyle |= WS.WS_EX_TRANSPARENT | WS.WS_EX_LAYERED;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using System.Text;
|
|||
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Memory;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using Serilog;
|
||||
|
||||
|
|
@ -34,11 +35,12 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
private readonly HCURSOR[] cursors;
|
||||
|
||||
private readonly WndProcDelegate wndProcDelegate;
|
||||
private readonly bool[] imguiMouseIsDown;
|
||||
private readonly nint platformNamePtr;
|
||||
|
||||
private ViewportHandler viewportHandler;
|
||||
|
||||
private int mouseButtonsDown;
|
||||
private bool mouseTracked;
|
||||
private long lastTime;
|
||||
|
||||
private nint iniPathPtr;
|
||||
|
|
@ -64,7 +66,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors |
|
||||
ImGuiBackendFlags.HasSetMousePos |
|
||||
ImGuiBackendFlags.RendererHasViewports |
|
||||
ImGuiBackendFlags.PlatformHasViewports;
|
||||
ImGuiBackendFlags.PlatformHasViewports |
|
||||
ImGuiBackendFlags.HasMouseHoveredViewport;
|
||||
|
||||
this.platformNamePtr = Marshal.StringToHGlobalAnsi("imgui_impl_win32_c#");
|
||||
io.Handle->BackendPlatformName = (byte*)this.platformNamePtr;
|
||||
|
|
@ -74,8 +77,6 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable))
|
||||
this.viewportHandler = new(this);
|
||||
|
||||
this.imguiMouseIsDown = new bool[5];
|
||||
|
||||
this.cursors = new HCURSOR[9];
|
||||
this.cursors[(int)ImGuiMouseCursor.Arrow] = LoadCursorW(default, IDC.IDC_ARROW);
|
||||
this.cursors[(int)ImGuiMouseCursor.TextInput] = LoadCursorW(default, IDC.IDC_IBEAM);
|
||||
|
|
@ -95,8 +96,6 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
|
||||
private delegate LRESULT WndProcDelegate(HWND hWnd, uint uMsg, WPARAM wparam, LPARAM lparam);
|
||||
|
||||
private delegate BOOL MonitorEnumProcDelegate(HMONITOR monitor, HDC hdc, RECT* rect, LPARAM lparam);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool UpdateCursor { get; set; } = true;
|
||||
|
||||
|
|
@ -155,6 +154,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
public void NewFrame(int targetWidth, int targetHeight)
|
||||
{
|
||||
var io = ImGui.GetIO();
|
||||
var focusedWindow = GetForegroundWindow();
|
||||
|
||||
io.DisplaySize.X = targetWidth;
|
||||
io.DisplaySize.Y = targetHeight;
|
||||
|
|
@ -168,9 +168,9 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
|
||||
this.viewportHandler.UpdateMonitors();
|
||||
|
||||
this.UpdateMousePos();
|
||||
this.UpdateMouseData(focusedWindow);
|
||||
|
||||
this.ProcessKeyEventsWorkarounds();
|
||||
this.ProcessKeyEventsWorkarounds(focusedWindow);
|
||||
|
||||
// TODO: need to figure out some way to unify all this
|
||||
// The bottom case works(?) if the caller hooks SetCursor, but otherwise causes fps issues
|
||||
|
|
@ -224,6 +224,40 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
|
||||
switch (msg)
|
||||
{
|
||||
case WM.WM_MOUSEMOVE:
|
||||
{
|
||||
if (!this.mouseTracked)
|
||||
{
|
||||
var tme = new TRACKMOUSEEVENT
|
||||
{
|
||||
cbSize = (uint)sizeof(TRACKMOUSEEVENT),
|
||||
dwFlags = TME.TME_LEAVE,
|
||||
hwndTrack = hWndCurrent,
|
||||
};
|
||||
this.mouseTracked = TrackMouseEvent(&tme);
|
||||
}
|
||||
|
||||
var mousePos = new POINT(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
|
||||
if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) != 0)
|
||||
ClientToScreen(hWndCurrent, &mousePos);
|
||||
io.AddMousePosEvent(mousePos.x, mousePos.y);
|
||||
break;
|
||||
}
|
||||
|
||||
case WM.WM_MOUSELEAVE:
|
||||
{
|
||||
this.mouseTracked = false;
|
||||
var mouseScreenPos = new POINT(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
|
||||
ClientToScreen(hWndCurrent, &mouseScreenPos);
|
||||
if (this.ViewportFromPoint(mouseScreenPos).IsNull)
|
||||
{
|
||||
var fltMax = ImGuiNative.GETFLTMAX();
|
||||
io.AddMousePosEvent(-fltMax, -fltMax);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case WM.WM_LBUTTONDOWN:
|
||||
case WM.WM_LBUTTONDBLCLK:
|
||||
case WM.WM_RBUTTONDOWN:
|
||||
|
|
@ -236,11 +270,10 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
var button = GetButton(msg, wParam);
|
||||
if (io.WantCaptureMouse)
|
||||
{
|
||||
if (!ImGui.IsAnyMouseDown() && GetCapture() == nint.Zero)
|
||||
if (this.mouseButtonsDown == 0 && GetCapture() == nint.Zero)
|
||||
SetCapture(hWndCurrent);
|
||||
|
||||
io.MouseDown[button] = true;
|
||||
this.imguiMouseIsDown[button] = true;
|
||||
this.mouseButtonsDown |= 1 << button;
|
||||
io.AddMouseButtonEvent(button, true);
|
||||
return default(LRESULT);
|
||||
}
|
||||
|
||||
|
|
@ -256,13 +289,12 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
case WM.WM_XBUTTONUP:
|
||||
{
|
||||
var button = GetButton(msg, wParam);
|
||||
if (io.WantCaptureMouse && this.imguiMouseIsDown[button])
|
||||
if (io.WantCaptureMouse)
|
||||
{
|
||||
if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent)
|
||||
this.mouseButtonsDown &= ~(1 << button);
|
||||
if (this.mouseButtonsDown == 0 && GetCapture() == hWndCurrent)
|
||||
ReleaseCapture();
|
||||
|
||||
io.MouseDown[button] = false;
|
||||
this.imguiMouseIsDown[button] = false;
|
||||
io.AddMouseButtonEvent(button, false);
|
||||
return default(LRESULT);
|
||||
}
|
||||
|
||||
|
|
@ -272,7 +304,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
case WM.WM_MOUSEWHEEL:
|
||||
if (io.WantCaptureMouse)
|
||||
{
|
||||
io.MouseWheel += GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA;
|
||||
io.AddMouseWheelEvent(0, GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA);
|
||||
return default(LRESULT);
|
||||
}
|
||||
|
||||
|
|
@ -280,7 +312,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
case WM.WM_MOUSEHWHEEL:
|
||||
if (io.WantCaptureMouse)
|
||||
{
|
||||
io.MouseWheelH += GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA;
|
||||
io.AddMouseWheelEvent(GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA, 0);
|
||||
return default(LRESULT);
|
||||
}
|
||||
|
||||
|
|
@ -374,68 +406,86 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
this.viewportHandler.UpdateMonitors();
|
||||
break;
|
||||
|
||||
case WM.WM_KILLFOCUS when hWndCurrent == this.hWnd:
|
||||
if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent)
|
||||
ReleaseCapture();
|
||||
case WM.WM_SETFOCUS when hWndCurrent == this.hWnd:
|
||||
io.AddFocusEvent(true);
|
||||
break;
|
||||
|
||||
ImGui.GetIO().WantCaptureMouse = false;
|
||||
ImGui.ClearWindowFocus();
|
||||
case WM.WM_KILLFOCUS when hWndCurrent == this.hWnd:
|
||||
io.AddFocusEvent(false);
|
||||
// if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent)
|
||||
// ReleaseCapture();
|
||||
//
|
||||
// ImGui.GetIO().WantCaptureMouse = false;
|
||||
// ImGui.ClearWindowFocus();
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void UpdateMousePos()
|
||||
private void UpdateMouseData(HWND focusedWindow)
|
||||
{
|
||||
var io = ImGui.GetIO();
|
||||
var pt = default(POINT);
|
||||
|
||||
// Depending on if Viewports are enabled, we have to change how we process
|
||||
// the cursor position. If viewports are enabled, we pass the absolute cursor
|
||||
// position to ImGui. Otherwise, we use the old method of passing client-local
|
||||
// mouse position to ImGui.
|
||||
if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable))
|
||||
var mouseScreenPos = default(POINT);
|
||||
var hasMouseScreenPos = GetCursorPos(&mouseScreenPos) != 0;
|
||||
|
||||
var isAppFocused =
|
||||
focusedWindow != default
|
||||
&& (focusedWindow == this.hWnd
|
||||
|| IsChild(focusedWindow, this.hWnd)
|
||||
|| !ImGui.FindViewportByPlatformHandle(focusedWindow).IsNull);
|
||||
|
||||
if (isAppFocused)
|
||||
{
|
||||
// (Optional) Set OS mouse position from Dear ImGui if requested (rarely used, only when ImGuiConfigFlags_NavEnableSetMousePos is enabled by user)
|
||||
// When multi-viewports are enabled, all Dear ImGui positions are same as OS positions.
|
||||
if (io.WantSetMousePos)
|
||||
{
|
||||
SetCursorPos((int)io.MousePos.X, (int)io.MousePos.Y);
|
||||
var pos = new POINT((int)io.MousePos.X, (int)io.MousePos.Y);
|
||||
if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) != 0)
|
||||
ClientToScreen(this.hWnd, &pos);
|
||||
SetCursorPos(pos.x, pos.y);
|
||||
}
|
||||
}
|
||||
|
||||
if (GetCursorPos(&pt))
|
||||
{
|
||||
io.MousePos.X = pt.x;
|
||||
io.MousePos.Y = pt.y;
|
||||
}
|
||||
else
|
||||
{
|
||||
io.MousePos.X = float.MinValue;
|
||||
io.MousePos.Y = float.MinValue;
|
||||
}
|
||||
// (Optional) Fallback to provide mouse position when focused (WM_MOUSEMOVE already provides this when hovered or captured)
|
||||
if (!io.WantSetMousePos && !this.mouseTracked && hasMouseScreenPos)
|
||||
{
|
||||
// Single viewport mode: mouse position in client window coordinates (io.MousePos is (0,0) when the mouse is on the upper-left corner of the app window)
|
||||
// (This is the position you can get with ::GetCursorPos() + ::ScreenToClient() or WM_MOUSEMOVE.)
|
||||
// Multi-viewport mode: mouse position in OS absolute coordinates (io.MousePos is (0,0) when the mouse is on the upper-left of the primary monitor)
|
||||
// (This is the position you can get with ::GetCursorPos() or WM_MOUSEMOVE + ::ClientToScreen(). In theory adding viewport->Pos to a client position would also be the same.)
|
||||
var mousePos = mouseScreenPos;
|
||||
if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) == 0)
|
||||
ClientToScreen(focusedWindow, &mousePos);
|
||||
io.AddMousePosEvent(mousePos.x, mousePos.y);
|
||||
}
|
||||
|
||||
// (Optional) When using multiple viewports: call io.AddMouseViewportEvent() with the viewport the OS mouse cursor is hovering.
|
||||
// If ImGuiBackendFlags_HasMouseHoveredViewport is not set by the backend, Dear imGui will ignore this field and infer the information using its flawed heuristic.
|
||||
// - [X] Win32 backend correctly ignore viewports with the _NoInputs flag (here using ::WindowFromPoint with WM_NCHITTEST + HTTRANSPARENT in WndProc does that)
|
||||
// Some backend are not able to handle that correctly. If a backend report an hovered viewport that has the _NoInputs flag (e.g. when dragging a window
|
||||
// for docking, the viewport has the _NoInputs flag in order to allow us to find the viewport under), then Dear ImGui is forced to ignore the value reported
|
||||
// by the backend, and use its flawed heuristic to guess the viewport behind.
|
||||
// - [X] Win32 backend correctly reports this regardless of another viewport behind focused and dragged from (we need this to find a useful drag and drop target).
|
||||
if (hasMouseScreenPos)
|
||||
{
|
||||
var viewport = this.ViewportFromPoint(mouseScreenPos);
|
||||
io.AddMouseViewportEvent(!viewport.IsNull ? viewport.ID : 0u);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (io.WantSetMousePos)
|
||||
{
|
||||
pt.x = (int)io.MousePos.X;
|
||||
pt.y = (int)io.MousePos.Y;
|
||||
ClientToScreen(this.hWnd, &pt);
|
||||
SetCursorPos(pt.x, pt.y);
|
||||
}
|
||||
|
||||
if (GetCursorPos(&pt) && ScreenToClient(this.hWnd, &pt))
|
||||
{
|
||||
io.MousePos.X = pt.x;
|
||||
io.MousePos.Y = pt.y;
|
||||
}
|
||||
else
|
||||
{
|
||||
io.MousePos.X = float.MinValue;
|
||||
io.MousePos.Y = float.MinValue;
|
||||
}
|
||||
io.AddMouseViewportEvent(0);
|
||||
}
|
||||
}
|
||||
|
||||
private ImGuiViewportPtr ViewportFromPoint(POINT mouseScreenPos)
|
||||
{
|
||||
var hoveredHwnd = WindowFromPoint(mouseScreenPos);
|
||||
return hoveredHwnd != default ? ImGui.FindViewportByPlatformHandle(hoveredHwnd) : default;
|
||||
}
|
||||
|
||||
private bool UpdateMouseCursor()
|
||||
{
|
||||
var io = ImGui.GetIO();
|
||||
|
|
@ -451,7 +501,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
return true;
|
||||
}
|
||||
|
||||
private void ProcessKeyEventsWorkarounds()
|
||||
private void ProcessKeyEventsWorkarounds(HWND focusedWindow)
|
||||
{
|
||||
// Left & right Shift keys: when both are pressed together, Windows tend to not generate the WM_KEYUP event for the first released one.
|
||||
if (ImGui.IsKeyDown(ImGuiKey.LeftShift) && !IsVkDown(VK.VK_LSHIFT))
|
||||
|
|
@ -480,7 +530,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
{
|
||||
// See: https://github.com/goatcorp/ImGuiScene/pull/13
|
||||
// > GetForegroundWindow from winuser.h is a surprisingly expensive function.
|
||||
var isForeground = GetForegroundWindow() == this.hWnd;
|
||||
var isForeground = focusedWindow == this.hWnd;
|
||||
for (var i = (int)ImGuiKey.NamedKeyBegin; i < (int)ImGuiKey.NamedKeyEnd; i++)
|
||||
{
|
||||
// Skip raising modifier keys if the game is focused.
|
||||
|
|
@ -646,14 +696,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
return;
|
||||
|
||||
var pio = ImGui.GetPlatformIO();
|
||||
|
||||
if (ImGui.GetPlatformIO().Handle->Monitors.Data != null)
|
||||
{
|
||||
// We allocated the platform monitor data in OnUpdateMonitors ourselves,
|
||||
// so we have to free it ourselves to ImGui doesn't try to, or else it will crash
|
||||
Marshal.FreeHGlobal(new IntPtr(ImGui.GetPlatformIO().Handle->Monitors.Data));
|
||||
ImGui.GetPlatformIO().Handle->Monitors = default;
|
||||
}
|
||||
ImGui.GetPlatformIO().Handle->Monitors.Free();
|
||||
|
||||
fixed (char* windowClassNamePtr = WindowClassName)
|
||||
{
|
||||
|
|
@ -693,59 +736,50 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
// Here we use a manual ImVector overload, free the existing monitor data,
|
||||
// and allocate our own, as we are responsible for telling ImGui about monitors
|
||||
var pio = ImGui.GetPlatformIO();
|
||||
var numMonitors = GetSystemMetrics(SM.SM_CMONITORS);
|
||||
var data = Marshal.AllocHGlobal(Marshal.SizeOf<ImGuiPlatformMonitor>() * numMonitors);
|
||||
if (pio.Handle->Monitors.Data != null)
|
||||
Marshal.FreeHGlobal(new IntPtr(pio.Handle->Monitors.Data));
|
||||
pio.Handle->Monitors = new(numMonitors, numMonitors, (ImGuiPlatformMonitor*)data.ToPointer());
|
||||
pio.Handle->Monitors.Resize(0);
|
||||
|
||||
// ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO();
|
||||
// Marshal.FreeHGlobal(platformIO.Handle->Monitors.Data);
|
||||
// int numMonitors = GetSystemMetrics(SystemMetric.SM_CMONITORS);
|
||||
// nint data = Marshal.AllocHGlobal(Marshal.SizeOf<ImGuiPlatformMonitor>() * numMonitors);
|
||||
// platformIO.Handle->Monitors = new ImVector(numMonitors, numMonitors, data);
|
||||
|
||||
var monitorIndex = -1;
|
||||
var enumfn = new MonitorEnumProcDelegate(
|
||||
(hMonitor, _, _, _) =>
|
||||
{
|
||||
monitorIndex++;
|
||||
var info = new MONITORINFO { cbSize = (uint)sizeof(MONITORINFO) };
|
||||
if (!GetMonitorInfoW(hMonitor, &info))
|
||||
return true;
|
||||
|
||||
var monitorLt = new Vector2(info.rcMonitor.left, info.rcMonitor.top);
|
||||
var monitorRb = new Vector2(info.rcMonitor.right, info.rcMonitor.bottom);
|
||||
var workLt = new Vector2(info.rcWork.left, info.rcWork.top);
|
||||
var workRb = new Vector2(info.rcWork.right, info.rcWork.bottom);
|
||||
// Give ImGui the info for this display
|
||||
|
||||
ref var imMonitor = ref ImGui.GetPlatformIO().Monitors.Ref(monitorIndex);
|
||||
imMonitor.MainPos = monitorLt;
|
||||
imMonitor.MainSize = monitorRb - monitorLt;
|
||||
imMonitor.WorkPos = workLt;
|
||||
imMonitor.WorkSize = workRb - workLt;
|
||||
imMonitor.DpiScale = 1f;
|
||||
return true;
|
||||
});
|
||||
EnumDisplayMonitors(
|
||||
default,
|
||||
null,
|
||||
(delegate* unmanaged<HMONITOR, HDC, RECT*, LPARAM, BOOL>)Marshal.GetFunctionPointerForDelegate(enumfn),
|
||||
default);
|
||||
EnumDisplayMonitors(default, null, &EnumDisplayMonitorsCallback, default);
|
||||
|
||||
Log.Information("Monitors set up!");
|
||||
for (var i = 0; i < numMonitors; i++)
|
||||
foreach (ref var monitor in pio.Handle->Monitors)
|
||||
{
|
||||
var monitor = pio.Handle->Monitors[i];
|
||||
Log.Information(
|
||||
"Monitor {Index}: {MainPos} {MainSize} {WorkPos} {WorkSize}",
|
||||
i,
|
||||
"Monitor: {MainPos} {MainSize} {WorkPos} {WorkSize}",
|
||||
monitor.MainPos,
|
||||
monitor.MainSize,
|
||||
monitor.WorkPos,
|
||||
monitor.WorkSize);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
static BOOL EnumDisplayMonitorsCallback(HMONITOR hMonitor, HDC hdc, RECT* rect, LPARAM lParam)
|
||||
{
|
||||
var info = new MONITORINFO { cbSize = (uint)sizeof(MONITORINFO) };
|
||||
if (!GetMonitorInfoW(hMonitor, &info))
|
||||
return true;
|
||||
|
||||
var monitorLt = new Vector2(info.rcMonitor.left, info.rcMonitor.top);
|
||||
var monitorRb = new Vector2(info.rcMonitor.right, info.rcMonitor.bottom);
|
||||
var workLt = new Vector2(info.rcWork.left, info.rcWork.top);
|
||||
var workRb = new Vector2(info.rcWork.right, info.rcWork.bottom);
|
||||
|
||||
// Give ImGui the info for this display
|
||||
var imMonitor = new ImGuiPlatformMonitor
|
||||
{
|
||||
MainPos = monitorLt,
|
||||
MainSize = monitorRb - monitorLt,
|
||||
WorkPos = workLt,
|
||||
WorkSize = workRb - workLt,
|
||||
DpiScale = 1f,
|
||||
};
|
||||
if ((info.dwFlags & MONITORINFOF_PRIMARY) != 0)
|
||||
ImGui.GetPlatformIO().Monitors.PushFront(imMonitor);
|
||||
else
|
||||
ImGui.GetPlatformIO().Monitors.PushBack(imMonitor);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
|
||||
|
|
@ -794,6 +828,9 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
|
|||
null);
|
||||
}
|
||||
|
||||
if (data->Hwnd == 0)
|
||||
Util.Fatal($"CreateWindowExW failed: {GetLastError()}", "ImGui Viewport error");
|
||||
|
||||
data->HwndOwned = true;
|
||||
viewport.PlatformRequestResize = false;
|
||||
viewport.PlatformHandle = viewport.PlatformHandleRaw = data->Hwnd;
|
||||
|
|
|
|||
|
|
@ -15,10 +15,6 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
|
|||
/// <summary>Color stacks to use while evaluating a SeString.</summary>
|
||||
internal sealed class SeStringColorStackSet
|
||||
{
|
||||
/// <summary>Parsed <see cref="UIColor"/>, containing colors to use with <see cref="MacroCode.ColorType"/> and
|
||||
/// <see cref="MacroCode.EdgeColorType"/>.</summary>
|
||||
private readonly uint[,] colorTypes;
|
||||
|
||||
/// <summary>Foreground color stack while evaluating a SeString for rendering.</summary>
|
||||
/// <remarks>Touched only from the main thread.</remarks>
|
||||
private readonly List<uint> colorStack = [];
|
||||
|
|
@ -39,30 +35,38 @@ internal sealed class SeStringColorStackSet
|
|||
foreach (var row in uiColor)
|
||||
maxId = (int)Math.Max(row.RowId, maxId);
|
||||
|
||||
this.colorTypes = new uint[maxId + 1, 4];
|
||||
this.ColorTypes = new uint[maxId + 1, 4];
|
||||
foreach (var row in uiColor)
|
||||
{
|
||||
// Contains ABGR.
|
||||
this.colorTypes[row.RowId, 0] = row.Dark;
|
||||
this.colorTypes[row.RowId, 1] = row.Light;
|
||||
this.colorTypes[row.RowId, 2] = row.ClassicFF;
|
||||
this.colorTypes[row.RowId, 3] = row.ClearBlue;
|
||||
this.ColorTypes[row.RowId, 0] = row.Dark;
|
||||
this.ColorTypes[row.RowId, 1] = row.Light;
|
||||
this.ColorTypes[row.RowId, 2] = row.ClassicFF;
|
||||
this.ColorTypes[row.RowId, 3] = row.ClearBlue;
|
||||
}
|
||||
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
// ImGui wants RGBA in LE.
|
||||
fixed (uint* p = this.colorTypes)
|
||||
fixed (uint* p = this.ColorTypes)
|
||||
{
|
||||
foreach (ref var r in new Span<uint>(p, this.colorTypes.GetLength(0) * this.colorTypes.GetLength(1)))
|
||||
foreach (ref var r in new Span<uint>(p, this.ColorTypes.GetLength(0) * this.ColorTypes.GetLength(1)))
|
||||
r = BinaryPrimitives.ReverseEndianness(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="SeStringColorStackSet"/> class.</summary>
|
||||
/// <param name="colorTypes">Color types.</param>
|
||||
public SeStringColorStackSet(uint[,] colorTypes) => this.ColorTypes = colorTypes;
|
||||
|
||||
/// <summary>Gets a value indicating whether at least one color has been pushed to the edge color stack.</summary>
|
||||
public bool HasAdditionalEdgeColor { get; private set; }
|
||||
|
||||
/// <summary>Gets the parsed <see cref="UIColor"/> containing colors to use with <see cref="MacroCode.ColorType"/>
|
||||
/// and <see cref="MacroCode.EdgeColorType"/>.</summary>
|
||||
public uint[,] ColorTypes { get; }
|
||||
|
||||
/// <summary>Resets the colors in the stack.</summary>
|
||||
/// <param name="drawState">Draw state.</param>
|
||||
internal void Initialize(scoped ref SeStringDrawState drawState)
|
||||
|
|
@ -191,9 +195,9 @@ internal sealed class SeStringColorStackSet
|
|||
}
|
||||
|
||||
// Opacity component is ignored.
|
||||
var color = themeIndex >= 0 && themeIndex < this.colorTypes.GetLength(1) &&
|
||||
colorTypeIndex < this.colorTypes.GetLength(0)
|
||||
? this.colorTypes[colorTypeIndex, themeIndex]
|
||||
var color = themeIndex >= 0 && themeIndex < this.ColorTypes.GetLength(1) &&
|
||||
colorTypeIndex < this.ColorTypes.GetLength(0)
|
||||
? this.ColorTypes[colorTypeIndex, themeIndex]
|
||||
: 0u;
|
||||
|
||||
rgbaStack.Add(color | 0xFF000000u);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
|
|
@ -25,7 +26,7 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
|
|||
|
||||
/// <summary>Draws SeString.</summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal unsafe class SeStringRenderer : IInternalDisposableService
|
||||
internal class SeStringRenderer : IServiceType
|
||||
{
|
||||
private const int ImGuiContextCurrentWindowOffset = 0x3FF0;
|
||||
private const int ImGuiWindowDcOffset = 0x118;
|
||||
|
|
@ -47,28 +48,19 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
|
||||
/// <summary>Parsed text fragments from a SeString.</summary>
|
||||
/// <remarks>Touched only from the main thread.</remarks>
|
||||
private readonly List<TextFragment> fragments = [];
|
||||
private readonly List<TextFragment> fragmentsMainThread = [];
|
||||
|
||||
/// <summary>Color stacks to use while evaluating a SeString for rendering.</summary>
|
||||
/// <remarks>Touched only from the main thread.</remarks>
|
||||
private readonly SeStringColorStackSet colorStackSet;
|
||||
|
||||
/// <summary>Splits a draw list so that different layers of a single glyph can be drawn out of order.</summary>
|
||||
private ImDrawListSplitter* splitter = ImGui.ImDrawListSplitter();
|
||||
private readonly SeStringColorStackSet colorStackSetMainThread;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private SeStringRenderer(DataManager dm, TargetSigScanner sigScanner)
|
||||
{
|
||||
this.colorStackSet = new(dm.Excel.GetSheet<UIColor>());
|
||||
this.colorStackSetMainThread = new(dm.Excel.GetSheet<UIColor>());
|
||||
this.gfd = dm.GetFile<GfdFile>("common/font/gfdata.gfd")!;
|
||||
}
|
||||
|
||||
/// <summary>Finalizes an instance of the <see cref="SeStringRenderer"/> class.</summary>
|
||||
~SeStringRenderer() => this.ReleaseUnmanagedResources();
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService() => this.ReleaseUnmanagedResources();
|
||||
|
||||
/// <summary>Compiles and caches a SeString from a text macro representation.</summary>
|
||||
/// <param name="text">SeString text macro representation.
|
||||
/// Newline characters will be normalized to newline payloads.</param>
|
||||
|
|
@ -80,6 +72,44 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
text.ReplaceLineEndings("<br>"),
|
||||
new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError }));
|
||||
|
||||
/// <summary>Creates a draw data that will draw the given SeString onto it.</summary>
|
||||
/// <param name="sss">SeString to render.</param>
|
||||
/// <param name="drawParams">Parameters for drawing.</param>
|
||||
/// <returns>A new self-contained draw data.</returns>
|
||||
public unsafe BufferBackedImDrawData CreateDrawData(
|
||||
ReadOnlySeStringSpan sss,
|
||||
scoped in SeStringDrawParams drawParams = default)
|
||||
{
|
||||
if (drawParams.TargetDrawList is not null)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"{nameof(SeStringDrawParams.TargetDrawList)} may not be specified.",
|
||||
nameof(drawParams));
|
||||
}
|
||||
|
||||
var dd = BufferBackedImDrawData.Create();
|
||||
|
||||
try
|
||||
{
|
||||
var size = this.Draw(sss, drawParams with { TargetDrawList = dd.ListPtr }).Size;
|
||||
|
||||
var offset = drawParams.ScreenOffset ?? Vector2.Zero;
|
||||
foreach (var vtx in new Span<ImDrawVert>(dd.ListPtr.VtxBuffer.Data, dd.ListPtr.VtxBuffer.Size))
|
||||
offset = Vector2.Min(offset, vtx.Pos);
|
||||
|
||||
dd.Data.DisplayPos = offset;
|
||||
dd.Data.DisplaySize = size - offset;
|
||||
dd.Data.Valid = 1;
|
||||
dd.UpdateDrawDataStatistics();
|
||||
return dd;
|
||||
}
|
||||
catch
|
||||
{
|
||||
dd.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Compiles and caches a SeString from a text macro representation, and then draws it.</summary>
|
||||
/// <param name="text">SeString text macro representation.
|
||||
/// Newline characters will be normalized to newline payloads.</param>
|
||||
|
|
@ -113,28 +143,42 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
/// <param name="imGuiId">ImGui ID, if link functionality is desired.</param>
|
||||
/// <param name="buttonFlags">Button flags to use on link interaction.</param>
|
||||
/// <returns>Interaction result of the rendered text.</returns>
|
||||
public SeStringDrawResult Draw(
|
||||
public unsafe SeStringDrawResult Draw(
|
||||
ReadOnlySeStringSpan sss,
|
||||
scoped in SeStringDrawParams drawParams = default,
|
||||
ImGuiId imGuiId = default,
|
||||
ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault)
|
||||
{
|
||||
// Drawing is only valid if done from the main thread anyway, especially with interactivity.
|
||||
ThreadSafety.AssertMainThread();
|
||||
// Interactivity is supported only from the main thread.
|
||||
if (!imGuiId.IsEmpty())
|
||||
ThreadSafety.AssertMainThread();
|
||||
|
||||
if (drawParams.TargetDrawList is not null && imGuiId)
|
||||
throw new ArgumentException("ImGuiId cannot be set if TargetDrawList is manually set.", nameof(imGuiId));
|
||||
|
||||
// This also does argument validation for drawParams. Do it here.
|
||||
var state = new SeStringDrawState(sss, drawParams, this.colorStackSet, this.splitter);
|
||||
using var cleanup = new DisposeSafety.ScopedFinalizer();
|
||||
|
||||
// Reset and initialize the state.
|
||||
this.fragments.Clear();
|
||||
this.colorStackSet.Initialize(ref state);
|
||||
ImFont* font = null;
|
||||
if (drawParams.Font.HasValue)
|
||||
font = drawParams.Font.Value;
|
||||
if (ThreadSafety.IsMainThread && drawParams.TargetDrawList is null && font is null)
|
||||
font = ImGui.GetFont();
|
||||
if (font is null)
|
||||
throw new ArgumentException("Specified font is empty.");
|
||||
|
||||
// This also does argument validation for drawParams. Do it here.
|
||||
// `using var` makes a struct read-only, but we do want to modify it.
|
||||
var stateStorage = new SeStringDrawState(
|
||||
sss,
|
||||
drawParams,
|
||||
ThreadSafety.IsMainThread ? this.colorStackSetMainThread : new(this.colorStackSetMainThread.ColorTypes),
|
||||
ThreadSafety.IsMainThread ? this.fragmentsMainThread : [],
|
||||
font);
|
||||
ref var state = ref Unsafe.AsRef(in stateStorage);
|
||||
|
||||
// Analyze the provided SeString and break it up to text fragments.
|
||||
this.CreateTextFragments(ref state);
|
||||
var fragmentSpan = CollectionsMarshal.AsSpan(this.fragments);
|
||||
var fragmentSpan = CollectionsMarshal.AsSpan(state.Fragments);
|
||||
|
||||
// Calculate size.
|
||||
var size = Vector2.Zero;
|
||||
|
|
@ -147,24 +191,17 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
|
||||
state.SplitDrawList();
|
||||
|
||||
// Handle cases where ImGui.AlignTextToFramePadding has been called.
|
||||
var context = ImGui.GetCurrentContext();
|
||||
var currLineTextBaseOffset = 0f;
|
||||
if (!context.IsNull)
|
||||
{
|
||||
var currentWindow = context.CurrentWindow;
|
||||
if (!currentWindow.IsNull)
|
||||
{
|
||||
currLineTextBaseOffset = currentWindow.DC.CurrLineTextBaseOffset;
|
||||
}
|
||||
}
|
||||
|
||||
var itemSize = size;
|
||||
if (currLineTextBaseOffset != 0f)
|
||||
if (drawParams.TargetDrawList is null)
|
||||
{
|
||||
itemSize.Y += 2 * currLineTextBaseOffset;
|
||||
foreach (ref var f in fragmentSpan)
|
||||
f.Offset += new Vector2(0, currLineTextBaseOffset);
|
||||
// Handle cases where ImGui.AlignTextToFramePadding has been called.
|
||||
var currLineTextBaseOffset = ImGui.GetCurrentContext().CurrentWindow.DC.CurrLineTextBaseOffset;
|
||||
if (currLineTextBaseOffset != 0f)
|
||||
{
|
||||
itemSize.Y += 2 * currLineTextBaseOffset;
|
||||
foreach (ref var f in fragmentSpan)
|
||||
f.Offset += new Vector2(0, currLineTextBaseOffset);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw all text fragments.
|
||||
|
|
@ -280,15 +317,6 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
return displayRune.Value != 0;
|
||||
}
|
||||
|
||||
private void ReleaseUnmanagedResources()
|
||||
{
|
||||
if (this.splitter is not null)
|
||||
{
|
||||
this.splitter->Destroy();
|
||||
this.splitter = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Creates text fragment, taking line and word breaking into account.</summary>
|
||||
/// <param name="state">Draw state.</param>
|
||||
private void CreateTextFragments(ref SeStringDrawState state)
|
||||
|
|
@ -391,7 +419,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
var overflows = Math.Max(w, xy.X + fragment.VisibleWidth) > state.WrapWidth;
|
||||
|
||||
// Test if the fragment does not fit into the current line and the current line is not empty.
|
||||
if (xy.X != 0 && this.fragments.Count > 0 && !this.fragments[^1].BreakAfter && overflows)
|
||||
if (xy.X != 0 && state.Fragments.Count > 0 && !state.Fragments[^1].BreakAfter && overflows)
|
||||
{
|
||||
// Introduce break if this is the first time testing the current break unit or the current fragment
|
||||
// is an entity.
|
||||
|
|
@ -401,7 +429,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
xy.X = 0;
|
||||
xy.Y += state.LineHeight;
|
||||
w = 0;
|
||||
CollectionsMarshal.AsSpan(this.fragments)[^1].BreakAfter = true;
|
||||
CollectionsMarshal.AsSpan(state.Fragments)[^1].BreakAfter = true;
|
||||
fragment.Offset = xy;
|
||||
|
||||
// Now that the fragment is given its own line, test if it overflows again.
|
||||
|
|
@ -419,16 +447,16 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
fragment = this.CreateFragment(state, prev, curr, true, xy, link, entity, remainingWidth);
|
||||
}
|
||||
}
|
||||
else if (this.fragments.Count > 0 && xy.X != 0)
|
||||
else if (state.Fragments.Count > 0 && xy.X != 0)
|
||||
{
|
||||
// New fragment fits into the current line, and it has a previous fragment in the same line.
|
||||
// If the previous fragment ends with a soft hyphen, adjust its width so that the width of its
|
||||
// trailing soft hyphens are not considered.
|
||||
if (this.fragments[^1].EndsWithSoftHyphen)
|
||||
xy.X += this.fragments[^1].AdvanceWidthWithoutSoftHyphen - this.fragments[^1].AdvanceWidth;
|
||||
if (state.Fragments[^1].EndsWithSoftHyphen)
|
||||
xy.X += state.Fragments[^1].AdvanceWidthWithoutSoftHyphen - state.Fragments[^1].AdvanceWidth;
|
||||
|
||||
// Adjust this fragment's offset from kerning distance.
|
||||
xy.X += state.CalculateScaledDistance(this.fragments[^1].LastRune, fragment.FirstRune);
|
||||
xy.X += state.CalculateScaledDistance(state.Fragments[^1].LastRune, fragment.FirstRune);
|
||||
fragment.Offset = xy;
|
||||
}
|
||||
|
||||
|
|
@ -439,7 +467,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
w = Math.Max(w, xy.X + fragment.VisibleWidth);
|
||||
xy.X += fragment.AdvanceWidth;
|
||||
prev = fragment.To;
|
||||
this.fragments.Add(fragment);
|
||||
state.Fragments.Add(fragment);
|
||||
|
||||
if (fragment.BreakAfter)
|
||||
{
|
||||
|
|
@ -491,7 +519,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
if (gfdTextureSrv != 0)
|
||||
{
|
||||
state.Draw(
|
||||
new ImTextureID(gfdTextureSrv),
|
||||
new(gfdTextureSrv),
|
||||
offset + new Vector2(x, MathF.Round((state.LineHeight - size.Y) / 2)),
|
||||
size,
|
||||
useHq ? gfdEntry.HqUv0 : gfdEntry.Uv0,
|
||||
|
|
@ -528,7 +556,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
|
||||
return;
|
||||
|
||||
static nint GetGfdTextureSrv()
|
||||
static unsafe nint GetGfdTextureSrv()
|
||||
{
|
||||
var uim = UIModule.Instance();
|
||||
if (uim is null)
|
||||
|
|
@ -553,7 +581,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
/// <summary>Determines a bitmap icon to display for the given SeString payload.</summary>
|
||||
/// <param name="sss">Byte span that should include a SeString payload.</param>
|
||||
/// <returns>Icon to display, or <see cref="None"/> if it should not be displayed as an icon.</returns>
|
||||
private BitmapFontIcon GetBitmapFontIconFor(ReadOnlySpan<byte> sss)
|
||||
private unsafe BitmapFontIcon GetBitmapFontIconFor(ReadOnlySpan<byte> sss)
|
||||
{
|
||||
var e = new ReadOnlySeStringSpan(sss).GetEnumerator();
|
||||
if (!e.MoveNext() || e.Current.MacroCode is not MacroCode.Icon and not MacroCode.Icon2)
|
||||
|
|
@ -710,38 +738,4 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
firstDisplayRune ?? default,
|
||||
lastNonSoftHyphenRune);
|
||||
}
|
||||
|
||||
/// <summary>Represents a text fragment in a SeString span.</summary>
|
||||
/// <param name="From">Starting byte offset (inclusive) in a SeString.</param>
|
||||
/// <param name="To">Ending byte offset (exclusive) in a SeString.</param>
|
||||
/// <param name="Link">Byte offset of the link that decorates this text fragment, or <c>-1</c> if none.</param>
|
||||
/// <param name="Offset">Offset in pixels w.r.t. <see cref="SeStringDrawParams.ScreenOffset"/>.</param>
|
||||
/// <param name="Entity">Replacement entity, if any.</param>
|
||||
/// <param name="VisibleWidth">Visible width of this text fragment. This is the width required to draw everything
|
||||
/// without clipping.</param>
|
||||
/// <param name="AdvanceWidth">Advance width of this text fragment. This is the width required to add to the cursor
|
||||
/// to position the next fragment correctly.</param>
|
||||
/// <param name="AdvanceWidthWithoutSoftHyphen">Same with <paramref name="AdvanceWidth"/>, but trimming all the
|
||||
/// trailing soft hyphens.</param>
|
||||
/// <param name="BreakAfter">Whether to insert a line break after this text fragment.</param>
|
||||
/// <param name="EndsWithSoftHyphen">Whether this text fragment ends with one or more soft hyphens.</param>
|
||||
/// <param name="FirstRune">First rune in this text fragment.</param>
|
||||
/// <param name="LastRune">Last rune in this text fragment, for the purpose of calculating kerning distance with
|
||||
/// the following text fragment in the same line, if any.</param>
|
||||
private record struct TextFragment(
|
||||
int From,
|
||||
int To,
|
||||
int Link,
|
||||
Vector2 Offset,
|
||||
SeStringReplacementEntity Entity,
|
||||
float VisibleWidth,
|
||||
float AdvanceWidth,
|
||||
float AdvanceWidthWithoutSoftHyphen,
|
||||
bool BreakAfter,
|
||||
bool EndsWithSoftHyphen,
|
||||
Rune FirstRune,
|
||||
Rune LastRune)
|
||||
{
|
||||
public bool IsSoftHyphenVisible => this.EndsWithSoftHyphen && this.BreakAfter;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
|
||||
|
||||
/// <summary>Represents a text fragment in a SeString span.</summary>
|
||||
/// <param name="From">Starting byte offset (inclusive) in a SeString.</param>
|
||||
/// <param name="To">Ending byte offset (exclusive) in a SeString.</param>
|
||||
/// <param name="Link">Byte offset of the link that decorates this text fragment, or <c>-1</c> if none.</param>
|
||||
/// <param name="Offset">Offset in pixels w.r.t. <see cref="SeStringDrawParams.ScreenOffset"/>.</param>
|
||||
/// <param name="Entity">Replacement entity, if any.</param>
|
||||
/// <param name="VisibleWidth">Visible width of this text fragment. This is the width required to draw everything
|
||||
/// without clipping.</param>
|
||||
/// <param name="AdvanceWidth">Advance width of this text fragment. This is the width required to add to the cursor
|
||||
/// to position the next fragment correctly.</param>
|
||||
/// <param name="AdvanceWidthWithoutSoftHyphen">Same with <paramref name="AdvanceWidth"/>, but trimming all the
|
||||
/// trailing soft hyphens.</param>
|
||||
/// <param name="BreakAfter">Whether to insert a line break after this text fragment.</param>
|
||||
/// <param name="EndsWithSoftHyphen">Whether this text fragment ends with one or more soft hyphens.</param>
|
||||
/// <param name="FirstRune">First rune in this text fragment.</param>
|
||||
/// <param name="LastRune">Last rune in this text fragment, for the purpose of calculating kerning distance with
|
||||
/// the following text fragment in the same line, if any.</param>
|
||||
internal record struct TextFragment(
|
||||
int From,
|
||||
int To,
|
||||
int Link,
|
||||
Vector2 Offset,
|
||||
SeStringReplacementEntity Entity,
|
||||
float VisibleWidth,
|
||||
float AdvanceWidth,
|
||||
float AdvanceWidthWithoutSoftHyphen,
|
||||
bool BreakAfter,
|
||||
bool EndsWithSoftHyphen,
|
||||
Rune FirstRune,
|
||||
Rune LastRune)
|
||||
{
|
||||
/// <summary>Gets a value indicating whether the fragment ends with a visible soft hyphen.</summary>
|
||||
public bool IsSoftHyphenVisible => this.EndsWithSoftHyphen && this.BreakAfter;
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
|
@ -6,6 +7,8 @@ using System.Text;
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using Lumina.Text.Payloads;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
|
@ -19,46 +22,75 @@ public unsafe ref struct SeStringDrawState
|
|||
private static readonly int ChannelCount = Enum.GetValues<SeStringDrawChannel>().Length;
|
||||
|
||||
private readonly ImDrawList* drawList;
|
||||
private readonly SeStringColorStackSet colorStackSet;
|
||||
private readonly ImDrawListSplitter* splitter;
|
||||
|
||||
private ImDrawListSplitter splitter;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="SeStringDrawState"/> struct.</summary>
|
||||
/// <param name="span">Raw SeString byte span.</param>
|
||||
/// <param name="ssdp">Instance of <see cref="SeStringDrawParams"/> to initialize from.</param>
|
||||
/// <param name="colorStackSet">Instance of <see cref="SeStringColorStackSet"/> to use.</param>
|
||||
/// <param name="splitter">Instance of ImGui Splitter to use.</param>
|
||||
/// <param name="fragments">Fragments.</param>
|
||||
/// <param name="font">Font to use.</param>
|
||||
internal SeStringDrawState(
|
||||
ReadOnlySpan<byte> span,
|
||||
scoped in SeStringDrawParams ssdp,
|
||||
SeStringColorStackSet colorStackSet,
|
||||
ImDrawListSplitter* splitter)
|
||||
List<TextFragment> fragments,
|
||||
ImFont* font)
|
||||
{
|
||||
this.colorStackSet = colorStackSet;
|
||||
this.splitter = splitter;
|
||||
this.drawList = ssdp.TargetDrawList ?? ImGui.GetWindowDrawList();
|
||||
this.Span = span;
|
||||
this.ColorStackSet = colorStackSet;
|
||||
this.Fragments = fragments;
|
||||
this.Font = font;
|
||||
|
||||
if (ssdp.TargetDrawList is null)
|
||||
{
|
||||
if (!ThreadSafety.IsMainThread)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"{nameof(ssdp.TargetDrawList)} must be set to render outside the main thread.");
|
||||
}
|
||||
|
||||
this.drawList = ssdp.TargetDrawList ?? ImGui.GetWindowDrawList();
|
||||
this.ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos();
|
||||
this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize();
|
||||
this.WrapWidth = ssdp.WrapWidth ?? ImGui.GetContentRegionAvail().X;
|
||||
this.Color = ssdp.Color ?? ImGui.GetColorU32(ImGuiCol.Text);
|
||||
this.LinkHoverBackColor = ssdp.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered);
|
||||
this.LinkActiveBackColor = ssdp.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive);
|
||||
this.ThemeIndex = ssdp.ThemeIndex ?? AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.drawList = ssdp.TargetDrawList.Value;
|
||||
this.ScreenOffset = Vector2.Zero;
|
||||
this.FontSize = ssdp.FontSize ?? throw new ArgumentException(
|
||||
$"{nameof(ssdp.FontSize)} must be set to render outside the main thread.");
|
||||
this.WrapWidth = ssdp.WrapWidth ?? float.MaxValue;
|
||||
this.Color = ssdp.Color ?? uint.MaxValue;
|
||||
this.LinkHoverBackColor = 0; // Interactivity is unused outside the main thread.
|
||||
this.LinkActiveBackColor = 0; // Interactivity is unused outside the main thread.
|
||||
this.ThemeIndex = ssdp.ThemeIndex ?? 0;
|
||||
}
|
||||
|
||||
this.splitter = default;
|
||||
this.GetEntity = ssdp.GetEntity;
|
||||
this.ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos();
|
||||
this.ScreenOffset = new(MathF.Round(this.ScreenOffset.X), MathF.Round(this.ScreenOffset.Y));
|
||||
this.Font = ssdp.EffectiveFont;
|
||||
this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize();
|
||||
this.FontSizeScale = this.FontSize / this.Font->FontSize;
|
||||
this.LineHeight = MathF.Round(ssdp.EffectiveLineHeight);
|
||||
this.WrapWidth = ssdp.WrapWidth ?? ImGui.GetContentRegionAvail().X;
|
||||
this.LinkUnderlineThickness = ssdp.LinkUnderlineThickness ?? 0f;
|
||||
this.Opacity = ssdp.EffectiveOpacity;
|
||||
this.EdgeOpacity = (ssdp.EdgeStrength ?? 0.25f) * ssdp.EffectiveOpacity;
|
||||
this.Color = ssdp.Color ?? ImGui.GetColorU32(ImGuiCol.Text);
|
||||
this.EdgeColor = ssdp.EdgeColor ?? 0xFF000000;
|
||||
this.ShadowColor = ssdp.ShadowColor ?? 0xFF000000;
|
||||
this.LinkHoverBackColor = ssdp.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered);
|
||||
this.LinkActiveBackColor = ssdp.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive);
|
||||
this.ForceEdgeColor = ssdp.ForceEdgeColor;
|
||||
this.ThemeIndex = ssdp.ThemeIndex ?? AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType;
|
||||
this.Bold = ssdp.Bold;
|
||||
this.Italic = ssdp.Italic;
|
||||
this.Edge = ssdp.Edge;
|
||||
this.Shadow = ssdp.Shadow;
|
||||
|
||||
this.ColorStackSet.Initialize(ref this);
|
||||
fragments.Clear();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SeStringDrawParams.TargetDrawList"/>
|
||||
|
|
@ -135,7 +167,7 @@ public unsafe ref struct SeStringDrawState
|
|||
|
||||
/// <summary>Gets a value indicating whether the edge should be drawn.</summary>
|
||||
public readonly bool ShouldDrawEdge =>
|
||||
(this.Edge || this.colorStackSet.HasAdditionalEdgeColor) && this.EdgeColor >= 0x1000000;
|
||||
(this.Edge || this.ColorStackSet.HasAdditionalEdgeColor) && this.EdgeColor >= 0x1000000;
|
||||
|
||||
/// <summary>Gets a value indicating whether the edge should be drawn.</summary>
|
||||
public readonly bool ShouldDrawShadow => this is { Shadow: true, ShadowColor: >= 0x1000000 };
|
||||
|
|
@ -143,11 +175,17 @@ public unsafe ref struct SeStringDrawState
|
|||
/// <summary>Gets a value indicating whether the edge should be drawn.</summary>
|
||||
public readonly bool ShouldDrawForeground => this is { Color: >= 0x1000000 };
|
||||
|
||||
/// <summary>Gets the color stacks.</summary>
|
||||
internal SeStringColorStackSet ColorStackSet { get; }
|
||||
|
||||
/// <summary>Gets the text fragments.</summary>
|
||||
internal List<TextFragment> Fragments { get; }
|
||||
|
||||
/// <summary>Sets the current channel in the ImGui draw list splitter.</summary>
|
||||
/// <param name="channelIndex">Channel to switch to.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly void SetCurrentChannel(SeStringDrawChannel channelIndex) =>
|
||||
this.splitter->SetCurrentChannel(this.drawList, (int)channelIndex);
|
||||
public void SetCurrentChannel(SeStringDrawChannel channelIndex) =>
|
||||
this.splitter.SetCurrentChannel(this.drawList, (int)channelIndex);
|
||||
|
||||
/// <summary>Draws a single texture.</summary>
|
||||
/// <param name="igTextureId">ImGui texture ID to draw from.</param>
|
||||
|
|
@ -216,7 +254,7 @@ public unsafe ref struct SeStringDrawState
|
|||
/// <summary>Draws a single glyph using current styling configurations.</summary>
|
||||
/// <param name="g">Glyph to draw.</param>
|
||||
/// <param name="offset">Offset of the glyph in pixels w.r.t. <see cref="ScreenOffset"/>.</param>
|
||||
internal readonly void DrawGlyph(scoped in ImGuiHelpers.ImFontGlyphReal g, Vector2 offset)
|
||||
internal void DrawGlyph(scoped in ImGuiHelpers.ImFontGlyphReal g, Vector2 offset)
|
||||
{
|
||||
var texId = this.Font->ContainerAtlas->Textures.Ref<ImFontAtlasTexture>(g.TextureIndex).TexID;
|
||||
var xy0 = new Vector2(
|
||||
|
|
@ -268,7 +306,7 @@ public unsafe ref struct SeStringDrawState
|
|||
/// <param name="offset">Offset of the glyph in pixels w.r.t.
|
||||
/// <see cref="SeStringDrawParams.ScreenOffset"/>.</param>
|
||||
/// <param name="advanceWidth">Advance width of the glyph.</param>
|
||||
internal readonly void DrawLinkUnderline(Vector2 offset, float advanceWidth)
|
||||
internal void DrawLinkUnderline(Vector2 offset, float advanceWidth)
|
||||
{
|
||||
if (this.LinkUnderlineThickness < 1f)
|
||||
return;
|
||||
|
|
@ -350,15 +388,15 @@ public unsafe ref struct SeStringDrawState
|
|||
switch (payload.MacroCode)
|
||||
{
|
||||
case MacroCode.Color:
|
||||
this.colorStackSet.HandleColorPayload(ref this, payload);
|
||||
this.ColorStackSet.HandleColorPayload(ref this, payload);
|
||||
return true;
|
||||
|
||||
case MacroCode.EdgeColor:
|
||||
this.colorStackSet.HandleEdgeColorPayload(ref this, payload);
|
||||
this.ColorStackSet.HandleEdgeColorPayload(ref this, payload);
|
||||
return true;
|
||||
|
||||
case MacroCode.ShadowColor:
|
||||
this.colorStackSet.HandleShadowColorPayload(ref this, payload);
|
||||
this.ColorStackSet.HandleShadowColorPayload(ref this, payload);
|
||||
return true;
|
||||
|
||||
case MacroCode.Bold when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u):
|
||||
|
|
@ -379,11 +417,11 @@ public unsafe ref struct SeStringDrawState
|
|||
return true;
|
||||
|
||||
case MacroCode.ColorType:
|
||||
this.colorStackSet.HandleColorTypePayload(ref this, payload);
|
||||
this.ColorStackSet.HandleColorTypePayload(ref this, payload);
|
||||
return true;
|
||||
|
||||
case MacroCode.EdgeColorType:
|
||||
this.colorStackSet.HandleEdgeColorTypePayload(ref this, payload);
|
||||
this.ColorStackSet.HandleEdgeColorTypePayload(ref this, payload);
|
||||
return true;
|
||||
|
||||
default:
|
||||
|
|
@ -393,10 +431,9 @@ public unsafe ref struct SeStringDrawState
|
|||
|
||||
/// <summary>Splits the draw list.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal readonly void SplitDrawList() =>
|
||||
this.splitter->Split(this.drawList, ChannelCount);
|
||||
internal void SplitDrawList() => this.splitter.Split(this.drawList, ChannelCount);
|
||||
|
||||
/// <summary>Merges the draw list.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal readonly void MergeDrawList() => this.splitter->Merge(this.drawList);
|
||||
internal void MergeDrawList() => this.splitter.Merge(this.drawList);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -533,6 +533,13 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
this.creditsDarkeningAnimation.Restart();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="DataWindow.GetWidget{T}"/>
|
||||
public T GetDataWindowWidget<T>() where T : IDataWindowWidget => this.dataWindow.GetWidget<T>();
|
||||
|
||||
/// <summary>Sets the data window current widget.</summary>
|
||||
/// <param name="widget">Widget to set current.</param>
|
||||
public void SetDataWindowWidget(IDataWindowWidget widget) => this.dataWindow.CurrentWidget = widget;
|
||||
|
||||
private void OnDraw()
|
||||
{
|
||||
this.FrameCount++;
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ internal class DataWindow : Window, IDisposable
|
|||
|
||||
private bool isExcept;
|
||||
private bool selectionCollapsed;
|
||||
private IDataWindowWidget currentWidget;
|
||||
|
||||
private bool isLoaded;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -82,9 +82,12 @@ internal class DataWindow : Window, IDisposable
|
|||
|
||||
this.RespectCloseHotkey = false;
|
||||
this.orderedModules = this.modules.OrderBy(module => module.DisplayName);
|
||||
this.currentWidget = this.orderedModules.First();
|
||||
this.CurrentWidget = this.orderedModules.First();
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the current widget.</summary>
|
||||
public IDataWindowWidget CurrentWidget { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose() => this.modules.OfType<IDisposable>().AggregateToDisposable().Dispose();
|
||||
|
||||
|
|
@ -99,6 +102,20 @@ internal class DataWindow : Window, IDisposable
|
|||
{
|
||||
}
|
||||
|
||||
/// <summary>Gets the data window widget of the specified type.</summary>
|
||||
/// <typeparam name="T">Type of the data window widget to find.</typeparam>
|
||||
/// <returns>Found widget.</returns>
|
||||
public T GetWidget<T>() where T : IDataWindowWidget
|
||||
{
|
||||
foreach (var m in this.modules)
|
||||
{
|
||||
if (m is T w)
|
||||
return w;
|
||||
}
|
||||
|
||||
throw new ArgumentException($"No widget of type {typeof(T).FullName} found.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the DataKind dropdown menu.
|
||||
/// </summary>
|
||||
|
|
@ -110,7 +127,7 @@ internal class DataWindow : Window, IDisposable
|
|||
|
||||
if (this.modules.FirstOrDefault(module => module.IsWidgetCommand(dataKind)) is { } targetModule)
|
||||
{
|
||||
this.currentWidget = targetModule;
|
||||
this.CurrentWidget = targetModule;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -153,9 +170,9 @@ internal class DataWindow : Window, IDisposable
|
|||
{
|
||||
foreach (var widget in this.orderedModules)
|
||||
{
|
||||
if (ImGui.Selectable(widget.DisplayName, this.currentWidget == widget))
|
||||
if (ImGui.Selectable(widget.DisplayName, this.CurrentWidget == widget))
|
||||
{
|
||||
this.currentWidget = widget;
|
||||
this.CurrentWidget = widget;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -206,9 +223,9 @@ internal class DataWindow : Window, IDisposable
|
|||
|
||||
try
|
||||
{
|
||||
if (this.currentWidget is { Ready: true })
|
||||
if (this.CurrentWidget is { Ready: true })
|
||||
{
|
||||
this.currentWidget.Draw();
|
||||
this.CurrentWidget.Draw();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Components;
|
||||
using Dalamud.Interface.Textures.Internal;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Internal;
|
||||
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
||||
|
||||
|
|
@ -87,12 +93,30 @@ internal class FontAwesomeTestWidget : IDataWindowWidget
|
|||
ImGuiHelpers.ScaledDummy(10f);
|
||||
for (var i = 0; i < this.icons?.Count; i++)
|
||||
{
|
||||
if (this.icons[i] == FontAwesomeIcon.None)
|
||||
continue;
|
||||
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.Text($"0x{(int)this.icons[i].ToIconChar():X}");
|
||||
ImGuiHelpers.ScaledRelativeSameLine(50f);
|
||||
ImGui.Text($"{this.iconNames?[i]}");
|
||||
ImGuiHelpers.ScaledRelativeSameLine(280f);
|
||||
ImGui.PushFont(this.useFixedWidth ? InterfaceManager.IconFontFixedWidth : InterfaceManager.IconFont);
|
||||
ImGui.Text(this.icons[i].ToIconString());
|
||||
ImGuiHelpers.ScaledRelativeSameLine(320f);
|
||||
if (this.useFixedWidth
|
||||
? ImGui.Button($"{(char)this.icons[i]}##FontAwesomeIconButton{i}")
|
||||
: ImGuiComponents.IconButton($"##FontAwesomeIconButton{i}", this.icons[i]))
|
||||
{
|
||||
_ = Service<DevTextureSaveMenu>.Get().ShowTextureSaveMenuAsync(
|
||||
this.DisplayName,
|
||||
this.icons[i].ToString(),
|
||||
Task.FromResult(
|
||||
Service<TextureManager>.Get().CreateTextureFromSeString(
|
||||
ReadOnlySeString.FromText(this.icons[i].ToIconString()),
|
||||
new() { Font = ImGui.GetFont(), FontSize = ImGui.GetFontSize() })));
|
||||
}
|
||||
|
||||
ImGui.PopFont();
|
||||
ImGuiHelpers.ScaledDummy(2f);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Data;
|
||||
|
|
@ -9,11 +10,13 @@ using Dalamud.Interface.ImGuiSeStringRenderer;
|
|||
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
|
||||
using Dalamud.Interface.Textures.Internal;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Internal;
|
||||
using Dalamud.Storage.Assets;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Lumina.Text;
|
||||
using Lumina.Text.Parse;
|
||||
using Lumina.Text.Payloads;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
|
|
@ -56,11 +59,11 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
|
|||
/// <inheritdoc/>
|
||||
public void Draw()
|
||||
{
|
||||
var t2 = ImGui.ColorConvertU32ToFloat4(this.style.Color ?? ImGui.GetColorU32(ImGuiCol.Text));
|
||||
var t2 = ImGui.ColorConvertU32ToFloat4(this.style.Color ??= ImGui.GetColorU32(ImGuiCol.Text));
|
||||
if (ImGui.ColorEdit4("Color", ref t2))
|
||||
this.style.Color = ImGui.ColorConvertFloat4ToU32(t2);
|
||||
|
||||
t2 = ImGui.ColorConvertU32ToFloat4(this.style.EdgeColor ?? 0xFF000000u);
|
||||
t2 = ImGui.ColorConvertU32ToFloat4(this.style.EdgeColor ??= 0xFF000000u);
|
||||
if (ImGui.ColorEdit4("Edge Color", ref t2))
|
||||
this.style.EdgeColor = ImGui.ColorConvertFloat4ToU32(t2);
|
||||
|
||||
|
|
@ -69,27 +72,27 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
|
|||
if (ImGui.Checkbox("Forced"u8, ref t))
|
||||
this.style.ForceEdgeColor = t;
|
||||
|
||||
t2 = ImGui.ColorConvertU32ToFloat4(this.style.ShadowColor ?? 0xFF000000u);
|
||||
if (ImGui.ColorEdit4("Shadow Color", ref t2))
|
||||
t2 = ImGui.ColorConvertU32ToFloat4(this.style.ShadowColor ??= 0xFF000000u);
|
||||
if (ImGui.ColorEdit4("Shadow Color"u8, ref t2))
|
||||
this.style.ShadowColor = ImGui.ColorConvertFloat4ToU32(t2);
|
||||
|
||||
t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered));
|
||||
if (ImGui.ColorEdit4("Link Hover Color", ref t2))
|
||||
t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkHoverBackColor ??= ImGui.GetColorU32(ImGuiCol.ButtonHovered));
|
||||
if (ImGui.ColorEdit4("Link Hover Color"u8, ref t2))
|
||||
this.style.LinkHoverBackColor = ImGui.ColorConvertFloat4ToU32(t2);
|
||||
|
||||
t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive));
|
||||
if (ImGui.ColorEdit4("Link Active Color", ref t2))
|
||||
t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkActiveBackColor ??= ImGui.GetColorU32(ImGuiCol.ButtonActive));
|
||||
if (ImGui.ColorEdit4("Link Active Color"u8, ref t2))
|
||||
this.style.LinkActiveBackColor = ImGui.ColorConvertFloat4ToU32(t2);
|
||||
|
||||
var t3 = this.style.LineHeight ?? 1f;
|
||||
var t3 = this.style.LineHeight ??= 1f;
|
||||
if (ImGui.DragFloat("Line Height"u8, ref t3, 0.01f, 0.4f, 3f, "%.02f"))
|
||||
this.style.LineHeight = t3;
|
||||
|
||||
t3 = this.style.Opacity ?? ImGui.GetStyle().Alpha;
|
||||
t3 = this.style.Opacity ??= 1f;
|
||||
if (ImGui.DragFloat("Opacity"u8, ref t3, 0.005f, 0f, 1f, "%.02f"))
|
||||
this.style.Opacity = t3;
|
||||
|
||||
t3 = this.style.EdgeStrength ?? 0.25f;
|
||||
t3 = this.style.EdgeStrength ??= 0.25f;
|
||||
if (ImGui.DragFloat("Edge Strength"u8, ref t3, 0.005f, 0f, 1f, "%.02f"))
|
||||
this.style.EdgeStrength = t3;
|
||||
|
||||
|
|
@ -240,6 +243,27 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
|
|||
Service<SeStringRenderer>.Get().CompileAndCache(this.testString).Data.Span));
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (ImGui.Button("Copy as Image"))
|
||||
{
|
||||
_ = Service<DevTextureSaveMenu>.Get().ShowTextureSaveMenuAsync(
|
||||
this.DisplayName,
|
||||
$"From {nameof(SeStringRendererTestWidget)}",
|
||||
Task.FromResult(
|
||||
Service<TextureManager>.Get().CreateTextureFromSeString(
|
||||
ReadOnlySeString.FromMacroString(
|
||||
this.testString,
|
||||
new(ExceptionMode: MacroStringParseExceptionMode.EmbedError)),
|
||||
this.style with
|
||||
{
|
||||
Font = ImGui.GetFont(),
|
||||
FontSize = ImGui.GetFontSize(),
|
||||
WrapWidth = ImGui.GetContentRegionAvail().X,
|
||||
ThemeIndex = AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType,
|
||||
})));
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(3);
|
||||
ImGuiHelpers.CompileSeStringWrapped(
|
||||
"Optional features implemented for the following test input:<br>" +
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
|
@ -306,12 +306,12 @@ internal class TexWidget : IDataWindowWidget
|
|||
pres->Release();
|
||||
|
||||
ImGui.Text($"RC: Resource({rcres})/View({rcsrv})");
|
||||
ImGui.Text(source.ToString());
|
||||
ImGui.Text($"{source.Width} x {source.Height} | {source}");
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.Text("RC: -"u8);
|
||||
ImGui.Text(" "u8);
|
||||
ImGui.Text("RC: -");
|
||||
ImGui.Text(string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -342,6 +342,10 @@ internal class TexWidget : IDataWindowWidget
|
|||
runLater?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>Adds a texture wrap for debug display purposes.</summary>
|
||||
/// <param name="textureTask">Task returning a texture.</param>
|
||||
public void AddTexture(Task<IDalamudTextureWrap> textureTask) => this.addedTextures.Add(new(Api10: textureTask));
|
||||
|
||||
private unsafe void DrawBlame(List<TextureManager.IBlameableDalamudTextureWrap> allBlames)
|
||||
{
|
||||
var im = Service<InterfaceManager>.Get();
|
||||
|
|
|
|||
|
|
@ -86,7 +86,8 @@ internal class TitleScreenMenuWindow : Window, IDisposable
|
|||
: base(
|
||||
"TitleScreenMenuOverlay",
|
||||
ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar |
|
||||
ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus)
|
||||
ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus |
|
||||
ImGuiWindowFlags.NoDocking)
|
||||
{
|
||||
this.showTsm = consoleManager.AddVariable("dalamud.show_tsm", "Show the Title Screen Menu", true);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Bindings.ImGui;
|
||||
|
||||
|
|
@ -33,10 +34,22 @@ public interface IFontHandle : IDisposable
|
|||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use <see cref="Push"/> directly if you want to keep the current ImGui font if the font is not ready.<br />
|
||||
/// Alternatively, use <see cref="WaitAsync"/> to wait for this property to become <c>true</c>.
|
||||
/// Alternatively, use <see cref="WaitAsync()"/> to wait for this property to become <c>true</c>.
|
||||
/// </remarks>
|
||||
bool Available { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to lock the fully constructed instance of <see cref="ImFontPtr"/> corresponding to the this
|
||||
/// <see cref="IFontHandle"/>, for use in any thread.<br />
|
||||
/// Modification of the font will exhibit undefined behavior if some other thread also uses the font.
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">The error message, if any.</param>
|
||||
/// <returns>
|
||||
/// An instance of <see cref="ILockedImFont"/> that <b>must</b> be disposed after use on success;
|
||||
/// <c>null</c> with <paramref name="errorMessage"/> populated on failure.
|
||||
/// </returns>
|
||||
ILockedImFont? TryLock(out string? errorMessage);
|
||||
|
||||
/// <summary>
|
||||
/// Locks the fully constructed instance of <see cref="ImFontPtr"/> corresponding to the this
|
||||
/// <see cref="IFontHandle"/>, for use in any thread.<br />
|
||||
|
|
@ -92,4 +105,11 @@ public interface IFontHandle : IDisposable
|
|||
/// </summary>
|
||||
/// <returns>A task containing this <see cref="IFontHandle"/>.</returns>
|
||||
Task<IFontHandle> WaitAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Waits for <see cref="Available"/> to become <c>true</c>.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task containing this <see cref="IFontHandle"/>.</returns>
|
||||
Task<IFontHandle> WaitAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -238,12 +238,17 @@ internal abstract class FontHandle : IFontHandle
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<IFontHandle> WaitAsync()
|
||||
public Task<IFontHandle> WaitAsync() => this.WaitAsync(CancellationToken.None);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<IFontHandle> WaitAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (this.Available)
|
||||
return Task.FromResult<IFontHandle>(this);
|
||||
|
||||
var tcs = new TaskCompletionSource<IFontHandle>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
cancellationToken.Register(() => tcs.TrySetCanceled());
|
||||
|
||||
this.ImFontChanged += OnImFontChanged;
|
||||
this.Disposed += OnDisposed;
|
||||
if (this.Available)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
using Dalamud.Interface.ImGuiSeStringRenderer;
|
||||
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Dalamud.Utility;
|
||||
|
||||
namespace Dalamud.Interface.Textures.Internal;
|
||||
|
||||
/// <summary>Service responsible for loading and disposing ImGui texture wraps.</summary>
|
||||
internal sealed partial class TextureManager
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly SeStringRenderer seStringRenderer = Service<SeStringRenderer>.Get();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDalamudTextureWrap CreateTextureFromSeString(
|
||||
ReadOnlySpan<byte> text,
|
||||
scoped in SeStringDrawParams drawParams = default,
|
||||
string? debugName = null)
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
using var dd = this.seStringRenderer.CreateDrawData(text, drawParams);
|
||||
var texture = this.CreateDrawListTexture(debugName ?? nameof(this.CreateTextureFromSeString));
|
||||
try
|
||||
{
|
||||
texture.Size = dd.Data.DisplaySize;
|
||||
texture.Draw(dd.DataPtr);
|
||||
return texture;
|
||||
}
|
||||
catch
|
||||
{
|
||||
texture.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
|||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
|
|
@ -248,7 +249,7 @@ internal sealed partial class TextureManager
|
|||
usage = D3D11_USAGE.D3D11_USAGE_DYNAMIC;
|
||||
else
|
||||
usage = D3D11_USAGE.D3D11_USAGE_DEFAULT;
|
||||
|
||||
|
||||
using var texture = this.device.CreateTexture2D(
|
||||
new()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Runtime.CompilerServices;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Interface.ImGuiSeStringRenderer;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Dalamud.IoC;
|
||||
|
|
@ -283,6 +284,18 @@ internal sealed class TextureManagerPluginScoped
|
|||
return textureWrap;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDalamudTextureWrap CreateTextureFromSeString(
|
||||
ReadOnlySpan<byte> text,
|
||||
scoped in SeStringDrawParams drawParams = default,
|
||||
string? debugName = null)
|
||||
{
|
||||
var manager = this.ManagerOrThrow;
|
||||
var textureWrap = manager.CreateTextureFromSeString(text, drawParams, debugName);
|
||||
manager.Blame(textureWrap, this.plugin);
|
||||
return textureWrap;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<IBitmapCodecInfo> GetSupportedImageDecoderInfos() =>
|
||||
this.ManagerOrThrow.Wic.GetSupportedDecoderInfos();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Bindings.ImGui;
|
||||
|
|
@ -691,13 +692,14 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
|
|||
FontAtlasAutoRebuildMode autoRebuildMode,
|
||||
bool isGlobalScaled = true,
|
||||
string? debugName = null) =>
|
||||
this.scopedFinalizer.Add(Service<FontAtlasFactory>
|
||||
.Get()
|
||||
.CreateFontAtlas(
|
||||
this.namespaceName + ":" + (debugName ?? "custom"),
|
||||
autoRebuildMode,
|
||||
isGlobalScaled,
|
||||
this.plugin));
|
||||
this.scopedFinalizer.Add(
|
||||
Service<FontAtlasFactory>
|
||||
.Get()
|
||||
.CreateFontAtlas(
|
||||
this.namespaceName + ":" + (debugName ?? "custom"),
|
||||
autoRebuildMode,
|
||||
isGlobalScaled,
|
||||
this.plugin));
|
||||
|
||||
/// <summary>
|
||||
/// Unregister the UiBuilder. Do not call this in plugin code.
|
||||
|
|
@ -868,6 +870,15 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
|
|||
// Note: do not dispose w; we do not own it
|
||||
}
|
||||
|
||||
public ILockedImFont? TryLock(out string? errorMessage)
|
||||
{
|
||||
if (this.wrapped is { } w)
|
||||
return w.TryLock(out errorMessage);
|
||||
|
||||
errorMessage = nameof(ObjectDisposedException);
|
||||
return null;
|
||||
}
|
||||
|
||||
public ILockedImFont Lock() =>
|
||||
this.wrapped?.Lock() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper));
|
||||
|
||||
|
|
@ -876,7 +887,13 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
|
|||
public void Pop() => this.WrappedNotDisposed.Pop();
|
||||
|
||||
public Task<IFontHandle> WaitAsync() =>
|
||||
this.WrappedNotDisposed.WaitAsync().ContinueWith(_ => (IFontHandle)this);
|
||||
this.wrapped?.WaitAsync().ContinueWith(_ => (IFontHandle)this)
|
||||
?? Task.FromException<IFontHandle>(new ObjectDisposedException(nameof(FontHandleWrapper)));
|
||||
|
||||
public Task<IFontHandle> WaitAsync(CancellationToken cancellationToken) =>
|
||||
this.wrapped?.WaitAsync(cancellationToken)
|
||||
.ContinueWith(_ => (IFontHandle)this, cancellationToken)
|
||||
?? Task.FromException<IFontHandle>(new ObjectDisposedException(nameof(FontHandleWrapper)));
|
||||
|
||||
public override string ToString() =>
|
||||
$"{nameof(FontHandleWrapper)}({this.wrapped?.ToString() ?? "disposed"})";
|
||||
|
|
|
|||
92
Dalamud/Interface/Utility/BufferBackedImDrawData.cs
Normal file
92
Dalamud/Interface/Utility/BufferBackedImDrawData.cs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Bindings.ImGui;
|
||||
|
||||
namespace Dalamud.Interface.Utility;
|
||||
|
||||
/// <summary>Wrapper aroundx <see cref="ImDrawData"/> containing one <see cref="ImDrawList"/>.</summary>
|
||||
public unsafe struct BufferBackedImDrawData : IDisposable
|
||||
{
|
||||
private nint buffer;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="BufferBackedImDrawData"/> struct.</summary>
|
||||
/// <param name="buffer">Address of buffer to use.</param>
|
||||
private BufferBackedImDrawData(nint buffer) => this.buffer = buffer;
|
||||
|
||||
/// <summary>Gets the <see cref="ImDrawData"/> stored in this buffer.</summary>
|
||||
public readonly ref ImDrawData Data => ref ((DataStruct*)this.buffer)->Data;
|
||||
|
||||
/// <summary>Gets the <see cref="ImDrawDataPtr"/> stored in this buffer.</summary>
|
||||
public readonly ImDrawDataPtr DataPtr => new((ImDrawData*)Unsafe.AsPointer(ref this.Data));
|
||||
|
||||
/// <summary>Gets the <see cref="ImDrawList"/> stored in this buffer.</summary>
|
||||
public readonly ref ImDrawList List => ref ((DataStruct*)this.buffer)->List;
|
||||
|
||||
/// <summary>Gets the <see cref="ImDrawListPtr"/> stored in this buffer.</summary>
|
||||
public readonly ImDrawListPtr ListPtr => new((ImDrawList*)Unsafe.AsPointer(ref this.List));
|
||||
|
||||
/// <summary>Creates a new instance of <see cref="BufferBackedImDrawData"/>.</summary>
|
||||
/// <returns>A new instance of <see cref="BufferBackedImDrawData"/>.</returns>
|
||||
public static BufferBackedImDrawData Create()
|
||||
{
|
||||
if (ImGui.GetCurrentContext().IsNull || ImGui.GetIO().FontDefault.Handle is null)
|
||||
throw new("ImGui is not ready");
|
||||
|
||||
var res = new BufferBackedImDrawData(Marshal.AllocHGlobal(sizeof(DataStruct)));
|
||||
var ds = (DataStruct*)res.buffer;
|
||||
*ds = default;
|
||||
|
||||
var atlas = ImGui.GetIO().Fonts;
|
||||
ds->SharedData = *ImGui.GetDrawListSharedData().Handle;
|
||||
ds->SharedData.TexIdCommon = atlas.Textures[atlas.TextureIndexCommon].TexID;
|
||||
ds->SharedData.TexUvWhitePixel = atlas.TexUvWhitePixel;
|
||||
ds->SharedData.TexUvLines = (Vector4*)Unsafe.AsPointer(ref atlas.TexUvLines[0]);
|
||||
ds->SharedData.Font = ImGui.GetIO().FontDefault;
|
||||
ds->SharedData.FontSize = ds->SharedData.Font->FontSize;
|
||||
ds->SharedData.ClipRectFullscreen = new(
|
||||
float.NegativeInfinity,
|
||||
float.NegativeInfinity,
|
||||
float.PositiveInfinity,
|
||||
float.PositiveInfinity);
|
||||
|
||||
ds->List.Data = &ds->SharedData;
|
||||
ds->ListPtr = &ds->List;
|
||||
ds->Data.CmdLists = &ds->ListPtr;
|
||||
ds->Data.CmdListsCount = 1;
|
||||
ds->Data.FramebufferScale = Vector2.One;
|
||||
|
||||
res.ListPtr._ResetForNewFrame();
|
||||
res.ListPtr.PushClipRectFullScreen();
|
||||
res.ListPtr.PushTextureID(new(atlas.TextureIndexCommon));
|
||||
return res;
|
||||
}
|
||||
|
||||
/// <summary>Updates the statistics information stored in <see cref="DataPtr"/> from <see cref="ListPtr"/>.</summary>
|
||||
public readonly void UpdateDrawDataStatistics()
|
||||
{
|
||||
this.Data.TotalIdxCount = this.List.IdxBuffer.Size;
|
||||
this.Data.TotalVtxCount = this.List.VtxBuffer.Size;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (this.buffer != 0)
|
||||
{
|
||||
this.ListPtr._ClearFreeMemory();
|
||||
Marshal.FreeHGlobal(this.buffer);
|
||||
this.buffer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct DataStruct
|
||||
{
|
||||
public ImDrawData Data;
|
||||
public ImDrawList* ListPtr;
|
||||
public ImDrawList List;
|
||||
public ImDrawListSharedData SharedData;
|
||||
}
|
||||
}
|
||||
|
|
@ -575,6 +575,15 @@ public static partial class ImGuiHelpers
|
|||
public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) =>
|
||||
self.IsNull ? other : self;
|
||||
|
||||
/// <summary>Creates a draw data that will draw the given SeString onto it.</summary>
|
||||
/// <param name="sss">SeString to render.</param>
|
||||
/// <param name="style">Initial rendering style.</param>
|
||||
/// <returns>A new self-contained draw data.</returns>
|
||||
internal static BufferBackedImDrawData CreateDrawData(
|
||||
ReadOnlySpan<byte> sss,
|
||||
scoped in SeStringDrawParams style = default) =>
|
||||
Service<SeStringRenderer>.Get().CreateDrawData(sss, style);
|
||||
|
||||
/// <summary>
|
||||
/// Mark 4K page as used, after adding a codepoint to a font.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ using System.Text;
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Interface.ImGuiFileDialog;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Interface.ImGuiNotification.Internal;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Internal.Windows.Data.Widgets;
|
||||
using Dalamud.Interface.Textures.Internal;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Serilog;
|
||||
|
|
@ -33,6 +35,14 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
|
|||
this.interfaceManager.Draw += this.InterfaceManagerOnDraw;
|
||||
}
|
||||
|
||||
private enum ContextMenuActionType
|
||||
{
|
||||
None,
|
||||
SaveAsFile,
|
||||
CopyToClipboard,
|
||||
SendToTexWidget,
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService() => this.interfaceManager.Draw -= this.InterfaceManagerOnDraw;
|
||||
|
||||
|
|
@ -66,15 +76,16 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
|
|||
var textureManager = await Service<TextureManager>.GetAsync();
|
||||
var popupName = $"{nameof(this.ShowTextureSaveMenuAsync)}_{textureWrap.Handle.Handle:X}";
|
||||
|
||||
ContextMenuActionType action;
|
||||
BitmapCodecInfo? encoder;
|
||||
{
|
||||
var first = true;
|
||||
var encoders = textureManager.Wic.GetSupportedEncoderInfos().ToList();
|
||||
var tcs = new TaskCompletionSource<BitmapCodecInfo?>(
|
||||
var tcs = new TaskCompletionSource<(ContextMenuActionType Action, BitmapCodecInfo? Codec)>(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
Service<InterfaceManager>.Get().Draw += DrawChoices;
|
||||
|
||||
encoder = await tcs.Task;
|
||||
(action, encoder) = await tcs.Task;
|
||||
|
||||
[SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "This shall not escape")]
|
||||
void DrawChoices()
|
||||
|
|
@ -98,13 +109,20 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
|
|||
}
|
||||
|
||||
if (ImGui.Selectable("Copy"u8))
|
||||
tcs.TrySetResult(null);
|
||||
tcs.TrySetResult((ContextMenuActionType.CopyToClipboard, null));
|
||||
if (ImGui.Selectable("Send to TexWidget"u8))
|
||||
tcs.TrySetResult((ContextMenuActionType.SendToTexWidget, null));
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
foreach (var encoder2 in encoders)
|
||||
{
|
||||
if (ImGui.Selectable(encoder2.Name))
|
||||
tcs.TrySetResult(encoder2);
|
||||
tcs.TrySetResult((ContextMenuActionType.SaveAsFile, encoder2));
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
const float previewImageWidth = 320;
|
||||
var size = textureWrap.Size;
|
||||
if (size.X > previewImageWidth)
|
||||
|
|
@ -120,50 +138,68 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
|
|||
}
|
||||
}
|
||||
|
||||
if (encoder is null)
|
||||
switch (action)
|
||||
{
|
||||
isCopy = true;
|
||||
await textureManager.CopyToClipboardAsync(textureWrap, name, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
var props = new Dictionary<string, object>();
|
||||
if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff)
|
||||
props["CompressionQuality"] = 1.0f;
|
||||
else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg ||
|
||||
encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif ||
|
||||
encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp)
|
||||
props["ImageQuality"] = 1.0f;
|
||||
case ContextMenuActionType.CopyToClipboard:
|
||||
isCopy = true;
|
||||
await textureManager.CopyToClipboardAsync(textureWrap, name, true);
|
||||
break;
|
||||
|
||||
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
this.fileDialogManager.SaveFileDialog(
|
||||
"Save texture...",
|
||||
$"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}",
|
||||
name + encoder.Extensions.First(),
|
||||
encoder.Extensions.First(),
|
||||
(ok, path2) =>
|
||||
{
|
||||
if (!ok)
|
||||
tcs.SetCanceled();
|
||||
else
|
||||
tcs.SetResult(path2);
|
||||
});
|
||||
var path = await tcs.Task.ConfigureAwait(false);
|
||||
|
||||
await textureManager.SaveToFileAsync(textureWrap, encoder.ContainerGuid, path, props: props);
|
||||
|
||||
var notif = Service<NotificationManager>.Get().AddNotification(
|
||||
new()
|
||||
{
|
||||
Content = $"File saved to: {path}",
|
||||
Title = initiatorName,
|
||||
Type = NotificationType.Success,
|
||||
});
|
||||
notif.Click += n =>
|
||||
case ContextMenuActionType.SendToTexWidget:
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
|
||||
n.Notification.DismissNow();
|
||||
};
|
||||
var framework = await Service<Framework>.GetAsync();
|
||||
var dalamudInterface = await Service<DalamudInterface>.GetAsync();
|
||||
await framework.RunOnFrameworkThread(
|
||||
() =>
|
||||
{
|
||||
var texWidget = dalamudInterface.GetDataWindowWidget<TexWidget>();
|
||||
dalamudInterface.SetDataWindowWidget(texWidget);
|
||||
texWidget.AddTexture(Task.FromResult(textureWrap.CreateWrapSharingLowLevelResource()));
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case ContextMenuActionType.SaveAsFile when encoder is not null:
|
||||
{
|
||||
var props = new Dictionary<string, object>();
|
||||
if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff)
|
||||
props["CompressionQuality"] = 1.0f;
|
||||
else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg ||
|
||||
encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif ||
|
||||
encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp)
|
||||
props["ImageQuality"] = 1.0f;
|
||||
|
||||
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
this.fileDialogManager.SaveFileDialog(
|
||||
"Save texture...",
|
||||
$"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}",
|
||||
name + encoder.Extensions.First(),
|
||||
encoder.Extensions.First(),
|
||||
(ok, path2) =>
|
||||
{
|
||||
if (!ok)
|
||||
tcs.SetCanceled();
|
||||
else
|
||||
tcs.SetResult(path2);
|
||||
});
|
||||
var path = await tcs.Task.ConfigureAwait(false);
|
||||
|
||||
await textureManager.SaveToFileAsync(textureWrap, encoder.ContainerGuid, path, props: props);
|
||||
|
||||
var notif = Service<NotificationManager>.Get().AddNotification(
|
||||
new()
|
||||
{
|
||||
Content = $"File saved to: {path}",
|
||||
Title = initiatorName,
|
||||
Type = NotificationType.Success,
|
||||
});
|
||||
notif.Click += n =>
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
|
||||
n.Notification.DismissNow();
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using CheapLoc;
|
||||
|
|
@ -19,10 +16,13 @@ using Dalamud.Interface.Utility.Internal;
|
|||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Interface.Windowing.Persistence;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
using static TerraFX.Interop.Windows.Windows;
|
||||
|
||||
namespace Dalamud.Interface.Windowing;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -31,11 +31,15 @@ namespace Dalamud.Interface.Windowing;
|
|||
public abstract class Window
|
||||
{
|
||||
private const float FadeInOutTime = 0.072f;
|
||||
private const string AdditionsPopupName = "WindowSystemContextActions";
|
||||
|
||||
private static readonly ModuleLog Log = new("WindowSystem");
|
||||
|
||||
private static bool wasEscPressedLastFrame = false;
|
||||
|
||||
private readonly TitleBarButton additionsButton;
|
||||
private readonly List<TitleBarButton> allButtons = [];
|
||||
|
||||
private bool internalLastIsOpen = false;
|
||||
private bool internalIsOpen = false;
|
||||
private bool internalIsPinned = false;
|
||||
|
|
@ -72,6 +76,20 @@ public abstract class Window
|
|||
this.WindowName = name;
|
||||
this.Flags = flags;
|
||||
this.ForceMainWindow = forceMainWindow;
|
||||
|
||||
this.additionsButton = new()
|
||||
{
|
||||
Icon = FontAwesomeIcon.Bars,
|
||||
IconOffset = new Vector2(2.5f, 1),
|
||||
Click = _ =>
|
||||
{
|
||||
this.internalIsClickthrough = false;
|
||||
this.presetDirty = false;
|
||||
ImGui.OpenPopup(AdditionsPopupName);
|
||||
},
|
||||
Priority = int.MinValue,
|
||||
AvailableClickthrough = true,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -482,14 +500,12 @@ public abstract class Window
|
|||
ImGuiP.GetCurrentWindow().InheritNoInputs = this.internalIsClickthrough;
|
||||
}
|
||||
|
||||
// Not supported yet on non-main viewports
|
||||
if ((this.internalIsPinned || this.internalIsClickthrough || this.internalAlpha.HasValue) &&
|
||||
ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
|
||||
if (ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
|
||||
{
|
||||
this.internalAlpha = null;
|
||||
this.internalIsPinned = false;
|
||||
this.internalIsClickthrough = false;
|
||||
this.presetDirty = true;
|
||||
if ((flags & ImGuiWindowFlags.NoInputs) == ImGuiWindowFlags.NoInputs)
|
||||
ImGui.GetWindowViewport().Flags |= ImGuiViewportFlags.NoInputs;
|
||||
else
|
||||
ImGui.GetWindowViewport().Flags &= ~ImGuiViewportFlags.NoInputs;
|
||||
}
|
||||
|
||||
if (this.hasError)
|
||||
|
|
@ -513,7 +529,6 @@ public abstract class Window
|
|||
}
|
||||
}
|
||||
|
||||
const string additionsPopupName = "WindowSystemContextActions";
|
||||
var flagsApplicableForTitleBarIcons = !flags.HasFlag(ImGuiWindowFlags.NoDecoration) &&
|
||||
!flags.HasFlag(ImGuiWindowFlags.NoTitleBar);
|
||||
var showAdditions = (this.AllowPinning || this.AllowClickthrough) &&
|
||||
|
|
@ -524,13 +539,8 @@ public abstract class Window
|
|||
{
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f);
|
||||
|
||||
if (ImGui.BeginPopup(additionsPopupName, ImGuiWindowFlags.NoMove))
|
||||
if (ImGui.BeginPopup(AdditionsPopupName, ImGuiWindowFlags.NoMove))
|
||||
{
|
||||
var isAvailable = ImGuiHelpers.CheckIsWindowOnMainViewport();
|
||||
|
||||
if (!isAvailable)
|
||||
ImGui.BeginDisabled();
|
||||
|
||||
if (this.internalIsClickthrough)
|
||||
ImGui.BeginDisabled();
|
||||
|
||||
|
|
@ -578,21 +588,11 @@ public abstract class Window
|
|||
this.presetDirty = true;
|
||||
}
|
||||
|
||||
if (isAvailable)
|
||||
{
|
||||
ImGui.TextColored(ImGuiColors.DalamudGrey,
|
||||
Loc.Localize("WindowSystemContextActionClickthroughDisclaimer",
|
||||
"Open this menu again by clicking the three dashes to disable clickthrough."));
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextColored(ImGuiColors.DalamudGrey,
|
||||
Loc.Localize("WindowSystemContextActionViewportDisclaimer",
|
||||
"These features are only available if this window is inside the game window."));
|
||||
}
|
||||
|
||||
if (!isAvailable)
|
||||
ImGui.EndDisabled();
|
||||
ImGui.TextColored(
|
||||
ImGuiColors.DalamudGrey,
|
||||
Loc.Localize(
|
||||
"WindowSystemContextActionClickthroughDisclaimer",
|
||||
"Open this menu again by clicking the three dashes to disable clickthrough."));
|
||||
|
||||
if (ImGui.Button(Loc.Localize("WindowSystemContextActionPrintWindow", "Print window")))
|
||||
printWindow = true;
|
||||
|
|
@ -603,34 +603,15 @@ public abstract class Window
|
|||
ImGui.PopStyleVar();
|
||||
}
|
||||
|
||||
unsafe
|
||||
if (flagsApplicableForTitleBarIcons)
|
||||
{
|
||||
var window = ImGuiP.GetCurrentWindow();
|
||||
|
||||
ImRect outRect;
|
||||
ImGuiP.TitleBarRect(&outRect, window);
|
||||
|
||||
var additionsButton = new TitleBarButton
|
||||
{
|
||||
Icon = FontAwesomeIcon.Bars,
|
||||
IconOffset = new Vector2(2.5f, 1),
|
||||
Click = _ =>
|
||||
{
|
||||
this.internalIsClickthrough = false;
|
||||
this.presetDirty = false;
|
||||
ImGui.OpenPopup(additionsPopupName);
|
||||
},
|
||||
Priority = int.MinValue,
|
||||
AvailableClickthrough = true,
|
||||
};
|
||||
|
||||
if (flagsApplicableForTitleBarIcons)
|
||||
{
|
||||
this.DrawTitleBarButtons(window, flags, outRect,
|
||||
showAdditions
|
||||
? this.TitleBarButtons.Append(additionsButton)
|
||||
: this.TitleBarButtons);
|
||||
}
|
||||
this.allButtons.Clear();
|
||||
this.allButtons.EnsureCapacity(this.TitleBarButtons.Count + 1);
|
||||
this.allButtons.AddRange(this.TitleBarButtons);
|
||||
if (showAdditions)
|
||||
this.allButtons.Add(this.additionsButton);
|
||||
this.allButtons.Sort(static (a, b) => b.Priority - a.Priority);
|
||||
this.DrawTitleBarButtons();
|
||||
}
|
||||
|
||||
if (wasFocused)
|
||||
|
|
@ -797,8 +778,11 @@ public abstract class Window
|
|||
}
|
||||
}
|
||||
|
||||
private unsafe void DrawTitleBarButtons(ImGuiWindowPtr window, ImGuiWindowFlags flags, ImRect titleBarRect, IEnumerable<TitleBarButton> buttons)
|
||||
private unsafe void DrawTitleBarButtons()
|
||||
{
|
||||
var window = ImGuiP.GetCurrentWindow();
|
||||
var flags = window.Flags;
|
||||
var titleBarRect = window.TitleBarRect();
|
||||
ImGui.PushClipRect(ImGui.GetWindowPos(), ImGui.GetWindowPos() + ImGui.GetWindowSize(), false);
|
||||
|
||||
var style = ImGui.GetStyle();
|
||||
|
|
@ -833,26 +817,22 @@ public abstract class Window
|
|||
var max = pos + new Vector2(fontSize, fontSize);
|
||||
ImRect bb = new(pos, max);
|
||||
var isClipped = !ImGuiP.ItemAdd(bb, id, null, 0);
|
||||
bool hovered, held;
|
||||
var pressed = false;
|
||||
bool hovered, held, pressed;
|
||||
|
||||
if (this.internalIsClickthrough)
|
||||
{
|
||||
hovered = false;
|
||||
held = false;
|
||||
|
||||
// ButtonBehavior does not function if the window is clickthrough, so we have to do it ourselves
|
||||
if (ImGui.IsMouseHoveringRect(pos, max))
|
||||
{
|
||||
hovered = true;
|
||||
var pad = ImGui.GetStyle().TouchExtraPadding;
|
||||
var rect = new ImRect(pos - pad, max + pad);
|
||||
hovered = rect.Contains(ImGui.GetMousePos());
|
||||
|
||||
// We can't use ImGui native functions here, because they don't work with clickthrough
|
||||
if ((Windows.Win32.PInvoke.GetKeyState((int)VirtualKey.LBUTTON) & 0x8000) != 0)
|
||||
{
|
||||
held = true;
|
||||
pressed = true;
|
||||
}
|
||||
}
|
||||
// Temporarily enable inputs
|
||||
// This will be reset on next frame, and then enabled again if it is still being hovered
|
||||
if (hovered && ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
|
||||
ImGui.GetWindowViewport().Flags &= ~ImGuiViewportFlags.NoInputs;
|
||||
|
||||
// We can't use ImGui native functions here, because they don't work with clickthrough
|
||||
pressed = held = hovered && (GetKeyState(VK.VK_LBUTTON) & 0x8000) != 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -881,7 +861,7 @@ public abstract class Window
|
|||
return pressed;
|
||||
}
|
||||
|
||||
foreach (var button in buttons.OrderBy(x => x.Priority))
|
||||
foreach (var button in this.allButtons)
|
||||
{
|
||||
if (this.internalIsClickthrough && !button.AvailableClickthrough)
|
||||
return;
|
||||
|
|
@ -1035,7 +1015,7 @@ public abstract class Window
|
|||
/// <summary>
|
||||
/// Gets or sets an action that is called when the button is clicked.
|
||||
/// </summary>
|
||||
public Action<ImGuiMouseButton> Click { get; set; }
|
||||
public Action<ImGuiMouseButton>? Click { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the priority the button shall be shown in.
|
||||
|
|
|
|||
102
Dalamud/Networking/Rpc/Model/DalamudUri.cs
Normal file
102
Dalamud/Networking/Rpc/Model/DalamudUri.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Web;
|
||||
|
||||
namespace Dalamud.Networking.Rpc.Model;
|
||||
|
||||
/// <summary>
|
||||
/// A Dalamud Uri, in the format:
|
||||
/// <code>dalamud://{NAMESPACE}/{ARBITRARY}</code>
|
||||
/// </summary>
|
||||
public record DalamudUri
|
||||
{
|
||||
private readonly Uri rawUri;
|
||||
|
||||
private DalamudUri(Uri uri)
|
||||
{
|
||||
if (uri.Scheme != "dalamud")
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(uri), "URI must be of scheme dalamud.");
|
||||
}
|
||||
|
||||
this.rawUri = uri;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the namespace that this URI should be routed to. Generally a high level component like "PluginInstaller".
|
||||
/// </summary>
|
||||
public string Namespace => this.rawUri.Authority;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw (untargeted) path and query params for this URI.
|
||||
/// </summary>
|
||||
public string Data =>
|
||||
this.rawUri.GetComponents(UriComponents.PathAndQuery | UriComponents.Fragment, UriFormat.UriEscaped);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw (untargeted) path for this URI.
|
||||
/// </summary>
|
||||
public string Path => this.rawUri.AbsolutePath;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of segments based on the provided Data element.
|
||||
/// </summary>
|
||||
public string[] Segments => this.GetDataSegments();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw query parameters for this URI, if any.
|
||||
/// </summary>
|
||||
public string Query => this.rawUri.Query;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the query params (as a parsed NameValueCollection) in this URI.
|
||||
/// </summary>
|
||||
public NameValueCollection QueryParams => HttpUtility.ParseQueryString(this.Query);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the fragment (if one is specified) in this URI.
|
||||
/// </summary>
|
||||
public string Fragment => this.rawUri.Fragment;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => this.rawUri.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Build a DalamudURI from a given URI.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI to convert to a Dalamud URI.</param>
|
||||
/// <returns>Returns a DalamudUri.</returns>
|
||||
public static DalamudUri FromUri(Uri uri)
|
||||
{
|
||||
return new DalamudUri(uri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build a DalamudURI from a URI in string format.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI to convert to a Dalamud URI.</param>
|
||||
/// <returns>Returns a DalamudUri.</returns>
|
||||
public static DalamudUri FromUri(string uri) => FromUri(new Uri(uri));
|
||||
|
||||
private string[] GetDataSegments()
|
||||
{
|
||||
// reimplementation of the System.URI#Segments, under MIT license.
|
||||
var path = this.Path;
|
||||
|
||||
var segments = new List<string>();
|
||||
var current = 0;
|
||||
while (current < path.Length)
|
||||
{
|
||||
var next = path.IndexOf('/', current);
|
||||
if (next == -1)
|
||||
{
|
||||
next = path.Length - 1;
|
||||
}
|
||||
|
||||
segments.Add(path.Substring(current, (next - current) + 1));
|
||||
current = next + 1;
|
||||
}
|
||||
|
||||
return segments.ToArray();
|
||||
}
|
||||
}
|
||||
95
Dalamud/Networking/Rpc/RpcConnection.cs
Normal file
95
Dalamud/Networking/Rpc/RpcConnection.cs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Networking.Rpc.Service;
|
||||
|
||||
using Serilog;
|
||||
|
||||
using StreamJsonRpc;
|
||||
|
||||
namespace Dalamud.Networking.Rpc;
|
||||
|
||||
/// <summary>
|
||||
/// A single RPC client session connected via a stream (named pipe or Unix socket).
|
||||
/// </summary>
|
||||
internal class RpcConnection : IDisposable
|
||||
{
|
||||
private readonly Stream stream;
|
||||
private readonly RpcServiceRegistry registry;
|
||||
private readonly CancellationTokenSource cts = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RpcConnection"/> class.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream that this connection will handle.</param>
|
||||
/// <param name="registry">A registry of RPC services.</param>
|
||||
public RpcConnection(Stream stream, RpcServiceRegistry registry)
|
||||
{
|
||||
this.Id = Guid.CreateVersion7();
|
||||
this.stream = stream;
|
||||
this.registry = registry;
|
||||
|
||||
var formatter = new JsonMessageFormatter();
|
||||
var handler = new HeaderDelimitedMessageHandler(stream, stream, formatter);
|
||||
|
||||
this.Rpc = new JsonRpc(handler);
|
||||
this.Rpc.AllowModificationWhileListening = true;
|
||||
this.Rpc.Disconnected += this.OnDisconnected;
|
||||
this.registry.Attach(this.Rpc);
|
||||
|
||||
this.Rpc.StartListening();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the GUID for this connection.
|
||||
/// </summary>
|
||||
public Guid Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the JsonRpc instance for this connection.
|
||||
/// </summary>
|
||||
public JsonRpc Rpc { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a task that's called on RPC completion.
|
||||
/// </summary>
|
||||
public Task Completion => this.Rpc.Completion;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (!this.cts.IsCancellationRequested)
|
||||
{
|
||||
this.cts.Cancel();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
this.Rpc.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, "Error disposing JsonRpc for client {Id}", this.Id);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
this.stream.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, "Error disposing stream for client {Id}", this.Id);
|
||||
}
|
||||
|
||||
this.cts.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void OnDisconnected(object? sender, JsonRpcDisconnectedEventArgs e)
|
||||
{
|
||||
Log.Debug("RPC client {Id} disconnected: {Reason}", this.Id, e.Description);
|
||||
this.registry.Detach(this.Rpc);
|
||||
this.Dispose();
|
||||
}
|
||||
}
|
||||
91
Dalamud/Networking/Rpc/RpcHostService.cs
Normal file
91
Dalamud/Networking/Rpc/RpcHostService.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Networking.Rpc.Transport;
|
||||
|
||||
namespace Dalamud.Networking.Rpc;
|
||||
|
||||
/// <summary>
|
||||
/// The Dalamud service repsonsible for hosting the RPC.
|
||||
/// </summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal class RpcHostService : IServiceType, IInternalDisposableService
|
||||
{
|
||||
private readonly ModuleLog log = new("RPC");
|
||||
private readonly RpcServiceRegistry registry = new();
|
||||
private readonly List<IRpcTransport> transports = [];
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RpcHostService"/> class.
|
||||
/// </summary>
|
||||
[ServiceManager.ServiceConstructor]
|
||||
public RpcHostService()
|
||||
{
|
||||
this.StartUnixTransport();
|
||||
|
||||
if (this.transports.Count == 0)
|
||||
{
|
||||
this.log.Warning("No RPC hosts could be started on this platform");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all active RPC transports.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IRpcTransport> Transports => this.transports;
|
||||
|
||||
/// <summary>
|
||||
/// Add a new service Object to the RPC host.
|
||||
/// </summary>
|
||||
/// <param name="service">The object to add.</param>
|
||||
public void AddService(object service) => this.registry.AddService(service);
|
||||
|
||||
/// <summary>
|
||||
/// Add a new standalone method to the RPC host.
|
||||
/// </summary>
|
||||
/// <param name="name">The method name to add.</param>
|
||||
/// <param name="handler">The handler to add.</param>
|
||||
public void AddMethod(string name, Delegate handler) => this.registry.AddMethod(name, handler);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void DisposeService()
|
||||
{
|
||||
foreach (var host in this.transports)
|
||||
{
|
||||
host.Dispose();
|
||||
}
|
||||
|
||||
this.transports.Clear();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IRpcTransport.InvokeClientAsync"/>
|
||||
public async Task<T> InvokeClientAsync<T>(Guid clientId, string method, params object[] arguments)
|
||||
{
|
||||
var clients = this.transports.SelectMany(t => t.Connections).ToImmutableDictionary();
|
||||
|
||||
if (!clients.TryGetValue(clientId, out var session))
|
||||
throw new KeyNotFoundException($"No client {clientId}");
|
||||
|
||||
return await session.Rpc.InvokeAsync<T>(method, arguments).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IRpcTransport.BroadcastNotifyAsync"/>
|
||||
public async Task BroadcastNotifyAsync(string method, params object[] arguments)
|
||||
{
|
||||
await foreach (var transport in this.transports.ToAsyncEnumerable().ConfigureAwait(false))
|
||||
{
|
||||
await transport.BroadcastNotifyAsync(method, arguments).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void StartUnixTransport()
|
||||
{
|
||||
var transport = new UnixRpcTransport(this.registry);
|
||||
this.transports.Add(transport);
|
||||
transport.Start();
|
||||
this.log.Information("RpcHostService listening to UNIX socket: {Socket}", transport.SocketPath);
|
||||
}
|
||||
}
|
||||
85
Dalamud/Networking/Rpc/RpcServiceRegistry.cs
Normal file
85
Dalamud/Networking/Rpc/RpcServiceRegistry.cs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
using StreamJsonRpc;
|
||||
|
||||
namespace Dalamud.Networking.Rpc;
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe registry of local RPC target objects that are exposed to every connected JsonRpc session.
|
||||
/// New sessions get all previously registered targets; newly added targets are attached to all active sessions.
|
||||
/// </summary>
|
||||
internal class RpcServiceRegistry
|
||||
{
|
||||
private readonly Lock sync = new();
|
||||
private readonly List<object> targets = [];
|
||||
private readonly List<(string Name, Delegate Handler)> methods = [];
|
||||
private readonly List<JsonRpc> activeRpcs = [];
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new local RPC target object. Its public JSON-RPC methods become callable by clients.
|
||||
/// Adds <paramref name="service"/> to the registry and attaches it to all active RPC sessions.
|
||||
/// </summary>
|
||||
/// <param name="service">The service instance containing JSON-RPC callable methods to expose.</param>
|
||||
public void AddService(object service)
|
||||
{
|
||||
lock (this.sync)
|
||||
{
|
||||
this.targets.Add(service);
|
||||
foreach (var rpc in this.activeRpcs)
|
||||
{
|
||||
rpc.AddLocalRpcTarget(service);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new standalone JSON-RPC method.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the method to add.</param>
|
||||
/// <param name="handler">The handler to add.</param>
|
||||
public void AddMethod(string name, Delegate handler)
|
||||
{
|
||||
lock (this.sync)
|
||||
{
|
||||
this.methods.Add((name, handler));
|
||||
foreach (var rpc in this.activeRpcs)
|
||||
{
|
||||
rpc.AddLocalRpcMethod(name, handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches a JsonRpc instance <paramref name="rpc"/> to the registry so it receives all existing service targets.
|
||||
/// </summary>
|
||||
/// <param name="rpc">The JsonRpc instance to attach and populate with current targets.</param>
|
||||
internal void Attach(JsonRpc rpc)
|
||||
{
|
||||
lock (this.sync)
|
||||
{
|
||||
this.activeRpcs.Add(rpc);
|
||||
foreach (var t in this.targets)
|
||||
{
|
||||
rpc.AddLocalRpcTarget(t);
|
||||
}
|
||||
|
||||
foreach (var m in this.methods)
|
||||
{
|
||||
rpc.AddLocalRpcMethod(m.Name, m.Handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detaches a JsonRpc instance <paramref name="rpc"/> from the registry (e.g. when a client disconnects).
|
||||
/// </summary>
|
||||
/// <param name="rpc">The JsonRpc instance being detached.</param>
|
||||
internal void Detach(JsonRpc rpc)
|
||||
{
|
||||
lock (this.sync)
|
||||
{
|
||||
this.activeRpcs.Remove(rpc);
|
||||
}
|
||||
}
|
||||
}
|
||||
133
Dalamud/Networking/Rpc/Service/ClientHelloService.cs
Normal file
133
Dalamud/Networking/Rpc/Service/ClientHelloService.cs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using Lumina.Excel.Sheets;
|
||||
|
||||
namespace Dalamud.Networking.Rpc.Service;
|
||||
|
||||
/// <summary>
|
||||
/// A minimal service to respond with information about this client.
|
||||
/// </summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed class ClientHelloService : IInternalDisposableService
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ClientHelloService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="rpcHostService">Injected host service.</param>
|
||||
[ServiceManager.ServiceConstructor]
|
||||
public ClientHelloService(RpcHostService rpcHostService)
|
||||
{
|
||||
rpcHostService.AddMethod("hello", this.HandleHello);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle a hello request.
|
||||
/// </summary>
|
||||
/// <param name="request">.</param>
|
||||
/// <returns>Respond with information.</returns>
|
||||
public async Task<ClientHelloResponse> HandleHello(ClientHelloRequest request)
|
||||
{
|
||||
var dalamud = await Service<Dalamud>.GetAsync();
|
||||
|
||||
return new ClientHelloResponse
|
||||
{
|
||||
ApiVersion = "1.0",
|
||||
DalamudVersion = Util.GetScmVersion(),
|
||||
GameVersion = dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown",
|
||||
ProcessId = Environment.ProcessId,
|
||||
ProcessStartTime = new DateTimeOffset(Process.GetCurrentProcess().StartTime).ToUnixTimeSeconds(),
|
||||
ClientState = await this.GetClientIdentifier(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void DisposeService()
|
||||
{
|
||||
}
|
||||
|
||||
private async Task<string> GetClientIdentifier()
|
||||
{
|
||||
var framework = await Service<Framework>.GetAsync();
|
||||
var clientState = await Service<ClientState>.GetAsync();
|
||||
var dataManager = await Service<DataManager>.GetAsync();
|
||||
|
||||
var clientIdentifier = $"FFXIV Process ${Environment.ProcessId}";
|
||||
|
||||
await framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
if (clientState.IsLoggedIn)
|
||||
{
|
||||
var player = clientState.LocalPlayer;
|
||||
if (player != null)
|
||||
{
|
||||
var world = dataManager.GetExcelSheet<World>().GetRow(player.HomeWorld.RowId);
|
||||
clientIdentifier = $"Logged in as {player.Name.TextValue} @ {world.Name.ExtractText()}";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
clientIdentifier = "On login screen";
|
||||
}
|
||||
});
|
||||
|
||||
return clientIdentifier;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A request from a client to say hello.
|
||||
/// </summary>
|
||||
internal record ClientHelloRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the API version this client is expecting.
|
||||
/// </summary>
|
||||
public string ApiVersion { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user agent of the client.
|
||||
/// </summary>
|
||||
public string UserAgent { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A response from Dalamud to a hello request.
|
||||
/// </summary>
|
||||
internal record ClientHelloResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the API version this server has offered.
|
||||
/// </summary>
|
||||
public string? ApiVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current Dalamud version.
|
||||
/// </summary>
|
||||
public string? DalamudVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current game version.
|
||||
/// </summary>
|
||||
public string? GameVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the process ID of this client.
|
||||
/// </summary>
|
||||
public int? ProcessId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the time this process started.
|
||||
/// </summary>
|
||||
public long? ProcessStartTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a state for this client for user display.
|
||||
/// </summary>
|
||||
public string? ClientState { get; init; }
|
||||
}
|
||||
107
Dalamud/Networking/Rpc/Service/LinkHandlerService.cs
Normal file
107
Dalamud/Networking/Rpc/Service/LinkHandlerService.cs
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Networking.Rpc.Model;
|
||||
using Dalamud.Utility;
|
||||
|
||||
namespace Dalamud.Networking.Rpc.Service;
|
||||
|
||||
/// <summary>
|
||||
/// A service responsible for handling Dalamud URIs and dispatching them accordingly.
|
||||
/// </summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal class LinkHandlerService : IInternalDisposableService
|
||||
{
|
||||
private readonly ModuleLog log = new("LinkHandler");
|
||||
|
||||
// key: namespace (e.g. "plugin" or "PluginInstaller") -> list of handlers
|
||||
private readonly ConcurrentDictionary<string, List<Action<DalamudUri>>> handlers
|
||||
= new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinkHandlerService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="rpcHostService">The injected RPC host service.</param>
|
||||
[ServiceManager.ServiceConstructor]
|
||||
public LinkHandlerService(RpcHostService rpcHostService)
|
||||
{
|
||||
rpcHostService.AddMethod("handleLink", this.HandleLinkCall);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void DisposeService()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a handler for a namespace. All URIs with this namespace will be dispatched to the handler.
|
||||
/// </summary>
|
||||
/// <param name="ns">The namespace to use for this subscription.</param>
|
||||
/// <param name="handler">The command handler.</param>
|
||||
public void Register(string ns, Action<DalamudUri> handler)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ns))
|
||||
throw new ArgumentNullException(nameof(ns));
|
||||
|
||||
var list = this.handlers.GetOrAdd(ns, _ => []);
|
||||
lock (list)
|
||||
{
|
||||
list.Add(handler);
|
||||
}
|
||||
|
||||
this.log.Verbose("Registered handler for {Namespace}", ns);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregister a handler.
|
||||
/// </summary>
|
||||
/// <param name="ns">The namespace to use for this subscription.</param>
|
||||
/// <param name="handler">The command handler.</param>
|
||||
public void Unregister(string ns, Action<DalamudUri> handler)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ns))
|
||||
return;
|
||||
|
||||
if (!this.handlers.TryGetValue(ns, out var list))
|
||||
return;
|
||||
|
||||
list.RemoveAll(x => x == handler);
|
||||
|
||||
if (list.Count == 0)
|
||||
this.handlers.TryRemove(ns, out _);
|
||||
|
||||
this.log.Verbose("Unregistered handler for {Namespace}", ns);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatch a URI to matching handlers.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI to parse and dispatch.</param>
|
||||
public void Dispatch(DalamudUri uri)
|
||||
{
|
||||
this.log.Information("Received URI: {Uri}", uri.ToString());
|
||||
|
||||
var ns = uri.Namespace;
|
||||
if (!this.handlers.TryGetValue(ns, out var actions))
|
||||
return;
|
||||
|
||||
foreach (var h in actions)
|
||||
{
|
||||
h.InvokeSafely(uri);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The RPC-invokable link handler.
|
||||
/// </summary>
|
||||
/// <param name="uri">A plain-text URI to parse.</param>
|
||||
public void HandleLinkCall(string uri)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(uri))
|
||||
return;
|
||||
|
||||
var du = DalamudUri.FromUri(uri);
|
||||
this.Dispatch(du);
|
||||
}
|
||||
}
|
||||
67
Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs
Normal file
67
Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
using Dalamud.Game.Gui.Toast;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Interface.ImGuiNotification.Internal;
|
||||
using Dalamud.Networking.Rpc.Model;
|
||||
|
||||
namespace Dalamud.Networking.Rpc.Service.Links;
|
||||
|
||||
#if DEBUG
|
||||
|
||||
/// <summary>
|
||||
/// A debug controller for link handling.
|
||||
/// </summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed class DebugLinkHandler : IInternalDisposableService
|
||||
{
|
||||
private readonly LinkHandlerService linkHandlerService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DebugLinkHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="linkHandler">Injected LinkHandler.</param>
|
||||
[ServiceManager.ServiceConstructor]
|
||||
public DebugLinkHandler(LinkHandlerService linkHandler)
|
||||
{
|
||||
this.linkHandlerService = linkHandler;
|
||||
|
||||
this.linkHandlerService.Register("debug", this.HandleLink);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void DisposeService()
|
||||
{
|
||||
this.linkHandlerService.Unregister("debug", this.HandleLink);
|
||||
}
|
||||
|
||||
private void HandleLink(DalamudUri uri)
|
||||
{
|
||||
var action = uri.Path.Split("/").GetValue(1)?.ToString();
|
||||
switch (action)
|
||||
{
|
||||
case "toast":
|
||||
this.ShowToast(uri);
|
||||
break;
|
||||
case "notification":
|
||||
this.ShowNotification(uri);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowToast(DalamudUri uri)
|
||||
{
|
||||
var message = uri.QueryParams.Get("message") ?? "Hello, world!";
|
||||
Service<ToastGui>.Get().ShowNormal(message);
|
||||
}
|
||||
|
||||
private void ShowNotification(DalamudUri uri)
|
||||
{
|
||||
Service<NotificationManager>.Get().AddNotification(
|
||||
new Notification
|
||||
{
|
||||
Title = uri.QueryParams.Get("title"),
|
||||
Content = uri.QueryParams.Get("content") ?? "Hello, world!",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
57
Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs
Normal file
57
Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
using System.Linq;
|
||||
|
||||
using Dalamud.Console;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Networking.Rpc.Model;
|
||||
using Dalamud.Plugin.Internal.Types;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
#pragma warning disable DAL_RPC
|
||||
|
||||
namespace Dalamud.Networking.Rpc.Service.Links;
|
||||
|
||||
/// <inheritdoc cref="IPluginLinkHandler" />
|
||||
[PluginInterface]
|
||||
[ServiceManager.ScopedService]
|
||||
[ResolveVia<IPluginLinkHandler>]
|
||||
public class PluginLinkHandler : IInternalDisposableService, IPluginLinkHandler
|
||||
{
|
||||
private readonly LinkHandlerService linkHandler;
|
||||
private readonly LocalPlugin localPlugin;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PluginLinkHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="localPlugin">The plugin to bind this service to.</param>
|
||||
/// <param name="linkHandler">The central link handler.</param>
|
||||
internal PluginLinkHandler(LocalPlugin localPlugin, LinkHandlerService linkHandler)
|
||||
{
|
||||
this.linkHandler = linkHandler;
|
||||
this.localPlugin = localPlugin;
|
||||
|
||||
this.linkHandler.Register("plugin", this.HandleUri);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IPluginLinkHandler.PluginUriReceived? OnUriReceived;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void DisposeService()
|
||||
{
|
||||
this.OnUriReceived = null;
|
||||
this.linkHandler.Unregister("plugin", this.HandleUri);
|
||||
}
|
||||
|
||||
private void HandleUri(DalamudUri uri)
|
||||
{
|
||||
var target = uri.Path.Split("/").ElementAtOrDefault(1);
|
||||
var thisPlugin = ConsoleManagerPluginUtil.GetSanitizedNamespaceName(this.localPlugin.InternalName);
|
||||
if (target == null || !string.Equals(target, thisPlugin, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.OnUriReceived?.Invoke(uri);
|
||||
}
|
||||
}
|
||||
32
Dalamud/Networking/Rpc/Transport/IRpcTransport.cs
Normal file
32
Dalamud/Networking/Rpc/Transport/IRpcTransport.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Dalamud.Networking.Rpc.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for RPC host implementations (named pipes or Unix sockets).
|
||||
/// </summary>
|
||||
internal interface IRpcTransport : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a list of active RPC connections.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<Guid, RpcConnection> Connections { get; }
|
||||
|
||||
/// <summary>Starts accepting client connections.</summary>
|
||||
void Start();
|
||||
|
||||
/// <summary>Invoke an RPC request on a specific client expecting a result.</summary>
|
||||
/// <param name="clientId">The client ID to invoke.</param>
|
||||
/// <param name="method">The method to invoke.</param>
|
||||
/// <param name="arguments">Any arguments to invoke.</param>
|
||||
/// <returns>An optional return based on the specified RPC.</returns>
|
||||
/// <typeparam name="T">The expected response type.</typeparam>
|
||||
Task<T> InvokeClientAsync<T>(Guid clientId, string method, params object[] arguments);
|
||||
|
||||
/// <summary>Send a notification to all connected clients (no response expected).</summary>
|
||||
/// <param name="method">The method name to broadcast.</param>
|
||||
/// <param name="arguments">The arguments to broadcast.</param>
|
||||
/// <returns>Returns a Task when completed.</returns>
|
||||
Task BroadcastNotifyAsync(string method, params object[] arguments);
|
||||
}
|
||||
207
Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs
Normal file
207
Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Utility;
|
||||
|
||||
namespace Dalamud.Networking.Rpc.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// Simple multi-client JSON-RPC Unix socket host using StreamJsonRpc.
|
||||
/// </summary>
|
||||
internal class UnixRpcTransport : IRpcTransport
|
||||
{
|
||||
private readonly ModuleLog log = new("RPC/Transport/UnixSocket");
|
||||
|
||||
private readonly RpcServiceRegistry registry;
|
||||
private readonly CancellationTokenSource cts = new();
|
||||
private readonly ConcurrentDictionary<Guid, RpcConnection> sessions = new();
|
||||
private readonly string? cleanupSocketDirectory;
|
||||
|
||||
private Task? acceptLoopTask;
|
||||
private Socket? listenSocket;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UnixRpcTransport"/> class.
|
||||
/// </summary>
|
||||
/// <param name="registry">The RPC service registry to use.</param>
|
||||
/// <param name="socketDirectory">The Unix socket directory to use. If null, defaults to Dalamud home directory.</param>
|
||||
/// <param name="socketName">The name of the socket to create.</param>
|
||||
public UnixRpcTransport(RpcServiceRegistry registry, string? socketDirectory = null, string? socketName = null)
|
||||
{
|
||||
this.registry = registry;
|
||||
socketName ??= $"DalamudRPC.{Environment.ProcessId}.sock";
|
||||
|
||||
if (!socketDirectory.IsNullOrEmpty())
|
||||
{
|
||||
this.SocketPath = Path.Combine(socketDirectory, socketName);
|
||||
}
|
||||
else
|
||||
{
|
||||
socketDirectory = Service<Dalamud>.Get().StartInfo.TempDirectory;
|
||||
|
||||
if (socketDirectory == null)
|
||||
{
|
||||
this.SocketPath = Path.Combine(Path.GetTempPath(), socketName);
|
||||
this.log.Warning("Temp dir was not set in StartInfo; using system temp for unix socket.");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.SocketPath = Path.Combine(socketDirectory, socketName);
|
||||
this.cleanupSocketDirectory = socketDirectory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path of the Unix socket this RPC host is using.
|
||||
/// </summary>
|
||||
public string SocketPath { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyDictionary<Guid, RpcConnection> Connections => this.sessions;
|
||||
|
||||
/// <summary>Starts accepting client connections.</summary>
|
||||
public void Start()
|
||||
{
|
||||
if (this.acceptLoopTask != null) return;
|
||||
|
||||
// Make the directory for the socket if it doesn't exist
|
||||
var socketDir = Path.GetDirectoryName(this.SocketPath);
|
||||
if (!string.IsNullOrEmpty(socketDir) && !Directory.Exists(socketDir))
|
||||
{
|
||||
this.log.Error("Directory for unix socket does not exist: {Path}", socketDir);
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete existing socket for this PID, if it exists.
|
||||
if (File.Exists(this.SocketPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(this.SocketPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.log.Warning(ex, "Failed to delete existing socket file: {Path}", this.SocketPath);
|
||||
}
|
||||
}
|
||||
|
||||
this.acceptLoopTask = Task.Factory.StartNew(this.AcceptLoopAsync, TaskCreationOptions.LongRunning);
|
||||
}
|
||||
|
||||
/// <summary>Invoke an RPC request on a specific client expecting a result.</summary>
|
||||
/// <param name="clientId">The client ID to invoke.</param>
|
||||
/// <param name="method">The method to invoke.</param>
|
||||
/// <param name="arguments">Any arguments to invoke.</param>
|
||||
/// <returns>An optional return based on the specified RPC.</returns>
|
||||
/// <typeparam name="T">The expected response type.</typeparam>
|
||||
public Task<T> InvokeClientAsync<T>(Guid clientId, string method, params object[] arguments)
|
||||
{
|
||||
if (!this.sessions.TryGetValue(clientId, out var session))
|
||||
throw new KeyNotFoundException($"No client {clientId}");
|
||||
|
||||
return session.Rpc.InvokeAsync<T>(method, arguments);
|
||||
}
|
||||
|
||||
/// <summary>Send a notification to all connected clients (no response expected).</summary>
|
||||
/// <param name="method">The method name to broadcast.</param>
|
||||
/// <param name="arguments">The arguments to broadcast.</param>
|
||||
/// <returns>Returns a Task when completed.</returns>
|
||||
public Task BroadcastNotifyAsync(string method, params object[] arguments)
|
||||
{
|
||||
var list = this.sessions.Values;
|
||||
var tasks = new List<Task>(list.Count);
|
||||
foreach (var s in list)
|
||||
{
|
||||
tasks.Add(s.Rpc.NotifyAsync(method, arguments));
|
||||
}
|
||||
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.cts.Cancel();
|
||||
this.acceptLoopTask?.Wait(1000);
|
||||
|
||||
foreach (var kv in this.sessions)
|
||||
{
|
||||
kv.Value.Dispose();
|
||||
}
|
||||
|
||||
this.sessions.Clear();
|
||||
|
||||
this.listenSocket?.Dispose();
|
||||
|
||||
if (File.Exists(this.SocketPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(this.SocketPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.log.Warning(ex, "Failed to delete socket file on dispose: {Path}", this.SocketPath);
|
||||
}
|
||||
}
|
||||
|
||||
this.cts.Dispose();
|
||||
this.log.Information("UnixRpcHost disposed ({Socket})", this.SocketPath);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private async Task AcceptLoopAsync()
|
||||
{
|
||||
var token = this.cts.Token;
|
||||
|
||||
try
|
||||
{
|
||||
var endpoint = new UnixDomainSocketEndPoint(this.SocketPath);
|
||||
this.listenSocket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
|
||||
this.listenSocket.Bind(endpoint);
|
||||
this.listenSocket.Listen(128);
|
||||
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
Socket? clientSocket = null;
|
||||
try
|
||||
{
|
||||
clientSocket = await this.listenSocket.AcceptAsync(token).ConfigureAwait(false);
|
||||
|
||||
var stream = new NetworkStream(clientSocket, ownsSocket: true);
|
||||
var session = new RpcConnection(stream, this.registry);
|
||||
this.sessions.TryAdd(session.Id, session);
|
||||
|
||||
this.log.Debug("RPC connection created: {Id}", session.Id);
|
||||
|
||||
_ = session.Completion.ContinueWith(t =>
|
||||
{
|
||||
this.sessions.TryRemove(session.Id, out _);
|
||||
this.log.Debug("RPC connection removed: {Id}", session.Id);
|
||||
}, TaskScheduler.Default);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
clientSocket?.Dispose();
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
clientSocket?.Dispose();
|
||||
this.log.Error(ex, "Error in socket accept loop");
|
||||
await Task.Delay(500, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.log.Error(ex, "Fatal error in Unix socket accept loop");
|
||||
}
|
||||
}
|
||||
}
|
||||
24
Dalamud/Plugin/Services/IPluginLinkHandler.cs
Normal file
24
Dalamud/Plugin/Services/IPluginLinkHandler.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using Dalamud.Networking.Rpc.Model;
|
||||
|
||||
namespace Dalamud.Plugin.Services;
|
||||
|
||||
/// <summary>
|
||||
/// A service to allow plugins to subscribe to dalamud:// URIs targeting them. Plugins will receive any URI sent to the
|
||||
/// <c>dalamud://plugin/{PLUGIN_INTERNAL_NAME}/...</c> namespace.
|
||||
/// </summary>
|
||||
[Experimental("DAL_RPC", Message = "This service will be finalized around 7.41 and may change before then.")]
|
||||
public interface IPluginLinkHandler : IDalamudService
|
||||
{
|
||||
/// <summary>
|
||||
/// A delegate containing the received URI.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI opened by the user.</param>
|
||||
public delegate void PluginUriReceived(DalamudUri uri);
|
||||
|
||||
/// <summary>
|
||||
/// The event fired when a URI targeting this plugin is received.
|
||||
/// </summary>
|
||||
event PluginUriReceived OnUriReceived;
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Dalamud.Plugin.SelfTest;
|
||||
using Dalamud.Plugin.SelfTest;
|
||||
|
||||
namespace Dalamud.Plugin.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for registering and unregistering self-test steps from plugins.
|
||||
|
|
@ -44,7 +46,7 @@ namespace Dalamud.Plugin.SelfTest;
|
|||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public interface ISelfTestRegistry
|
||||
public interface ISelfTestRegistry : IDalamudService
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers the self-test steps for this plugin.
|
||||
|
|
@ -2,9 +2,7 @@ using System.Collections.Generic;
|
|||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game;
|
||||
namespace Dalamud.Plugin.Services;
|
||||
|
||||
/// <summary>
|
||||
/// A SigScanner facilitates searching for memory signatures in a given ProcessModule.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Objects;
|
||||
namespace Dalamud.Plugin.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Get and set various kinds of targets for the player.
|
||||
|
|
@ -37,13 +37,13 @@ public interface ITargetManager : IDalamudService
|
|||
/// Set to null to clear the target.
|
||||
/// </summary>
|
||||
public IGameObject? SoftTarget { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the gpose target.
|
||||
/// Set to null to clear the target.
|
||||
/// </summary>
|
||||
public IGameObject? GPoseTarget { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the mouseover nameplate target.
|
||||
/// Set to null to clear the target.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
|
@ -6,6 +6,7 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.ImGuiSeStringRenderer;
|
||||
using Dalamud.Interface.Internal.Windows.Data.Widgets;
|
||||
using Dalamud.Interface.Textures;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
|
|
@ -186,6 +187,17 @@ public interface ITextureProvider : IDalamudService
|
|||
string? debugName = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Creates a texture by drawing a SeString onto it.</summary>
|
||||
/// <param name="text">SeString to render.</param>
|
||||
/// <param name="drawParams">Parameters for drawing.</param>
|
||||
/// <param name="debugName">Name for debug display purposes.</param>
|
||||
/// <returns>The new texture.</returns>
|
||||
/// <remarks>Can be only be used from the main thread.</remarks>
|
||||
public IDalamudTextureWrap CreateTextureFromSeString(
|
||||
ReadOnlySpan<byte> text,
|
||||
scoped in SeStringDrawParams drawParams = default,
|
||||
string? debugName = null);
|
||||
|
||||
/// <summary>Gets the supported bitmap decoders.</summary>
|
||||
/// <returns>The supported bitmap decoders.</returns>
|
||||
/// <remarks>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<Project>
|
||||
|
||||
<PropertyGroup Label="Target">
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<Platforms>x64</Platforms>
|
||||
<LangVersion>13.0</LangVersion>
|
||||
|
|
|
|||
|
|
@ -1,65 +1,68 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Analyzers -->
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4"/>
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556"/>
|
||||
<PackageVersion Include="JetBrains.Annotations" Version="2025.2.2"/>
|
||||
<ItemGroup>
|
||||
<!-- Analyzers -->
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageVersion Include="JetBrains.Annotations" Version="2025.2.2" />
|
||||
|
||||
<!-- Misc Libraries -->
|
||||
<PackageVersion Include="BitFaster.Caching" Version="2.4.1"/>
|
||||
<PackageVersion Include="CheapLoc" Version="1.1.8"/>
|
||||
<PackageVersion Include="MinSharp" Version="1.0.4"/>
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3"/>
|
||||
<PackageVersion Include="Lumina" Version="6.5.1"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.ObjectPool" Version="8.0.7"/>
|
||||
<PackageVersion Include="System.Collections.Immutable" Version="8.0.0"/>
|
||||
<PackageVersion Include="System.Drawing.Common" Version="8.0.0"/>
|
||||
<PackageVersion Include="System.Reactive" Version="5.0.0"/>
|
||||
<PackageVersion Include="System.Reflection.MetadataLoadContext" Version="8.0.0"/>
|
||||
<PackageVersion Include="System.Resources.Extensions" Version="8.0.0"/>
|
||||
<PackageVersion Include="DotNet.ReproducibleBuilds" Version="1.2.39"/>
|
||||
<PackageVersion Include="sqlite-net-pcl" Version="1.8.116"/>
|
||||
<!-- Misc Libraries -->
|
||||
<PackageVersion Include="BitFaster.Caching" Version="2.4.1" />
|
||||
<PackageVersion Include="CheapLoc" Version="1.1.8" />
|
||||
<PackageVersion Include="MinSharp" Version="1.0.4" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="Lumina" Version="6.5.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.ObjectPool" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Collections.Immutable" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Reactive" Version="5.0.0" />
|
||||
<PackageVersion Include="System.Reflection.MetadataLoadContext" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Resources.Extensions" Version="10.0.0" />
|
||||
<PackageVersion Include="DotNet.ReproducibleBuilds" Version="1.2.39" />
|
||||
<PackageVersion Include="sqlite-net-pcl" Version="1.8.116" />
|
||||
|
||||
<!-- DirectX / Win32 -->
|
||||
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.22621.2"/>
|
||||
<PackageVersion Include="SharpDX.Direct3D11" Version="4.2.0"/>
|
||||
<PackageVersion Include="SharpDX.Mathematics" Version="4.2.0"/>
|
||||
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.183"/>
|
||||
<!-- DirectX / Win32 -->
|
||||
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.22621.2" />
|
||||
<PackageVersion Include="SharpDX.Direct3D11" Version="4.2.0" />
|
||||
<PackageVersion Include="SharpDX.Mathematics" Version="4.2.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.183" />
|
||||
|
||||
<!-- Logging -->
|
||||
<PackageVersion Include="Serilog" Version="4.0.2"/>
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="2.0.0"/>
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0"/>
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0"/>
|
||||
<!-- Logging -->
|
||||
<PackageVersion Include="Serilog" Version="4.0.2" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="2.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
|
||||
<!-- Injector Utilities -->
|
||||
<PackageVersion Include="Iced" Version="1.17.0"/>
|
||||
<PackageVersion Include="PeNet" Version="2.6.4"/>
|
||||
<!-- Injector Utilities -->
|
||||
<PackageVersion Include="Iced" Version="1.17.0" />
|
||||
<PackageVersion Include="PeNet" Version="2.6.4" />
|
||||
|
||||
<!-- HexaGen -->
|
||||
<PackageVersion Include="HexaGen.Runtime" Version="1.1.20"/>
|
||||
<!-- HexaGen -->
|
||||
<PackageVersion Include="HexaGen.Runtime" Version="1.1.20" />
|
||||
|
||||
<!-- Reloaded -->
|
||||
<PackageVersion Include="goatcorp.Reloaded.Hooks" Version="4.2.0-goatcorp7"/>
|
||||
<PackageVersion Include="goatcorp.Reloaded.Assembler" Version="1.0.14-goatcorp5"/>
|
||||
<PackageVersion Include="Reloaded.Memory" Version="7.0.0"/>
|
||||
<PackageVersion Include="Reloaded.Memory.Buffers" Version="2.0.0"/>
|
||||
<!-- Reloaded -->
|
||||
<PackageVersion Include="goatcorp.Reloaded.Hooks" Version="4.2.0-goatcorp7" />
|
||||
<PackageVersion Include="goatcorp.Reloaded.Assembler" Version="1.0.14-goatcorp5" />
|
||||
<PackageVersion Include="Reloaded.Memory" Version="7.0.0" />
|
||||
<PackageVersion Include="Reloaded.Memory.Buffers" Version="2.0.0" />
|
||||
|
||||
<!-- Unit Testing -->
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="16.10.0"/>
|
||||
<PackageVersion Include="xunit" Version="2.4.1"/>
|
||||
<PackageVersion Include="xunit.abstractions" Version="2.0.3"/>
|
||||
<PackageVersion Include="xunit.analyzers" Version="0.10.0"/>
|
||||
<PackageVersion Include="xunit.assert" Version="2.4.1"/>
|
||||
<PackageVersion Include="xunit.core" Version="2.4.1"/>
|
||||
<PackageVersion Include="xunit.extensibility.core" Version="2.4.1"/>
|
||||
<PackageVersion Include="xunit.extensibility.execution" Version="2.4.1"/>
|
||||
<PackageVersion Include="xunit.runner.console" Version="2.4.1"/>
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.3"/>
|
||||
</ItemGroup>
|
||||
<!-- Named Pipes / RPC -->
|
||||
<PackageVersion Include="StreamJsonRpc" Version="2.22.23" />
|
||||
|
||||
<!-- Unit Testing -->
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
|
||||
<PackageVersion Include="xunit" Version="2.4.1" />
|
||||
<PackageVersion Include="xunit.abstractions" Version="2.0.3" />
|
||||
<PackageVersion Include="xunit.analyzers" Version="0.10.0" />
|
||||
<PackageVersion Include="xunit.assert" Version="2.4.1" />
|
||||
<PackageVersion Include="xunit.core" Version="2.4.1" />
|
||||
<PackageVersion Include="xunit.extensibility.core" Version="2.4.1" />
|
||||
<PackageVersion Include="xunit.extensibility.execution" Version="2.4.1" />
|
||||
<PackageVersion Include="xunit.runner.console" Version="2.4.1" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -42,10 +42,7 @@ public class DalamudBuild : NukeBuild
|
|||
|
||||
AbsolutePath InjectorProjectDir => RootDirectory / "Dalamud.Injector";
|
||||
AbsolutePath InjectorProjectFile => InjectorProjectDir / "Dalamud.Injector.csproj";
|
||||
|
||||
AbsolutePath InjectorBootProjectDir => RootDirectory / "Dalamud.Injector.Boot";
|
||||
AbsolutePath InjectorBootProjectFile => InjectorBootProjectDir / "Dalamud.Injector.Boot.vcxproj";
|
||||
|
||||
|
||||
AbsolutePath TestProjectDir => RootDirectory / "Dalamud.Test";
|
||||
AbsolutePath TestProjectFile => TestProjectDir / "Dalamud.Test.csproj";
|
||||
|
||||
|
|
@ -172,14 +169,6 @@ public class DalamudBuild : NukeBuild
|
|||
.EnableNoRestore());
|
||||
});
|
||||
|
||||
Target CompileInjectorBoot => _ => _
|
||||
.Executes(() =>
|
||||
{
|
||||
MSBuildTasks.MSBuild(s => s
|
||||
.SetTargetPath(InjectorBootProjectFile)
|
||||
.SetConfiguration(Configuration));
|
||||
});
|
||||
|
||||
Target SetCILogging => _ => _
|
||||
.DependentFor(Compile)
|
||||
.OnlyWhenStatic(() => IsCIBuild)
|
||||
|
|
@ -196,7 +185,6 @@ public class DalamudBuild : NukeBuild
|
|||
.DependsOn(CompileDalamudBoot)
|
||||
.DependsOn(CompileDalamudCrashHandler)
|
||||
.DependsOn(CompileInjector)
|
||||
.DependsOn(CompileInjectorBoot)
|
||||
;
|
||||
|
||||
Target CI => _ => _
|
||||
|
|
@ -250,11 +238,6 @@ public class DalamudBuild : NukeBuild
|
|||
.SetProject(InjectorProjectFile)
|
||||
.SetConfiguration(Configuration));
|
||||
|
||||
MSBuildTasks.MSBuild(s => s
|
||||
.SetProjectFile(InjectorBootProjectFile)
|
||||
.SetConfiguration(Configuration)
|
||||
.SetTargets("Clean"));
|
||||
|
||||
FileSystemTasks.DeleteDirectory(ArtifactsDirectory);
|
||||
Directory.CreateDirectory(ArtifactsDirectory);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Nuke.Common" Version="6.2.1" />
|
||||
<PackageReference Include="System.Runtime.Serialization.Formatters" Version="9.0.0" />
|
||||
<PackageReference Include="System.Runtime.Serialization.Formatters" Version="10.0.0" />
|
||||
<PackageReference Remove="Microsoft.CodeAnalysis.BannedApiAnalyzers" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"sdk": {
|
||||
"version": "9.0.0",
|
||||
"version": "10.0.0",
|
||||
"rollForward": "latestMinor",
|
||||
"allowPrerelease": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,12 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Dalamud.Bindings.ImGui;
|
||||
|
||||
/// <summary>
|
||||
/// A structure representing a dynamic array for unmanaged types.
|
||||
/// </summary>
|
||||
public unsafe struct ImVector
|
||||
{
|
||||
public readonly int Size;
|
||||
|
|
@ -15,23 +20,23 @@ public unsafe struct ImVector
|
|||
Data = data;
|
||||
}
|
||||
|
||||
public ref T Ref<T>(int index)
|
||||
{
|
||||
return ref Unsafe.AsRef<T>((byte*)Data + index * Unsafe.SizeOf<T>());
|
||||
}
|
||||
public readonly ref T Ref<T>(int index) => ref Unsafe.AsRef<T>((byte*)this.Data + (index * Unsafe.SizeOf<T>()));
|
||||
|
||||
public IntPtr Address<T>(int index)
|
||||
{
|
||||
return (IntPtr)((byte*)Data + index * Unsafe.SizeOf<T>());
|
||||
}
|
||||
public readonly nint Address<T>(int index) => (nint)((byte*)this.Data + (index * Unsafe.SizeOf<T>()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A structure representing a dynamic array for unmanaged types.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of elements in the vector, must be unmanaged.</typeparam>
|
||||
public unsafe struct ImVector<T> where T : unmanaged
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public unsafe struct ImVector<T> : IEnumerable<T>
|
||||
where T : unmanaged
|
||||
{
|
||||
private int size;
|
||||
private int capacity;
|
||||
private T* data;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImVector{T}"/> struct with the specified size, capacity, and data pointer.
|
||||
/// </summary>
|
||||
|
|
@ -45,11 +50,6 @@ public unsafe struct ImVector<T> where T : unmanaged
|
|||
this.data = data;
|
||||
}
|
||||
|
||||
private int size;
|
||||
private int capacity;
|
||||
private unsafe T* data;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the element at the specified index.
|
||||
/// </summary>
|
||||
|
|
@ -58,80 +58,72 @@ public unsafe struct ImVector<T> where T : unmanaged
|
|||
/// <exception cref="IndexOutOfRangeException">Thrown when the index is out of range.</exception>
|
||||
public T this[int index]
|
||||
{
|
||||
get
|
||||
readonly get
|
||||
{
|
||||
if (index < 0 || index >= size)
|
||||
{
|
||||
if (index < 0 || index >= this.size)
|
||||
throw new IndexOutOfRangeException();
|
||||
}
|
||||
return data[index];
|
||||
return this.data[index];
|
||||
}
|
||||
set
|
||||
{
|
||||
if (index < 0 || index >= size)
|
||||
{
|
||||
if (index < 0 || index >= this.size)
|
||||
throw new IndexOutOfRangeException();
|
||||
}
|
||||
data[index] = value;
|
||||
this.data[index] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a pointer to the first element of the vector.
|
||||
/// </summary>
|
||||
public readonly T* Data => data;
|
||||
public readonly T* Data => this.data;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a pointer to the first element of the vector.
|
||||
/// </summary>
|
||||
public readonly T* Front => data;
|
||||
public readonly T* Front => this.data;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a pointer to the last element of the vector.
|
||||
/// </summary>
|
||||
public readonly T* Back => size > 0 ? data + size - 1 : null;
|
||||
public readonly T* Back => this.size > 0 ? this.data + this.size - 1 : null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the capacity of the vector.
|
||||
/// </summary>
|
||||
public int Capacity
|
||||
{
|
||||
readonly get => capacity;
|
||||
readonly get => this.capacity;
|
||||
set
|
||||
{
|
||||
if (capacity == value)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(value, this.size, nameof(Capacity));
|
||||
if (this.capacity == value)
|
||||
return;
|
||||
}
|
||||
|
||||
if (data == null)
|
||||
if (this.data == null)
|
||||
{
|
||||
data = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T)));
|
||||
this.data = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T)));
|
||||
}
|
||||
else
|
||||
{
|
||||
int newSize = Math.Min(size, value);
|
||||
T* newData = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T)));
|
||||
Buffer.MemoryCopy(data, newData, (nuint)(value * sizeof(T)), (nuint)(newSize * sizeof(T)));
|
||||
ImGui.MemFree(data);
|
||||
data = newData;
|
||||
size = newSize;
|
||||
var newSize = Math.Min(this.size, value);
|
||||
var newData = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T)));
|
||||
Buffer.MemoryCopy(this.data, newData, (nuint)(value * sizeof(T)), (nuint)(newSize * sizeof(T)));
|
||||
ImGui.MemFree(this.data);
|
||||
this.data = newData;
|
||||
this.size = newSize;
|
||||
}
|
||||
|
||||
capacity = value;
|
||||
this.capacity = value;
|
||||
|
||||
// Clear the rest of the data
|
||||
for (int i = size; i < capacity; i++)
|
||||
{
|
||||
data[i] = default;
|
||||
}
|
||||
new Span<T>(this.data + this.size, this.capacity - this.size).Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of elements in the vector.
|
||||
/// </summary>
|
||||
public readonly int Size => size;
|
||||
public readonly int Size => this.size;
|
||||
|
||||
/// <summary>
|
||||
/// Grows the capacity of the vector to at least the specified value.
|
||||
|
|
@ -139,10 +131,8 @@ public unsafe struct ImVector<T> where T : unmanaged
|
|||
/// <param name="newCapacity">The new capacity.</param>
|
||||
public void Grow(int newCapacity)
|
||||
{
|
||||
if (newCapacity > capacity)
|
||||
{
|
||||
Capacity = newCapacity * 2;
|
||||
}
|
||||
var newCapacity2 = this.capacity > 0 ? this.capacity + (this.capacity / 2) : 8;
|
||||
this.Capacity = newCapacity2 > newCapacity ? newCapacity2 : newCapacity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -151,10 +141,8 @@ public unsafe struct ImVector<T> where T : unmanaged
|
|||
/// <param name="size">The minimum capacity required.</param>
|
||||
public void EnsureCapacity(int size)
|
||||
{
|
||||
if (size > capacity)
|
||||
{
|
||||
if (size > this.capacity)
|
||||
Grow(size);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -164,25 +152,46 @@ public unsafe struct ImVector<T> where T : unmanaged
|
|||
public void Resize(int newSize)
|
||||
{
|
||||
EnsureCapacity(newSize);
|
||||
size = newSize;
|
||||
this.size = newSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all elements from the vector.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
public void Clear() => this.size = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Adds an element to the end of the vector.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to add.</param>
|
||||
[OverloadResolutionPriority(1)]
|
||||
public void PushBack(T value)
|
||||
{
|
||||
size = 0;
|
||||
this.EnsureCapacity(this.size + 1);
|
||||
this.data[this.size++] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an element to the end of the vector.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to add.</param>
|
||||
public void PushBack(T value)
|
||||
[OverloadResolutionPriority(2)]
|
||||
public void PushBack(in T value)
|
||||
{
|
||||
EnsureCapacity(size + 1);
|
||||
data[size++] = value;
|
||||
EnsureCapacity(this.size + 1);
|
||||
this.data[this.size++] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an element to the front of the vector.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to add.</param>
|
||||
public void PushFront(in T value)
|
||||
{
|
||||
if (this.size == 0)
|
||||
this.PushBack(value);
|
||||
else
|
||||
this.Insert(0, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -190,48 +199,126 @@ public unsafe struct ImVector<T> where T : unmanaged
|
|||
/// </summary>
|
||||
public void PopBack()
|
||||
{
|
||||
if (size > 0)
|
||||
if (this.size > 0)
|
||||
{
|
||||
size--;
|
||||
this.size--;
|
||||
}
|
||||
}
|
||||
|
||||
public ref T Insert(int index, in T v) {
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(index, nameof(index));
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, this.size, nameof(index));
|
||||
this.EnsureCapacity(this.size + 1);
|
||||
if (index < this.size)
|
||||
{
|
||||
Buffer.MemoryCopy(
|
||||
this.data + index,
|
||||
this.data + index + 1,
|
||||
(this.size - index) * sizeof(T),
|
||||
(this.size - index) * sizeof(T));
|
||||
}
|
||||
|
||||
this.data[index] = v;
|
||||
this.size++;
|
||||
return ref this.data[index];
|
||||
}
|
||||
|
||||
public Span<T> InsertRange(int index, ReadOnlySpan<T> v)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(index, nameof(index));
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, this.size, nameof(index));
|
||||
this.EnsureCapacity(this.size + v.Length);
|
||||
if (index < this.size)
|
||||
{
|
||||
Buffer.MemoryCopy(
|
||||
this.data + index,
|
||||
this.data + index + v.Length,
|
||||
(this.size - index) * sizeof(T),
|
||||
(this.size - index) * sizeof(T));
|
||||
}
|
||||
|
||||
var dstSpan = new Span<T>(this.data + index, v.Length);
|
||||
v.CopyTo(new(this.data + index, v.Length));
|
||||
this.size += v.Length;
|
||||
return dstSpan;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frees the memory allocated for the vector.
|
||||
/// </summary>
|
||||
public void Free()
|
||||
{
|
||||
if (data != null)
|
||||
if (this.data != null)
|
||||
{
|
||||
ImGui.MemFree(data);
|
||||
data = null;
|
||||
size = 0;
|
||||
capacity = 0;
|
||||
ImGui.MemFree(this.data);
|
||||
this.data = null;
|
||||
this.size = 0;
|
||||
this.capacity = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public ref T Ref(int index)
|
||||
public readonly ref T Ref(int index)
|
||||
{
|
||||
return ref Unsafe.AsRef<T>((byte*)Data + index * Unsafe.SizeOf<T>());
|
||||
return ref Unsafe.AsRef<T>((byte*)Data + (index * Unsafe.SizeOf<T>()));
|
||||
}
|
||||
|
||||
public ref TCast Ref<TCast>(int index)
|
||||
public readonly ref TCast Ref<TCast>(int index)
|
||||
{
|
||||
return ref Unsafe.AsRef<TCast>((byte*)Data + index * Unsafe.SizeOf<TCast>());
|
||||
return ref Unsafe.AsRef<TCast>((byte*)Data + (index * Unsafe.SizeOf<TCast>()));
|
||||
}
|
||||
|
||||
public void* Address(int index)
|
||||
public readonly void* Address(int index)
|
||||
{
|
||||
return (byte*)Data + index * Unsafe.SizeOf<T>();
|
||||
return (byte*)Data + (index * Unsafe.SizeOf<T>());
|
||||
}
|
||||
|
||||
public void* Address<TCast>(int index)
|
||||
public readonly void* Address<TCast>(int index)
|
||||
{
|
||||
return (byte*)Data + index * Unsafe.SizeOf<TCast>();
|
||||
return (byte*)Data + (index * Unsafe.SizeOf<TCast>());
|
||||
}
|
||||
|
||||
public ImVector* ToUntyped()
|
||||
public readonly ImVector* ToUntyped()
|
||||
{
|
||||
return (ImVector*)Unsafe.AsPointer(ref this);
|
||||
return (ImVector*)Unsafe.AsPointer(ref Unsafe.AsRef(in this));
|
||||
}
|
||||
|
||||
public readonly Span<T> AsSpan() => new(this.data, this.size);
|
||||
|
||||
public readonly Enumerator GetEnumerator() => new(this.data, this.data + this.size);
|
||||
|
||||
readonly IEnumerator<T> IEnumerable<T>.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
public struct Enumerator(T* begin, T* end) : IEnumerator<T>, IEnumerable<T>
|
||||
{
|
||||
private T* current = null;
|
||||
|
||||
public readonly ref T Current => ref *this.current;
|
||||
|
||||
readonly T IEnumerator<T>.Current => this.Current;
|
||||
|
||||
readonly object IEnumerator.Current => this.Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
var next = this.current == null ? begin : this.current + 1;
|
||||
if (next == end)
|
||||
return false;
|
||||
this.current = next;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset() => this.current = null;
|
||||
|
||||
public readonly Enumerator GetEnumerator() => new(begin, end);
|
||||
|
||||
readonly void IDisposable.Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
readonly IEnumerator<T> IEnumerable<T>.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
<PackageReference Include="Veldrid" Version="4.9.0" />
|
||||
<PackageReference Include="Veldrid.SDL2" Version="4.9.0" />
|
||||
<PackageReference Include="Veldrid.StartupUtilities" Version="4.9.0" />
|
||||
<PackageReference Remove="Microsoft.CodeAnalysis.BannedApiAnalyzers"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit e5f586630ef06fa48d5dc0d8c0fa679323093c77
|
||||
Subproject commit e5dedba42a3fea8f050ea54ac583a5874bf51c6f
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 27c8565f631b004c3266373890e41ecc627f775b
|
||||
Subproject commit bc327296758d57d3bdc963cb6ce71dd5b0c7e54c
|
||||
Loading…
Add table
Add a link
Reference in a new issue