mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-01-03 14:23:40 +01:00
Merge pull request #1641 from goatcorp/net8-rollup
[net8] Rollup changes from master
This commit is contained in:
commit
324806341c
99 changed files with 6308 additions and 1218 deletions
|
|
@ -32,6 +32,9 @@
|
|||
<IntDir>obj\$(Configuration)\</IntDir>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
<Import Project="$(VCTargetsPath)\BuildCustomizations\masm.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<LibraryPath>$(SolutionDir)bin\lib\$(Configuration)\libMinHook\;$(VC_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64)</LibraryPath>
|
||||
|
|
@ -55,7 +58,7 @@
|
|||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<EnableUAC>false</EnableUAC>
|
||||
<AdditionalDependencies>Version.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>Version.lib;Shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>..\lib\CoreCLR;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
|
|
@ -70,6 +73,7 @@
|
|||
<Link>
|
||||
<EnableCOMDATFolding>false</EnableCOMDATFolding>
|
||||
<OptimizeReferences>false</OptimizeReferences>
|
||||
<ModuleDefinitionFile Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">module.def</ModuleDefinitionFile>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
|
|
@ -83,9 +87,13 @@
|
|||
<Link>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<ModuleDefinitionFile Condition="'$(Configuration)|$(Platform)'=='Release|x64'">module.def</ModuleDefinitionFile>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
<Import Project="$(VCTargetsPath)\BuildCustomizations\masm.targets" />
|
||||
</ImportGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="..\lib\CoreCLR\nethost\nethost.dll">
|
||||
<Link>nethost.dll</Link>
|
||||
|
|
@ -129,6 +137,7 @@
|
|||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ntdll.cpp" />
|
||||
<ClCompile Include="unicode.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
|
|
@ -168,6 +177,7 @@
|
|||
<ClInclude Include="DalamudStartInfo.h" />
|
||||
<ClInclude Include="hooks.h" />
|
||||
<ClInclude Include="logging.h" />
|
||||
<ClInclude Include="ntdll.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
<ClInclude Include="unicode.h" />
|
||||
<ClInclude Include="utils.h" />
|
||||
|
|
@ -181,8 +191,14 @@
|
|||
<ItemGroup>
|
||||
<Image Include="dalamud.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<MASM Include="rewrite_entrypoint_thunks.asm" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="module.def" />
|
||||
</ItemGroup>
|
||||
<Target Name="RemoveExtraFiles" AfterTargets="PostBuildEvent">
|
||||
<Delete Files="$(OutDir)$(TargetName).lib" />
|
||||
<Delete Files="$(OutDir)$(TargetName).exp" />
|
||||
</Target>
|
||||
</Project>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -73,6 +73,9 @@
|
|||
<ClCompile Include="DalamudStartInfo.cpp">
|
||||
<Filter>Dalamud.Boot DLL</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ntdll.cpp">
|
||||
<Filter>Dalamud.Boot DLL</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\lib\CoreCLR\CoreCLR.h">
|
||||
|
|
@ -140,6 +143,9 @@
|
|||
</ClInclude>
|
||||
<ClInclude Include="resource.h" />
|
||||
<ClInclude Include="crashhandler_shared.h" />
|
||||
<ClInclude Include="ntdll.h">
|
||||
<Filter>Dalamud.Boot DLL</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="Dalamud.Boot.rc" />
|
||||
|
|
@ -147,4 +153,14 @@
|
|||
<ItemGroup>
|
||||
<Image Include="dalamud.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<MASM Include="rewrite_entrypoint_thunks.asm">
|
||||
<Filter>Dalamud.Boot DLL</Filter>
|
||||
</MASM>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="module.def">
|
||||
<Filter>Dalamud.Boot DLL</Filter>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -89,13 +89,16 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
|
|||
config.DalamudLoadMethod = json.value("LoadMethod", config.DalamudLoadMethod);
|
||||
config.WorkingDirectory = json.value("WorkingDirectory", config.WorkingDirectory);
|
||||
config.ConfigurationPath = json.value("ConfigurationPath", config.ConfigurationPath);
|
||||
config.LogPath = json.value("LogPath", config.LogPath);
|
||||
config.LogName = json.value("LogName", config.LogName);
|
||||
config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory);
|
||||
config.DefaultPluginDirectory = json.value("DefaultPluginDirectory", config.DefaultPluginDirectory);
|
||||
config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory);
|
||||
config.Language = json.value("Language", config.Language);
|
||||
config.GameVersion = json.value("GameVersion", config.GameVersion);
|
||||
config.DelayInitializeMs = json.value("DelayInitializeMs", config.DelayInitializeMs);
|
||||
config.TroubleshootingPackData = json.value("TroubleshootingPackData", std::string{});
|
||||
config.DelayInitializeMs = json.value("DelayInitializeMs", config.DelayInitializeMs);
|
||||
config.NoLoadPlugins = json.value("NoLoadPlugins", config.NoLoadPlugins);
|
||||
config.NoLoadThirdPartyPlugins = json.value("NoLoadThirdPartyPlugins", config.NoLoadThirdPartyPlugins);
|
||||
|
||||
config.BootLogPath = json.value("BootLogPath", config.BootLogPath);
|
||||
config.BootShowConsole = json.value("BootShowConsole", config.BootShowConsole);
|
||||
|
|
|
|||
|
|
@ -35,13 +35,16 @@ struct DalamudStartInfo {
|
|||
LoadMethod DalamudLoadMethod = LoadMethod::Entrypoint;
|
||||
std::string WorkingDirectory;
|
||||
std::string ConfigurationPath;
|
||||
std::string LogPath;
|
||||
std::string LogName;
|
||||
std::string PluginDirectory;
|
||||
std::string DefaultPluginDirectory;
|
||||
std::string AssetDirectory;
|
||||
ClientLanguage Language = ClientLanguage::English;
|
||||
std::string GameVersion;
|
||||
int DelayInitializeMs = 0;
|
||||
std::string TroubleshootingPackData;
|
||||
int DelayInitializeMs = 0;
|
||||
bool NoLoadPlugins;
|
||||
bool NoLoadThirdPartyPlugins;
|
||||
|
||||
std::string BootLogPath;
|
||||
bool BootShowConsole = false;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
HMODULE g_hModule;
|
||||
HINSTANCE g_hGameInstance = GetModuleHandleW(nullptr);
|
||||
|
||||
DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
|
||||
HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
|
||||
g_startInfo.from_envvars();
|
||||
|
||||
std::string jsonParseError;
|
||||
|
|
@ -114,7 +114,7 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
|
|||
logging::I("Calling InitializeClrAndGetEntryPoint");
|
||||
|
||||
void* entrypoint_vfn;
|
||||
int result = InitializeClrAndGetEntryPoint(
|
||||
const auto result = InitializeClrAndGetEntryPoint(
|
||||
g_hModule,
|
||||
g_startInfo.BootEnableEtw,
|
||||
runtimeconfig_path,
|
||||
|
|
@ -124,7 +124,7 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
|
|||
L"Dalamud.EntryPoint+InitDelegate, Dalamud",
|
||||
&entrypoint_vfn);
|
||||
|
||||
if (result != 0)
|
||||
if (FAILED(result))
|
||||
return result;
|
||||
|
||||
using custom_component_entry_point_fn = void (CORECLR_DELEGATE_CALLTYPE*)(LPVOID, HANDLE);
|
||||
|
|
@ -156,10 +156,10 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
|
|||
entrypoint_fn(lpParam, hMainThreadContinue);
|
||||
logging::I("Done!");
|
||||
|
||||
return 0;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
DllExport DWORD WINAPI Initialize(LPVOID lpParam) {
|
||||
extern "C" DWORD WINAPI Initialize(LPVOID lpParam) {
|
||||
return InitializeImpl(lpParam, CreateEvent(nullptr, TRUE, FALSE, nullptr));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,39 +2,9 @@
|
|||
|
||||
#include "hooks.h"
|
||||
|
||||
#include "ntdll.h"
|
||||
#include "logging.h"
|
||||
|
||||
enum {
|
||||
LDR_DLL_NOTIFICATION_REASON_LOADED = 1,
|
||||
LDR_DLL_NOTIFICATION_REASON_UNLOADED = 2,
|
||||
};
|
||||
|
||||
struct LDR_DLL_UNLOADED_NOTIFICATION_DATA {
|
||||
ULONG Flags; //Reserved.
|
||||
const UNICODE_STRING* FullDllName; //The full path name of the DLL module.
|
||||
const UNICODE_STRING* BaseDllName; //The base file name of the DLL module.
|
||||
PVOID DllBase; //A pointer to the base address for the DLL in memory.
|
||||
ULONG SizeOfImage; //The size of the DLL image, in bytes.
|
||||
};
|
||||
|
||||
struct LDR_DLL_LOADED_NOTIFICATION_DATA {
|
||||
ULONG Flags; //Reserved.
|
||||
const UNICODE_STRING* FullDllName; //The full path name of the DLL module.
|
||||
const UNICODE_STRING* BaseDllName; //The base file name of the DLL module.
|
||||
PVOID DllBase; //A pointer to the base address for the DLL in memory.
|
||||
ULONG SizeOfImage; //The size of the DLL image, in bytes.
|
||||
};
|
||||
|
||||
union LDR_DLL_NOTIFICATION_DATA {
|
||||
LDR_DLL_LOADED_NOTIFICATION_DATA Loaded;
|
||||
LDR_DLL_UNLOADED_NOTIFICATION_DATA Unloaded;
|
||||
};
|
||||
|
||||
using PLDR_DLL_NOTIFICATION_FUNCTION = VOID CALLBACK(_In_ ULONG NotificationReason, _In_ const LDR_DLL_NOTIFICATION_DATA* NotificationData, _In_opt_ PVOID Context);
|
||||
|
||||
static const auto LdrRegisterDllNotification = utils::loaded_module(GetModuleHandleW(L"ntdll.dll")).get_exported_function<NTSTATUS(NTAPI)(ULONG Flags, PLDR_DLL_NOTIFICATION_FUNCTION NotificationFunction, PVOID Context, PVOID* Cookie)>("LdrRegisterDllNotification");
|
||||
static const auto LdrUnregisterDllNotification = utils::loaded_module(GetModuleHandleW(L"ntdll.dll")).get_exported_function<NTSTATUS(NTAPI)(PVOID Cookie)>("LdrUnregisterDllNotification");
|
||||
|
||||
hooks::getprocaddress_singleton_import_hook::getprocaddress_singleton_import_hook()
|
||||
: m_pfnGetProcAddress(GetProcAddress)
|
||||
, m_thunk("kernel32!GetProcAddress(Singleton Import Hook)",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include <limits>
|
||||
#include <map>
|
||||
|
||||
#include "utils.h"
|
||||
|
|
|
|||
5
Dalamud.Boot/module.def
Normal file
5
Dalamud.Boot/module.def
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
LIBRARY Dalamud.Boot
|
||||
EXPORTS
|
||||
Initialize @1
|
||||
RewriteRemoteEntryPointW @2
|
||||
RewrittenEntryPoint @3
|
||||
15
Dalamud.Boot/ntdll.cpp
Normal file
15
Dalamud.Boot/ntdll.cpp
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
#include "pch.h"
|
||||
|
||||
#include "ntdll.h"
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
NTSTATUS LdrRegisterDllNotification(ULONG Flags, PLDR_DLL_NOTIFICATION_FUNCTION NotificationFunction, PVOID Context, PVOID* Cookie) {
|
||||
static const auto pfn = utils::loaded_module(GetModuleHandleW(L"ntdll.dll")).get_exported_function<NTSTATUS(NTAPI)(ULONG Flags, PLDR_DLL_NOTIFICATION_FUNCTION NotificationFunction, PVOID Context, PVOID* Cookie)>("LdrRegisterDllNotification");
|
||||
return pfn(Flags, NotificationFunction, Context, Cookie);
|
||||
}
|
||||
|
||||
NTSTATUS LdrUnregisterDllNotification(PVOID Cookie) {
|
||||
static const auto pfn = utils::loaded_module(GetModuleHandleW(L"ntdll.dll")).get_exported_function<NTSTATUS(NTAPI)(PVOID Cookie)>("LdrUnregisterDllNotification");
|
||||
return pfn(Cookie);
|
||||
}
|
||||
33
Dalamud.Boot/ntdll.h
Normal file
33
Dalamud.Boot/ntdll.h
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#pragma once
|
||||
|
||||
// ntdll exports
|
||||
enum {
|
||||
LDR_DLL_NOTIFICATION_REASON_LOADED = 1,
|
||||
LDR_DLL_NOTIFICATION_REASON_UNLOADED = 2,
|
||||
};
|
||||
|
||||
struct LDR_DLL_UNLOADED_NOTIFICATION_DATA {
|
||||
ULONG Flags; //Reserved.
|
||||
const UNICODE_STRING* FullDllName; //The full path name of the DLL module.
|
||||
const UNICODE_STRING* BaseDllName; //The base file name of the DLL module.
|
||||
PVOID DllBase; //A pointer to the base address for the DLL in memory.
|
||||
ULONG SizeOfImage; //The size of the DLL image, in bytes.
|
||||
};
|
||||
|
||||
struct LDR_DLL_LOADED_NOTIFICATION_DATA {
|
||||
ULONG Flags; //Reserved.
|
||||
const UNICODE_STRING* FullDllName; //The full path name of the DLL module.
|
||||
const UNICODE_STRING* BaseDllName; //The base file name of the DLL module.
|
||||
PVOID DllBase; //A pointer to the base address for the DLL in memory.
|
||||
ULONG SizeOfImage; //The size of the DLL image, in bytes.
|
||||
};
|
||||
|
||||
union LDR_DLL_NOTIFICATION_DATA {
|
||||
LDR_DLL_LOADED_NOTIFICATION_DATA Loaded;
|
||||
LDR_DLL_UNLOADED_NOTIFICATION_DATA Unloaded;
|
||||
};
|
||||
|
||||
using PLDR_DLL_NOTIFICATION_FUNCTION = VOID CALLBACK(_In_ ULONG NotificationReason, _In_ const LDR_DLL_NOTIFICATION_DATA* NotificationData, _In_opt_ PVOID Context);
|
||||
|
||||
NTSTATUS LdrRegisterDllNotification(ULONG Flags, PLDR_DLL_NOTIFICATION_FUNCTION NotificationFunction, PVOID Context, PVOID* Cookie);
|
||||
NTSTATUS LdrUnregisterDllNotification(PVOID Cookie);
|
||||
|
|
@ -15,18 +15,28 @@
|
|||
#include <Windows.h>
|
||||
|
||||
// Windows Header Files (2)
|
||||
#include <DbgHelp.h>
|
||||
#include <Dbt.h>
|
||||
#include <dwmapi.h>
|
||||
#include <iphlpapi.h>
|
||||
#include <PathCch.h>
|
||||
#include <Psapi.h>
|
||||
#include <ShlObj.h>
|
||||
#include <Shlwapi.h>
|
||||
#include <SubAuth.h>
|
||||
#include <TlHelp32.h>
|
||||
|
||||
// Windows Header Files (3)
|
||||
#include <icmpapi.h> // Must be loaded after iphlpapi.h
|
||||
|
||||
// MSVC Compiler Intrinsic
|
||||
#include <intrin.h>
|
||||
|
||||
// COM
|
||||
#include <comdef.h>
|
||||
|
||||
// C++ Standard Libraries
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
|
|
@ -61,9 +71,6 @@
|
|||
|
||||
#include "unicode.h"
|
||||
|
||||
// Commonly used macros
|
||||
#define DllExport extern "C" __declspec(dllexport)
|
||||
|
||||
// Global variables
|
||||
extern HMODULE g_hModule;
|
||||
extern HINSTANCE g_hGameInstance;
|
||||
|
|
|
|||
|
|
@ -1,115 +1,92 @@
|
|||
#include "pch.h"
|
||||
|
||||
#include "logging.h"
|
||||
#include "utils.h"
|
||||
|
||||
DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue);
|
||||
HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue);
|
||||
|
||||
struct RewrittenEntryPointParameters {
|
||||
void* pAllocation;
|
||||
char* pEntrypoint;
|
||||
char* pEntrypointBytes;
|
||||
size_t entrypointLength;
|
||||
char* pLoadInfo;
|
||||
HANDLE hMainThread;
|
||||
HANDLE hMainThreadContinue;
|
||||
};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct EntryPointThunkTemplate {
|
||||
struct DUMMYSTRUCTNAME {
|
||||
struct {
|
||||
const uint8_t op_mov_rdi[2]{ 0x48, 0xbf };
|
||||
void* ptr = nullptr;
|
||||
} fn;
|
||||
namespace thunks {
|
||||
constexpr uint64_t Terminator = 0xCCCCCCCCCCCCCCCCu;
|
||||
constexpr uint64_t Placeholder = 0x0606060606060606u;
|
||||
|
||||
extern "C" void EntryPointReplacement();
|
||||
extern "C" void RewrittenEntryPoint_Standalone();
|
||||
|
||||
const uint8_t op_call_rdi[2]{ 0xff, 0xd7 };
|
||||
} CallTrampoline;
|
||||
};
|
||||
void* resolve_thunk_address(void (*pfn)()) {
|
||||
const auto ptr = reinterpret_cast<uint8_t*>(pfn);
|
||||
if (*ptr == 0xe9)
|
||||
return ptr + 5 + *reinterpret_cast<int32_t*>(ptr + 1);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
struct TrampolineTemplate {
|
||||
const struct {
|
||||
const uint8_t op_sub_rsp_imm[3]{ 0x48, 0x81, 0xec };
|
||||
const uint32_t length = 0x80;
|
||||
} stack_alloc;
|
||||
size_t get_thunk_length(void (*pfn)()) {
|
||||
size_t length = 0;
|
||||
for (auto ptr = reinterpret_cast<char*>(resolve_thunk_address(pfn)); *reinterpret_cast<uint64_t*>(ptr) != Terminator; ptr++)
|
||||
length++;
|
||||
return length;
|
||||
}
|
||||
|
||||
struct DUMMYSTRUCTNAME {
|
||||
struct {
|
||||
const uint8_t op_mov_rcx_imm[2]{ 0x48, 0xb9 };
|
||||
void* val = nullptr;
|
||||
} lpLibFileName;
|
||||
template<typename T>
|
||||
void* fill_placeholders(void* pfn, const T& value) {
|
||||
auto ptr = static_cast<char*>(pfn);
|
||||
|
||||
struct {
|
||||
const uint8_t op_mov_rdi_imm[2]{ 0x48, 0xbf };
|
||||
decltype(&LoadLibraryW) ptr = nullptr;
|
||||
} fn;
|
||||
while (*reinterpret_cast<uint64_t*>(ptr) != Placeholder)
|
||||
ptr++;
|
||||
|
||||
const uint8_t op_call_rdi[2]{ 0xff, 0xd7 };
|
||||
} CallLoadLibrary_nethost;
|
||||
*reinterpret_cast<uint64_t*>(ptr) = 0;
|
||||
*reinterpret_cast<T*>(ptr) = value;
|
||||
return ptr + sizeof(value);
|
||||
}
|
||||
|
||||
struct DUMMYSTRUCTNAME {
|
||||
struct {
|
||||
const uint8_t op_mov_rcx_imm[2]{ 0x48, 0xb9 };
|
||||
void* val = nullptr;
|
||||
} lpLibFileName;
|
||||
template<typename T, typename...TArgs>
|
||||
void* fill_placeholders(void* ptr, const T& value, TArgs&&...more_values) {
|
||||
return fill_placeholders(fill_placeholders(ptr, value), std::forward<TArgs>(more_values)...);
|
||||
}
|
||||
|
||||
struct {
|
||||
const uint8_t op_mov_rdi_imm[2]{ 0x48, 0xbf };
|
||||
decltype(&LoadLibraryW) ptr = nullptr;
|
||||
} fn;
|
||||
std::vector<char> create_entrypointreplacement() {
|
||||
std::vector<char> buf(get_thunk_length(&EntryPointReplacement));
|
||||
memcpy(buf.data(), resolve_thunk_address(&EntryPointReplacement), buf.size());
|
||||
return buf;
|
||||
}
|
||||
|
||||
const uint8_t op_call_rdi[2]{ 0xff, 0xd7 };
|
||||
} CallLoadLibrary_DalamudBoot;
|
||||
std::vector<char> create_standalone_rewrittenentrypoint(const std::filesystem::path& dalamud_path) {
|
||||
const auto nethost_path = std::filesystem::path(dalamud_path).replace_filename(L"nethost.dll");
|
||||
|
||||
struct {
|
||||
const uint8_t hModule_op_mov_rcx_rax[3]{ 0x48, 0x89, 0xc1 };
|
||||
// These are null terminated, since pointers are returned from .c_str()
|
||||
const auto dalamud_path_wview = std::wstring_view(dalamud_path.c_str());
|
||||
const auto nethost_path_wview = std::wstring_view(nethost_path.c_str());
|
||||
|
||||
struct {
|
||||
const uint8_t op_mov_rdx_imm[2]{ 0x48, 0xba };
|
||||
void* val = nullptr;
|
||||
} lpProcName;
|
||||
// +2 is for null terminator
|
||||
const auto dalamud_path_view = std::span(reinterpret_cast<const char*>(dalamud_path_wview.data()), dalamud_path_wview.size() * 2 + 2);
|
||||
const auto nethost_path_view = std::span(reinterpret_cast<const char*>(nethost_path_wview.data()), nethost_path_wview.size() * 2 + 2);
|
||||
|
||||
struct {
|
||||
const uint8_t op_mov_rdi_imm[2]{ 0x48, 0xbf };
|
||||
decltype(&GetProcAddress) ptr = nullptr;
|
||||
} fn;
|
||||
std::vector<char> buffer;
|
||||
const auto thunk_template_length = thunks::get_thunk_length(&thunks::RewrittenEntryPoint_Standalone);
|
||||
buffer.reserve(thunk_template_length + dalamud_path_view.size() + nethost_path_view.size());
|
||||
buffer.resize(thunk_template_length);
|
||||
memcpy(buffer.data(), resolve_thunk_address(&thunks::RewrittenEntryPoint_Standalone), thunk_template_length);
|
||||
|
||||
const uint8_t op_call_rdi[2]{ 0xff, 0xd7 };
|
||||
} CallGetProcAddress;
|
||||
// &::GetProcAddress will return Dalamud.dll's import table entry.
|
||||
// GetProcAddress(..., "GetProcAddress") returns the address inside kernel32.dll.
|
||||
const auto kernel32 = GetModuleHandleA("kernel32.dll");
|
||||
|
||||
struct {
|
||||
const uint8_t op_add_rsp_imm[3]{ 0x48, 0x81, 0xc4 };
|
||||
const uint32_t length = 0x80;
|
||||
} stack_release;
|
||||
|
||||
struct DUMMYSTRUCTNAME2 {
|
||||
// rdi := returned value from GetProcAddress
|
||||
const uint8_t op_mov_rdi_rax[3]{ 0x48, 0x89, 0xc7 };
|
||||
// rax := return address
|
||||
const uint8_t op_pop_rax[1]{ 0x58 };
|
||||
|
||||
// rax := rax - sizeof thunk (last instruction must be call)
|
||||
struct {
|
||||
const uint8_t op_sub_rax_imm4[2]{ 0x48, 0x2d };
|
||||
const uint32_t displacement = static_cast<uint32_t>(sizeof EntryPointThunkTemplate);
|
||||
} op_sub_rax_to_entry_point;
|
||||
|
||||
struct {
|
||||
const uint8_t op_mov_rcx_imm[2]{ 0x48, 0xb9 };
|
||||
void* val = nullptr;
|
||||
} param;
|
||||
|
||||
const uint8_t op_push_rax[1]{ 0x50 };
|
||||
const uint8_t op_jmp_rdi[2]{ 0xff, 0xe7 };
|
||||
} CallInjectEntryPoint;
|
||||
|
||||
const char buf_CallGetProcAddress_lpProcName[20] = "RewrittenEntryPoint";
|
||||
uint8_t buf_EntryPointBackup[sizeof EntryPointThunkTemplate]{};
|
||||
|
||||
#pragma pack(push, 8)
|
||||
RewrittenEntryPointParameters parameters{};
|
||||
#pragma pack(pop)
|
||||
};
|
||||
#pragma pack(pop)
|
||||
thunks::fill_placeholders(buffer.data(),
|
||||
/* pfnLoadLibraryW = */ GetProcAddress(kernel32, "LoadLibraryW"),
|
||||
/* pfnGetProcAddress = */ GetProcAddress(kernel32, "GetProcAddress"),
|
||||
/* pRewrittenEntryPointParameters = */ Placeholder,
|
||||
/* nNethostOffset = */ 0,
|
||||
/* nDalamudOffset = */ nethost_path_view.size_bytes()
|
||||
);
|
||||
buffer.insert(buffer.end(), nethost_path_view.begin(), nethost_path_view.end());
|
||||
buffer.insert(buffer.end(), dalamud_path_view.begin(), dalamud_path_view.end());
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
void read_process_memory_or_throw(HANDLE hProcess, void* pAddress, void* data, size_t len) {
|
||||
SIZE_T read = 0;
|
||||
|
|
@ -126,6 +103,7 @@ void read_process_memory_or_throw(HANDLE hProcess, void* pAddress, T& data) {
|
|||
|
||||
void write_process_memory_or_throw(HANDLE hProcess, void* pAddress, const void* data, size_t len) {
|
||||
SIZE_T written = 0;
|
||||
const utils::memory_tenderizer tenderizer(hProcess, pAddress, len, PAGE_EXECUTE_READWRITE);
|
||||
if (!WriteProcessMemory(hProcess, pAddress, data, len, &written))
|
||||
throw std::runtime_error("WriteProcessMemory failure");
|
||||
if (written != len)
|
||||
|
|
@ -170,10 +148,17 @@ void* get_mapped_image_base_address(HANDLE hProcess, const std::filesystem::path
|
|||
exe.read(reinterpret_cast<char*>(&exe_section_headers[0]), sizeof IMAGE_SECTION_HEADER * exe_section_headers.size());
|
||||
if (!exe)
|
||||
throw std::runtime_error("Game executable is corrupt (Truncated section header).");
|
||||
|
||||
SYSTEM_INFO sysinfo;
|
||||
GetSystemInfo(&sysinfo);
|
||||
|
||||
for (MEMORY_BASIC_INFORMATION mbi{};
|
||||
VirtualQueryEx(hProcess, mbi.BaseAddress, &mbi, sizeof mbi);
|
||||
mbi.BaseAddress = static_cast<char*>(mbi.BaseAddress) + mbi.RegionSize) {
|
||||
|
||||
// wine: apparently there exists a RegionSize of 0xFFF
|
||||
mbi.RegionSize = (mbi.RegionSize + sysinfo.dwPageSize - 1) / sysinfo.dwPageSize * sysinfo.dwPageSize;
|
||||
|
||||
if (!(mbi.State & MEM_COMMIT) || mbi.Type != MEM_IMAGE)
|
||||
continue;
|
||||
|
||||
|
|
@ -241,31 +226,22 @@ void* get_mapped_image_base_address(HANDLE hProcess, const std::filesystem::path
|
|||
throw std::runtime_error("corresponding base address not found");
|
||||
}
|
||||
|
||||
std::string from_utf16(const std::wstring& wstr, UINT codePage = CP_UTF8) {
|
||||
std::string str(WideCharToMultiByte(codePage, 0, &wstr[0], static_cast<int>(wstr.size()), nullptr, 0, nullptr, nullptr), 0);
|
||||
WideCharToMultiByte(codePage, 0, &wstr[0], static_cast<int>(wstr.size()), &str[0], static_cast<int>(str.size()), nullptr, nullptr);
|
||||
return str;
|
||||
}
|
||||
|
||||
std::wstring to_utf16(const std::string& str, UINT codePage = CP_UTF8, bool errorOnInvalidChars = false) {
|
||||
std::wstring wstr(MultiByteToWideChar(codePage, 0, &str[0], static_cast<int>(str.size()), nullptr, 0), 0);
|
||||
MultiByteToWideChar(codePage, errorOnInvalidChars ? MB_ERR_INVALID_CHARS : 0, &str[0], static_cast<int>(str.size()), &wstr[0], static_cast<int>(wstr.size()));
|
||||
return wstr;
|
||||
}
|
||||
|
||||
/// @brief Rewrite target process' entry point so that this DLL can be loaded and executed first.
|
||||
/// @param hProcess Process handle.
|
||||
/// @param pcwzPath Path to target process.
|
||||
/// @param pcszLoadInfo JSON string to be passed to Initialize.
|
||||
/// @return 0 if successful; nonzero if unsuccessful
|
||||
/// @param pcwzLoadInfo JSON string to be passed to Initialize.
|
||||
/// @return null if successful; memory containing wide string allocated via GlobalAlloc if unsuccessful
|
||||
///
|
||||
/// When the process has just been started up via CreateProcess (CREATE_SUSPENDED), GetModuleFileName and alikes result in an error.
|
||||
/// Instead, we have to enumerate through all the files mapped into target process' virtual address space and find the base address
|
||||
/// of memory region corresponding to the path given.
|
||||
///
|
||||
DllExport DWORD WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath, const wchar_t* pcwzLoadInfo) {
|
||||
extern "C" HRESULT WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath, const wchar_t* pcwzLoadInfo) {
|
||||
std::wstring last_operation;
|
||||
SetLastError(ERROR_SUCCESS);
|
||||
try {
|
||||
const auto base_address = reinterpret_cast<char*>(get_mapped_image_base_address(hProcess, pcwzPath));
|
||||
last_operation = L"get_mapped_image_base_address";
|
||||
const auto base_address = static_cast<char*>(get_mapped_image_base_address(hProcess, pcwzPath));
|
||||
|
||||
IMAGE_DOS_HEADER dos_header{};
|
||||
union {
|
||||
|
|
@ -273,112 +249,150 @@ DllExport DWORD WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t*
|
|||
IMAGE_NT_HEADERS64 nt_header64{};
|
||||
};
|
||||
|
||||
last_operation = L"read_process_memory_or_throw(base_address)";
|
||||
read_process_memory_or_throw(hProcess, base_address, dos_header);
|
||||
|
||||
last_operation = L"read_process_memory_or_throw(base_address + dos_header.e_lfanew)";
|
||||
read_process_memory_or_throw(hProcess, base_address + dos_header.e_lfanew, nt_header64);
|
||||
const auto entrypoint = base_address + (nt_header32.OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC
|
||||
? nt_header32.OptionalHeader.AddressOfEntryPoint
|
||||
: nt_header64.OptionalHeader.AddressOfEntryPoint);
|
||||
|
||||
auto path = get_path_from_local_module(g_hModule).wstring();
|
||||
path.resize(path.size() + 1); // ensure null termination
|
||||
auto path_bytes = std::span(reinterpret_cast<const char*>(&path[0]), std::span(path).size_bytes());
|
||||
last_operation = L"get_path_from_local_module(g_hModule)";
|
||||
auto local_module_path = get_path_from_local_module(g_hModule);
|
||||
|
||||
last_operation = L"thunks::create_standalone_rewrittenentrypoint(local_module_path)";
|
||||
auto standalone_rewrittenentrypoint = thunks::create_standalone_rewrittenentrypoint(local_module_path);
|
||||
|
||||
auto nethost_path = (get_path_from_local_module(g_hModule).parent_path() / L"nethost.dll").wstring();
|
||||
nethost_path.resize(nethost_path.size() + 1); // ensure null termination
|
||||
auto nethost_path_bytes = std::span(reinterpret_cast<const char*>(&nethost_path[0]), std::span(nethost_path).size_bytes());
|
||||
last_operation = L"thunks::create_entrypointreplacement()";
|
||||
auto entrypoint_replacement = thunks::create_entrypointreplacement();
|
||||
|
||||
auto load_info = from_utf16(pcwzLoadInfo);
|
||||
last_operation = L"unicode::convert<std::string>(pcwzLoadInfo)";
|
||||
auto load_info = unicode::convert<std::string>(pcwzLoadInfo);
|
||||
load_info.resize(load_info.size() + 1); //ensure null termination
|
||||
|
||||
// Allocate full buffer in advance to keep reference to trampoline valid.
|
||||
std::vector<uint8_t> buffer(sizeof TrampolineTemplate + load_info.size() + nethost_path_bytes.size() + path_bytes.size());
|
||||
auto& trampoline = *reinterpret_cast<TrampolineTemplate*>(&buffer[0]);
|
||||
const auto load_info_buffer = std::span(buffer).subspan(sizeof trampoline, load_info.size());
|
||||
const auto nethost_path_buffer = std::span(buffer).subspan(sizeof trampoline + load_info.size(), nethost_path_bytes.size());
|
||||
const auto dalamud_path_buffer = std::span(buffer).subspan(sizeof trampoline + load_info.size() + nethost_path_bytes.size(), path_bytes.size());
|
||||
|
||||
new(&trampoline)TrampolineTemplate(); // this line initializes given buffer instead of allocating memory
|
||||
memcpy(&load_info_buffer[0], &load_info[0], load_info_buffer.size());
|
||||
memcpy(&nethost_path_buffer[0], &nethost_path_bytes[0], nethost_path_buffer.size());
|
||||
memcpy(&dalamud_path_buffer[0], &path_bytes[0], dalamud_path_buffer.size());
|
||||
|
||||
// Backup remote process' original entry point.
|
||||
read_process_memory_or_throw(hProcess, entrypoint, trampoline.buf_EntryPointBackup);
|
||||
const auto bufferSize = sizeof(RewrittenEntryPointParameters) + entrypoint_replacement.size() + load_info.size() + standalone_rewrittenentrypoint.size();
|
||||
last_operation = std::format(L"std::vector alloc({}b)", bufferSize);
|
||||
std::vector<uint8_t> buffer(bufferSize);
|
||||
|
||||
// Allocate buffer in remote process, which will be used to fill addresses in the local buffer.
|
||||
const auto remote_buffer = reinterpret_cast<char*>(VirtualAllocEx(hProcess, nullptr, buffer.size(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE));
|
||||
|
||||
// Fill the values to be used in RewrittenEntryPoint
|
||||
trampoline.parameters = {
|
||||
.pAllocation = remote_buffer,
|
||||
.pEntrypoint = entrypoint,
|
||||
.pEntrypointBytes = remote_buffer + offsetof(TrampolineTemplate, buf_EntryPointBackup),
|
||||
.entrypointLength = sizeof trampoline.buf_EntryPointBackup,
|
||||
.pLoadInfo = remote_buffer + (&load_info_buffer[0] - &buffer[0]),
|
||||
};
|
||||
last_operation = std::format(L"VirtualAllocEx({}b)", bufferSize);
|
||||
const auto remote_buffer = static_cast<char*>(VirtualAllocEx(hProcess, nullptr, buffer.size(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE));
|
||||
|
||||
auto& params = *reinterpret_cast<RewrittenEntryPointParameters*>(buffer.data());
|
||||
params.entrypointLength = entrypoint_replacement.size();
|
||||
params.pEntrypoint = entrypoint;
|
||||
|
||||
// Fill the addresses referred in machine code.
|
||||
trampoline.CallLoadLibrary_nethost.lpLibFileName.val = remote_buffer + (&nethost_path_buffer[0] - &buffer[0]);
|
||||
trampoline.CallLoadLibrary_nethost.fn.ptr = LoadLibraryW;
|
||||
trampoline.CallLoadLibrary_DalamudBoot.lpLibFileName.val = remote_buffer + (&dalamud_path_buffer[0] - &buffer[0]);
|
||||
trampoline.CallLoadLibrary_DalamudBoot.fn.ptr = LoadLibraryW;
|
||||
trampoline.CallGetProcAddress.lpProcName.val = remote_buffer + offsetof(TrampolineTemplate, buf_CallGetProcAddress_lpProcName);
|
||||
trampoline.CallGetProcAddress.fn.ptr = GetProcAddress;
|
||||
trampoline.CallInjectEntryPoint.param.val = remote_buffer + offsetof(TrampolineTemplate, parameters);
|
||||
// Backup original entry point.
|
||||
last_operation = std::format(L"read_process_memory_or_throw(entrypoint, {}b)", entrypoint_replacement.size());
|
||||
read_process_memory_or_throw(hProcess, entrypoint, &buffer[sizeof params], entrypoint_replacement.size());
|
||||
|
||||
memcpy(&buffer[sizeof params + entrypoint_replacement.size()], load_info.data(), load_info.size());
|
||||
|
||||
last_operation = L"thunks::fill_placeholders(EntryPointReplacement)";
|
||||
thunks::fill_placeholders(standalone_rewrittenentrypoint.data(), remote_buffer);
|
||||
memcpy(&buffer[sizeof params + entrypoint_replacement.size() + load_info.size()], standalone_rewrittenentrypoint.data(), standalone_rewrittenentrypoint.size());
|
||||
|
||||
// Write the local buffer into the buffer in remote process.
|
||||
last_operation = std::format(L"write_process_memory_or_throw(remote_buffer, {}b)", buffer.size());
|
||||
write_process_memory_or_throw(hProcess, remote_buffer, buffer.data(), buffer.size());
|
||||
|
||||
// Overwrite remote process' entry point with a thunk that immediately calls our trampoline function.
|
||||
EntryPointThunkTemplate thunk{};
|
||||
thunk.CallTrampoline.fn.ptr = remote_buffer;
|
||||
write_process_memory_or_throw(hProcess, entrypoint, thunk);
|
||||
last_operation = L"thunks::fill_placeholders(RewrittenEntryPoint_Standalone::pRewrittenEntryPointParameters)";
|
||||
thunks::fill_placeholders(entrypoint_replacement.data(), remote_buffer + sizeof params + entrypoint_replacement.size() + load_info.size());
|
||||
|
||||
return 0;
|
||||
// Overwrite remote process' entry point with a thunk that will load our DLLs and call our trampoline function.
|
||||
last_operation = std::format(L"write_process_memory_or_throw(entrypoint={:X}, {}b)", reinterpret_cast<uintptr_t>(entrypoint), buffer.size());
|
||||
write_process_memory_or_throw(hProcess, entrypoint, entrypoint_replacement.data(), entrypoint_replacement.size());
|
||||
FlushInstructionCache(hProcess, entrypoint, entrypoint_replacement.size());
|
||||
|
||||
return S_OK;
|
||||
} catch (const std::exception& e) {
|
||||
OutputDebugStringA(std::format("RewriteRemoteEntryPoint failure: {} (GetLastError: {})\n", e.what(), GetLastError()).c_str());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
const auto err = GetLastError();
|
||||
const auto hr = err == ERROR_SUCCESS ? E_FAIL : HRESULT_FROM_WIN32(err);
|
||||
auto formatted = std::format(
|
||||
L"{}: {} ({})",
|
||||
last_operation,
|
||||
unicode::convert<std::wstring>(e.what()),
|
||||
utils::format_win32_error(err));
|
||||
OutputDebugStringW((formatted + L"\r\n").c_str());
|
||||
|
||||
/// @deprecated
|
||||
DllExport DWORD WINAPI RewriteRemoteEntryPoint(HANDLE hProcess, const wchar_t* pcwzPath, const char* pcszLoadInfo) {
|
||||
return RewriteRemoteEntryPointW(hProcess, pcwzPath, to_utf16(pcszLoadInfo).c_str());
|
||||
ICreateErrorInfoPtr cei;
|
||||
if (FAILED(CreateErrorInfo(&cei)))
|
||||
return hr;
|
||||
if (FAILED(cei->SetSource(const_cast<LPOLESTR>(L"Dalamud.Boot"))))
|
||||
return hr;
|
||||
if (FAILED(cei->SetDescription(const_cast<LPOLESTR>(formatted.c_str()))))
|
||||
return hr;
|
||||
|
||||
IErrorInfoPtr ei;
|
||||
if (FAILED(cei.QueryInterface(IID_PPV_ARGS(&ei))))
|
||||
return hr;
|
||||
|
||||
(void)SetErrorInfo(0, ei);
|
||||
return hr;
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Entry point function "called" instead of game's original main entry point.
|
||||
/// @param params Parameters set up from RewriteRemoteEntryPoint.
|
||||
DllExport void WINAPI RewrittenEntryPoint(RewrittenEntryPointParameters& params) {
|
||||
params.hMainThreadContinue = CreateEventW(nullptr, true, false, nullptr);
|
||||
if (!params.hMainThreadContinue)
|
||||
ExitProcess(-1);
|
||||
extern "C" void WINAPI RewrittenEntryPoint_AdjustedStack(RewrittenEntryPointParameters & params) {
|
||||
HANDLE hMainThreadContinue = nullptr;
|
||||
auto hr = S_OK;
|
||||
std::wstring last_operation;
|
||||
std::wstring exc_msg;
|
||||
SetLastError(ERROR_SUCCESS);
|
||||
|
||||
// Do whatever the work in a separate thread to minimize the stack usage at this context,
|
||||
// as this function really should have been a naked procedure but __declspec(naked) isn't supported in x64 version of msvc.
|
||||
params.hMainThread = CreateThread(nullptr, 0, [](void* p) -> DWORD {
|
||||
try {
|
||||
std::string loadInfo;
|
||||
auto& params = *reinterpret_cast<RewrittenEntryPointParameters*>(p);
|
||||
{
|
||||
// Restore original entry point.
|
||||
// Use WriteProcessMemory instead of memcpy to avoid having to fiddle with VirtualProtect.
|
||||
write_process_memory_or_throw(GetCurrentProcess(), params.pEntrypoint, params.pEntrypointBytes, params.entrypointLength);
|
||||
try {
|
||||
const auto pOriginalEntryPointBytes = reinterpret_cast<char*>(¶ms) + sizeof(params);
|
||||
const auto pLoadInfo = pOriginalEntryPointBytes + params.entrypointLength;
|
||||
|
||||
// Make a copy of load info, as the whole params will be freed after this code block.
|
||||
loadInfo = params.pLoadInfo;
|
||||
}
|
||||
// Restore original entry point.
|
||||
// Use WriteProcessMemory instead of memcpy to avoid having to fiddle with VirtualProtect.
|
||||
last_operation = L"restore original entry point";
|
||||
write_process_memory_or_throw(GetCurrentProcess(), params.pEntrypoint, pOriginalEntryPointBytes, params.entrypointLength);
|
||||
FlushInstructionCache(GetCurrentProcess(), params.pEntrypoint, params.entrypointLength);
|
||||
|
||||
InitializeImpl(&loadInfo[0], params.hMainThreadContinue);
|
||||
return 0;
|
||||
} catch (const std::exception& e) {
|
||||
MessageBoxA(nullptr, std::format("Failed to load Dalamud.\n\nError: {}", e.what()).c_str(), "Dalamud.Boot", MB_OK | MB_ICONERROR);
|
||||
ExitProcess(-1);
|
||||
hMainThreadContinue = CreateEventW(nullptr, true, false, nullptr);
|
||||
last_operation = L"hMainThreadContinue = CreateEventW";
|
||||
if (!hMainThreadContinue)
|
||||
throw std::runtime_error("CreateEventW");
|
||||
|
||||
last_operation = L"InitializeImpl";
|
||||
hr = InitializeImpl(pLoadInfo, hMainThreadContinue);
|
||||
} catch (const std::exception& e) {
|
||||
if (hr == S_OK) {
|
||||
const auto err = GetLastError();
|
||||
hr = err == ERROR_SUCCESS ? E_FAIL : HRESULT_FROM_WIN32(err);
|
||||
}
|
||||
}, ¶ms, 0, nullptr);
|
||||
if (!params.hMainThread)
|
||||
ExitProcess(-1);
|
||||
|
||||
CloseHandle(params.hMainThread);
|
||||
WaitForSingleObject(params.hMainThreadContinue, INFINITE);
|
||||
VirtualFree(params.pAllocation, 0, MEM_RELEASE);
|
||||
ICreateErrorInfoPtr cei;
|
||||
IErrorInfoPtr ei;
|
||||
if (SUCCEEDED(CreateErrorInfo(&cei))
|
||||
&& SUCCEEDED(cei->SetDescription(const_cast<wchar_t*>(unicode::convert<std::wstring>(e.what()).c_str())))
|
||||
&& SUCCEEDED(cei.QueryInterface(IID_PPV_ARGS(&ei)))) {
|
||||
(void)SetErrorInfo(0, ei);
|
||||
}
|
||||
}
|
||||
|
||||
if (FAILED(hr)) {
|
||||
const _com_error err(hr);
|
||||
auto desc = err.Description();
|
||||
if (desc.length() == 0)
|
||||
desc = err.ErrorMessage();
|
||||
if (MessageBoxW(nullptr, std::format(
|
||||
L"Failed to load Dalamud. Load game without Dalamud(yes) or abort(no)?\n\n{}\n{}",
|
||||
last_operation,
|
||||
desc.GetBSTR()).c_str(),
|
||||
L"Dalamud.Boot", MB_OK | MB_YESNO) == IDNO)
|
||||
ExitProcess(-1);
|
||||
if (hMainThreadContinue) {
|
||||
CloseHandle(hMainThreadContinue);
|
||||
hMainThreadContinue = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (hMainThreadContinue)
|
||||
WaitForSingleObject(hMainThreadContinue, INFINITE);
|
||||
|
||||
VirtualFree(¶ms, 0, MEM_RELEASE);
|
||||
}
|
||||
|
|
|
|||
82
Dalamud.Boot/rewrite_entrypoint_thunks.asm
Normal file
82
Dalamud.Boot/rewrite_entrypoint_thunks.asm
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
PUBLIC EntryPointReplacement
|
||||
PUBLIC RewrittenEntryPoint_Standalone
|
||||
PUBLIC RewrittenEntryPoint
|
||||
|
||||
; 06 and 07 are invalid opcodes
|
||||
; CC is int3 = bp
|
||||
; using 0CCCCCCCCCCCCCCCCh as function terminator
|
||||
; using 00606060606060606h as placeholders
|
||||
|
||||
TERMINATOR = 0CCCCCCCCCCCCCCCCh
|
||||
PLACEHOLDER = 00606060606060606h
|
||||
|
||||
.code
|
||||
|
||||
EntryPointReplacement PROC
|
||||
start:
|
||||
; rsp % 0x10 = 0x08
|
||||
lea rax, [start]
|
||||
push rax
|
||||
|
||||
; rsp % 0x10 = 0x00
|
||||
mov rax, PLACEHOLDER
|
||||
|
||||
; this calls RewrittenEntryPoint_Standalone
|
||||
jmp rax
|
||||
|
||||
dq TERMINATOR
|
||||
EntryPointReplacement ENDP
|
||||
|
||||
RewrittenEntryPoint_Standalone PROC
|
||||
start:
|
||||
; stack is aligned to 0x10; see above
|
||||
sub rsp, 20h
|
||||
lea rcx, [embeddedData]
|
||||
add rcx, qword ptr [nNethostOffset]
|
||||
call qword ptr [pfnLoadLibraryW]
|
||||
|
||||
lea rcx, [embeddedData]
|
||||
add rcx, qword ptr [nDalamudOffset]
|
||||
call qword ptr [pfnLoadLibraryW]
|
||||
|
||||
mov rcx, rax
|
||||
lea rdx, [pcszEntryPointName]
|
||||
call qword ptr [pfnGetProcAddress]
|
||||
|
||||
mov rcx, qword ptr [pRewrittenEntryPointParameters]
|
||||
; this calls RewrittenEntryPoint
|
||||
jmp rax
|
||||
|
||||
pfnLoadLibraryW:
|
||||
dq PLACEHOLDER
|
||||
|
||||
pfnGetProcAddress:
|
||||
dq PLACEHOLDER
|
||||
|
||||
pRewrittenEntryPointParameters:
|
||||
dq PLACEHOLDER
|
||||
|
||||
nNethostOffset:
|
||||
dq PLACEHOLDER
|
||||
|
||||
nDalamudOffset:
|
||||
dq PLACEHOLDER
|
||||
|
||||
pcszEntryPointName:
|
||||
db "RewrittenEntryPoint", 0
|
||||
|
||||
embeddedData:
|
||||
|
||||
dq TERMINATOR
|
||||
RewrittenEntryPoint_Standalone ENDP
|
||||
|
||||
EXTERN RewrittenEntryPoint_AdjustedStack :PROC
|
||||
|
||||
RewrittenEntryPoint PROC
|
||||
; stack is aligned to 0x10; see above
|
||||
call RewrittenEntryPoint_AdjustedStack
|
||||
add rsp, 20h
|
||||
ret
|
||||
RewrittenEntryPoint ENDP
|
||||
|
||||
END
|
||||
|
|
@ -408,14 +408,20 @@ utils::signature_finder::result utils::signature_finder::find_one() const {
|
|||
return find(1, 1, false).front();
|
||||
}
|
||||
|
||||
utils::memory_tenderizer::memory_tenderizer(const void* pAddress, size_t length, DWORD dwNewProtect) : m_data(reinterpret_cast<char*>(const_cast<void*>(pAddress)), length) {
|
||||
utils::memory_tenderizer::memory_tenderizer(const void* pAddress, size_t length, DWORD dwNewProtect)
|
||||
: memory_tenderizer(GetCurrentProcess(), pAddress, length, dwNewProtect) {
|
||||
}
|
||||
|
||||
utils::memory_tenderizer::memory_tenderizer(HANDLE hProcess, const void* pAddress, size_t length, DWORD dwNewProtect)
|
||||
: m_process(hProcess)
|
||||
, m_data(static_cast<char*>(const_cast<void*>(pAddress)), length) {
|
||||
try {
|
||||
for (auto pCoveredAddress = &m_data[0];
|
||||
pCoveredAddress < &m_data[0] + m_data.size();
|
||||
pCoveredAddress = reinterpret_cast<char*>(m_regions.back().BaseAddress) + m_regions.back().RegionSize) {
|
||||
for (auto pCoveredAddress = m_data.data();
|
||||
pCoveredAddress < m_data.data() + m_data.size();
|
||||
pCoveredAddress = static_cast<char*>(m_regions.back().BaseAddress) + m_regions.back().RegionSize) {
|
||||
|
||||
MEMORY_BASIC_INFORMATION region{};
|
||||
if (!VirtualQuery(pCoveredAddress, ®ion, sizeof region)) {
|
||||
if (!VirtualQueryEx(hProcess, pCoveredAddress, ®ion, sizeof region)) {
|
||||
throw std::runtime_error(std::format(
|
||||
"VirtualQuery(addr=0x{:X}, ..., cb={}) failed with Win32 code 0x{:X}",
|
||||
reinterpret_cast<size_t>(pCoveredAddress),
|
||||
|
|
@ -423,7 +429,7 @@ utils::memory_tenderizer::memory_tenderizer(const void* pAddress, size_t length,
|
|||
GetLastError()));
|
||||
}
|
||||
|
||||
if (!VirtualProtect(region.BaseAddress, region.RegionSize, dwNewProtect, ®ion.Protect)) {
|
||||
if (!VirtualProtectEx(hProcess, region.BaseAddress, region.RegionSize, dwNewProtect, ®ion.Protect)) {
|
||||
throw std::runtime_error(std::format(
|
||||
"(Change)VirtualProtect(addr=0x{:X}, size=0x{:X}, ..., ...) failed with Win32 code 0x{:X}",
|
||||
reinterpret_cast<size_t>(region.BaseAddress),
|
||||
|
|
@ -436,7 +442,7 @@ utils::memory_tenderizer::memory_tenderizer(const void* pAddress, size_t length,
|
|||
|
||||
} catch (...) {
|
||||
for (auto& region : std::ranges::reverse_view(m_regions)) {
|
||||
if (!VirtualProtect(region.BaseAddress, region.RegionSize, region.Protect, ®ion.Protect)) {
|
||||
if (!VirtualProtectEx(hProcess, region.BaseAddress, region.RegionSize, region.Protect, ®ion.Protect)) {
|
||||
// Could not restore; fast fail
|
||||
__fastfail(GetLastError());
|
||||
}
|
||||
|
|
@ -448,7 +454,7 @@ utils::memory_tenderizer::memory_tenderizer(const void* pAddress, size_t length,
|
|||
|
||||
utils::memory_tenderizer::~memory_tenderizer() {
|
||||
for (auto& region : std::ranges::reverse_view(m_regions)) {
|
||||
if (!VirtualProtect(region.BaseAddress, region.RegionSize, region.Protect, ®ion.Protect)) {
|
||||
if (!VirtualProtectEx(m_process, region.BaseAddress, region.RegionSize, region.Protect, ®ion.Protect)) {
|
||||
// Could not restore; fast fail
|
||||
__fastfail(GetLastError());
|
||||
}
|
||||
|
|
@ -578,16 +584,6 @@ std::vector<std::string> utils::get_env_list(const wchar_t* pcszName) {
|
|||
return res;
|
||||
}
|
||||
|
||||
std::wstring utils::to_wstring(const std::string& str) {
|
||||
if (str.empty()) return std::wstring();
|
||||
size_t convertedChars = 0;
|
||||
size_t newStrSize = str.size() + 1;
|
||||
std::wstring wstr(newStrSize, L'\0');
|
||||
mbstowcs_s(&convertedChars, &wstr[0], newStrSize, str.c_str(), _TRUNCATE);
|
||||
wstr.resize(convertedChars - 1);
|
||||
return wstr;
|
||||
}
|
||||
|
||||
std::filesystem::path utils::get_module_path(HMODULE hModule) {
|
||||
std::wstring buf(MAX_PATH, L'\0');
|
||||
while (true) {
|
||||
|
|
@ -654,3 +650,25 @@ std::wstring utils::escape_shell_arg(const std::wstring& arg) {
|
|||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
std::wstring utils::format_win32_error(DWORD err) {
|
||||
wchar_t* pwszMsg = nullptr;
|
||||
FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER |
|
||||
FORMAT_MESSAGE_FROM_SYSTEM |
|
||||
FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr,
|
||||
err,
|
||||
MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
|
||||
reinterpret_cast<LPWSTR>(&pwszMsg),
|
||||
0,
|
||||
nullptr);
|
||||
if (pwszMsg) {
|
||||
std::wstring result = std::format(L"Win32 error ({}=0x{:X}): {}", err, err, pwszMsg);
|
||||
while (!result.empty() && std::isspace(result.back()))
|
||||
result.pop_back();
|
||||
LocalFree(pwszMsg);
|
||||
return result;
|
||||
}
|
||||
|
||||
return std::format(L"Win32 error ({}=0x{:X})", err, err);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,10 +111,13 @@ namespace utils {
|
|||
};
|
||||
|
||||
class memory_tenderizer {
|
||||
HANDLE m_process;
|
||||
std::span<char> m_data;
|
||||
std::vector<MEMORY_BASIC_INFORMATION> m_regions;
|
||||
|
||||
public:
|
||||
memory_tenderizer(HANDLE hProcess, const void* pAddress, size_t length, DWORD dwNewProtect);
|
||||
|
||||
memory_tenderizer(const void* pAddress, size_t length, DWORD dwNewProtect);
|
||||
|
||||
template<typename T, typename = std::enable_if_t<std::is_trivial_v<T>&& std::is_standard_layout_v<T>>>
|
||||
|
|
@ -264,8 +267,6 @@ namespace utils {
|
|||
return get_env_list<T>(unicode::convert<std::wstring>(pcszName).c_str());
|
||||
}
|
||||
|
||||
std::wstring to_wstring(const std::string& str);
|
||||
|
||||
std::filesystem::path get_module_path(HMODULE hModule);
|
||||
|
||||
/// @brief Find the game main window.
|
||||
|
|
@ -275,4 +276,6 @@ namespace utils {
|
|||
void wait_for_game_window();
|
||||
|
||||
std::wstring escape_shell_arg(const std::wstring& arg);
|
||||
|
||||
std::wstring format_win32_error(DWORD err);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
#include "logging.h"
|
||||
#include "utils.h"
|
||||
#include "hooks.h"
|
||||
|
||||
#include "crashhandler_shared.h"
|
||||
#include "DalamudStartInfo.h"
|
||||
|
|
@ -24,6 +25,7 @@
|
|||
|
||||
PVOID g_veh_handle = nullptr;
|
||||
bool g_veh_do_full_dump = false;
|
||||
std::optional<hooks::import_hook<decltype(SetUnhandledExceptionFilter)>> g_HookSetUnhandledExceptionFilter;
|
||||
|
||||
HANDLE g_crashhandler_process = nullptr;
|
||||
HANDLE g_crashhandler_event = nullptr;
|
||||
|
|
@ -110,13 +112,16 @@ static void append_injector_launch_args(std::vector<std::wstring>& args)
|
|||
case DalamudStartInfo::LoadMethod::DllInject:
|
||||
args.emplace_back(L"--mode=inject");
|
||||
}
|
||||
args.emplace_back(L"--logpath=\"" + utils::to_wstring(g_startInfo.BootLogPath) + L"\"");
|
||||
args.emplace_back(L"--dalamud-working-directory=\"" + utils::to_wstring(g_startInfo.WorkingDirectory) + L"\"");
|
||||
args.emplace_back(L"--dalamud-configuration-path=\"" + utils::to_wstring(g_startInfo.ConfigurationPath) + L"\"");
|
||||
args.emplace_back(L"--dalamud-plugin-directory=\"" + utils::to_wstring(g_startInfo.PluginDirectory) + L"\"");
|
||||
args.emplace_back(L"--dalamud-asset-directory=\"" + utils::to_wstring(g_startInfo.AssetDirectory) + L"\"");
|
||||
args.emplace_back(L"--dalamud-client-language=" + std::to_wstring(static_cast<int>(g_startInfo.Language)));
|
||||
args.emplace_back(L"--dalamud-delay-initialize=" + std::to_wstring(g_startInfo.DelayInitializeMs));
|
||||
args.emplace_back(L"--dalamud-working-directory=\"" + unicode::convert<std::wstring>(g_startInfo.WorkingDirectory) + L"\"");
|
||||
args.emplace_back(L"--dalamud-configuration-path=\"" + unicode::convert<std::wstring>(g_startInfo.ConfigurationPath) + L"\"");
|
||||
args.emplace_back(L"--logpath=\"" + unicode::convert<std::wstring>(g_startInfo.LogPath) + L"\"");
|
||||
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(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
|
||||
|
||||
if (g_startInfo.BootShowConsole)
|
||||
args.emplace_back(L"--console");
|
||||
if (g_startInfo.BootEnableEtw)
|
||||
|
|
@ -143,21 +148,7 @@ static void append_injector_launch_args(std::vector<std::wstring>& args)
|
|||
|
||||
LONG exception_handler(EXCEPTION_POINTERS* ex)
|
||||
{
|
||||
if (ex->ExceptionRecord->ExceptionCode == 0x12345678)
|
||||
{
|
||||
// pass
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!is_whitelist_exception(ex->ExceptionRecord->ExceptionCode))
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
|
||||
if (!is_ffxiv_address(L"ffxiv_dx11.exe", ex->ContextRecord->Rip) &&
|
||||
!is_ffxiv_address(L"cimgui.dll", ex->ContextRecord->Rip))
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
// block any other exceptions hitting the veh while the messagebox is open
|
||||
// block any other exceptions hitting the handler while the messagebox is open
|
||||
const auto lock = std::lock_guard(g_exception_handler_mutex);
|
||||
|
||||
exception_info exinfo{};
|
||||
|
|
@ -167,7 +158,7 @@ LONG exception_handler(EXCEPTION_POINTERS* ex)
|
|||
exinfo.ExceptionRecord = ex->ExceptionRecord ? *ex->ExceptionRecord : EXCEPTION_RECORD{};
|
||||
const auto time_now = std::chrono::system_clock::now();
|
||||
auto lifetime = std::chrono::duration_cast<std::chrono::seconds>(
|
||||
time_now.time_since_epoch()).count()
|
||||
time_now.time_since_epoch()).count()
|
||||
- std::chrono::duration_cast<std::chrono::seconds>(
|
||||
g_time_start.time_since_epoch()).count();
|
||||
exinfo.nLifetime = lifetime;
|
||||
|
|
@ -175,10 +166,14 @@ LONG exception_handler(EXCEPTION_POINTERS* ex)
|
|||
DuplicateHandle(GetCurrentProcess(), g_crashhandler_event, g_crashhandler_process, &exinfo.hEventHandle, 0, TRUE, DUPLICATE_SAME_ACCESS);
|
||||
|
||||
std::wstring stackTrace;
|
||||
if (void* fn; const auto err = static_cast<DWORD>(g_clr->get_function_pointer(
|
||||
if (!g_clr)
|
||||
{
|
||||
stackTrace = L"(no CLR stack trace available)";
|
||||
}
|
||||
else if (void* fn; const auto err = static_cast<DWORD>(g_clr->get_function_pointer(
|
||||
L"Dalamud.EntryPoint, Dalamud",
|
||||
L"VehCallback",
|
||||
L"Dalamud.EntryPoint+VehDelegate, Dalamud",
|
||||
L"Dalamud.EntryPoint+VehDelegate, Dalamud",
|
||||
nullptr, nullptr, &fn)))
|
||||
{
|
||||
stackTrace = std::format(L"Failed to read stack trace: 0x{:08x}", err);
|
||||
|
|
@ -188,17 +183,25 @@ LONG exception_handler(EXCEPTION_POINTERS* ex)
|
|||
stackTrace = static_cast<wchar_t*(*)()>(fn)();
|
||||
// Don't free it, as the program's going to be quit anyway
|
||||
}
|
||||
|
||||
|
||||
exinfo.dwStackTraceLength = static_cast<DWORD>(stackTrace.size());
|
||||
exinfo.dwTroubleshootingPackDataLength = static_cast<DWORD>(g_startInfo.TroubleshootingPackData.size());
|
||||
if (DWORD written; !WriteFile(g_crashhandler_pipe_write, &exinfo, static_cast<DWORD>(sizeof exinfo), &written, nullptr) || sizeof exinfo != written)
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
|
||||
if (DWORD written; !WriteFile(g_crashhandler_pipe_write, &stackTrace[0], static_cast<DWORD>(std::span(stackTrace).size_bytes()), &written, nullptr) || std::span(stackTrace).size_bytes() != written)
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
if (const auto nb = static_cast<DWORD>(std::span(stackTrace).size_bytes()))
|
||||
{
|
||||
if (DWORD written; !WriteFile(g_crashhandler_pipe_write, stackTrace.data(), nb, &written, nullptr) || nb != written)
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
if (DWORD written; !WriteFile(g_crashhandler_pipe_write, &g_startInfo.TroubleshootingPackData[0], static_cast<DWORD>(std::span(g_startInfo.TroubleshootingPackData).size_bytes()), &written, nullptr) || std::span(g_startInfo.TroubleshootingPackData).size_bytes() != written)
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
if (const auto nb = static_cast<DWORD>(std::span(g_startInfo.TroubleshootingPackData).size_bytes()))
|
||||
{
|
||||
if (DWORD written; !WriteFile(g_crashhandler_pipe_write, g_startInfo.TroubleshootingPackData.data(), nb, &written, nullptr) || nb != written)
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
AllowSetForegroundWindow(GetProcessId(g_crashhandler_process));
|
||||
|
||||
HANDLE waitHandles[] = { g_crashhandler_process, g_crashhandler_event };
|
||||
DWORD waitResult = WaitForMultipleObjects(2, waitHandles, FALSE, INFINITE);
|
||||
|
|
@ -217,13 +220,44 @@ LONG exception_handler(EXCEPTION_POINTERS* ex)
|
|||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
LONG WINAPI structured_exception_handler(EXCEPTION_POINTERS* ex)
|
||||
{
|
||||
return exception_handler(ex);
|
||||
}
|
||||
|
||||
LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex)
|
||||
{
|
||||
if (ex->ExceptionRecord->ExceptionCode == 0x12345678)
|
||||
{
|
||||
// pass
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!is_whitelist_exception(ex->ExceptionRecord->ExceptionCode))
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
|
||||
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_handler(ex);
|
||||
}
|
||||
|
||||
bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
|
||||
{
|
||||
if (g_veh_handle)
|
||||
return false;
|
||||
|
||||
g_veh_handle = AddVectoredExceptionHandler(1, exception_handler);
|
||||
SetUnhandledExceptionFilter(nullptr);
|
||||
g_veh_handle = AddVectoredExceptionHandler(TRUE, vectored_exception_handler);
|
||||
|
||||
g_HookSetUnhandledExceptionFilter.emplace("kernel32.dll!SetUnhandledExceptionFilter (lpTopLevelExceptionFilter)", "kernel32.dll", "SetUnhandledExceptionFilter", 0);
|
||||
g_HookSetUnhandledExceptionFilter->set_detour([](LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter) -> LPTOP_LEVEL_EXCEPTION_FILTER
|
||||
{
|
||||
logging::I("Overwriting UnhandledExceptionFilter from {} to {}", reinterpret_cast<ULONG_PTR>(lpTopLevelExceptionFilter), reinterpret_cast<ULONG_PTR>(structured_exception_handler));
|
||||
return g_HookSetUnhandledExceptionFilter->call_original(structured_exception_handler);
|
||||
});
|
||||
SetUnhandledExceptionFilter(structured_exception_handler);
|
||||
|
||||
g_veh_do_full_dump = doFullDump;
|
||||
g_time_start = std::chrono::system_clock::now();
|
||||
|
|
@ -355,6 +389,8 @@ bool veh::remove_handler()
|
|||
if (g_veh_handle && RemoveVectoredExceptionHandler(g_veh_handle) != 0)
|
||||
{
|
||||
g_veh_handle = nullptr;
|
||||
g_HookSetUnhandledExceptionFilter.reset();
|
||||
SetUnhandledExceptionFilter(nullptr);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -5,9 +5,8 @@
|
|||
#include "DalamudStartInfo.h"
|
||||
#include "hooks.h"
|
||||
#include "logging.h"
|
||||
#include "ntdll.h"
|
||||
#include "utils.h"
|
||||
#include <iphlpapi.h>
|
||||
#include <icmpapi.h>
|
||||
|
||||
template<typename T>
|
||||
static std::span<T> assume_nonempty_span(std::span<T> t, const char* descr) {
|
||||
|
|
@ -513,50 +512,6 @@ void xivfixes::backup_userdata_save(bool bApply) {
|
|||
}
|
||||
}
|
||||
|
||||
void xivfixes::clr_failfast_hijack(bool bApply)
|
||||
{
|
||||
static const char* LogTag = "[xivfixes:clr_failfast_hijack]";
|
||||
static std::optional<hooks::import_hook<decltype(RaiseFailFastException)>> s_HookClrFatalError;
|
||||
static std::optional<hooks::import_hook<decltype(SetUnhandledExceptionFilter)>> s_HookSetUnhandledExceptionFilter;
|
||||
|
||||
if (bApply)
|
||||
{
|
||||
if (!g_startInfo.BootEnabledGameFixes.contains("clr_failfast_hijack")) {
|
||||
logging::I("{} Turned off via environment variable.", LogTag);
|
||||
return;
|
||||
}
|
||||
|
||||
s_HookClrFatalError.emplace("kernel32.dll!RaiseFailFastException (import, backup_userdata_save)", "kernel32.dll", "RaiseFailFastException", 0);
|
||||
s_HookSetUnhandledExceptionFilter.emplace("kernel32.dll!SetUnhandledExceptionFilter (lpTopLevelExceptionFilter)", "kernel32.dll", "SetUnhandledExceptionFilter", 0);
|
||||
|
||||
s_HookClrFatalError->set_detour([](PEXCEPTION_RECORD pExceptionRecord,
|
||||
_In_opt_ PCONTEXT pContextRecord,
|
||||
_In_ DWORD dwFlags)
|
||||
{
|
||||
MessageBoxW(nullptr, L"An error in a Dalamud plugin was detected and the game cannot continue.\n\nPlease take a screenshot of this error message and let us know about it.", L"Dalamud", MB_OK | MB_ICONERROR);
|
||||
|
||||
return s_HookClrFatalError->call_original(pExceptionRecord, pContextRecord, dwFlags);
|
||||
});
|
||||
|
||||
s_HookSetUnhandledExceptionFilter->set_detour([](LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter) -> LPTOP_LEVEL_EXCEPTION_FILTER
|
||||
{
|
||||
logging::I("{} SetUnhandledExceptionFilter", LogTag);
|
||||
return nullptr;
|
||||
});
|
||||
|
||||
logging::I("{} Enable", LogTag);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (s_HookClrFatalError) {
|
||||
logging::I("{} Disable ClrFatalError", LogTag);
|
||||
s_HookClrFatalError.reset();
|
||||
s_HookSetUnhandledExceptionFilter.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void xivfixes::prevent_icmphandle_crashes(bool bApply) {
|
||||
static const char* LogTag = "[xivfixes:prevent_icmphandle_crashes]";
|
||||
|
||||
|
|
@ -590,6 +545,109 @@ void xivfixes::prevent_icmphandle_crashes(bool bApply) {
|
|||
}
|
||||
}
|
||||
|
||||
void xivfixes::symbol_load_patches(bool bApply) {
|
||||
static const char* LogTag = "[xivfixes:symbol_load_patches]";
|
||||
|
||||
static std::optional<hooks::import_hook<decltype(SymInitialize)>> s_hookSymInitialize;
|
||||
static PVOID s_dllNotificationCookie = nullptr;
|
||||
|
||||
static const auto RemoveFullPathPdbInfo = [](const utils::loaded_module& mod) {
|
||||
const auto ddva = mod.data_directory(IMAGE_DIRECTORY_ENTRY_DEBUG).VirtualAddress;
|
||||
if (!ddva)
|
||||
return;
|
||||
|
||||
const auto& ddir = mod.ref_as<IMAGE_DEBUG_DIRECTORY>(ddva);
|
||||
if (ddir.Type == IMAGE_DEBUG_TYPE_CODEVIEW) {
|
||||
// The Visual C++ debug information.
|
||||
// Ghidra calls it "DotNetPdbInfo".
|
||||
static constexpr DWORD DotNetPdbInfoSignatureValue = 0x53445352;
|
||||
struct DotNetPdbInfo {
|
||||
DWORD Signature; // RSDS
|
||||
GUID Guid;
|
||||
DWORD Age;
|
||||
char PdbPath[1];
|
||||
};
|
||||
|
||||
const auto& pdbref = mod.ref_as<DotNetPdbInfo>(ddir.AddressOfRawData);
|
||||
if (pdbref.Signature == DotNetPdbInfoSignatureValue) {
|
||||
const auto pathSpan = std::string_view(pdbref.PdbPath, strlen(pdbref.PdbPath));
|
||||
const auto pathWide = unicode::convert<std::wstring>(pathSpan);
|
||||
std::wstring windowsDirectory(GetWindowsDirectoryW(nullptr, 0) + 1, L'\0');
|
||||
windowsDirectory.resize(
|
||||
GetWindowsDirectoryW(windowsDirectory.data(), static_cast<UINT>(windowsDirectory.size())));
|
||||
if (!PathIsRelativeW(pathWide.c_str()) && !PathIsSameRootW(windowsDirectory.c_str(), pathWide.c_str())) {
|
||||
utils::memory_tenderizer pathOverwrite(&pdbref.PdbPath, pathSpan.size(), PAGE_READWRITE);
|
||||
auto sep = std::find(pathSpan.rbegin(), pathSpan.rend(), '/');
|
||||
if (sep == pathSpan.rend())
|
||||
sep = std::find(pathSpan.rbegin(), pathSpan.rend(), '\\');
|
||||
if (sep != pathSpan.rend()) {
|
||||
logging::I(
|
||||
"{} Stripping pdb path folder: {} to {}",
|
||||
LogTag,
|
||||
pathSpan,
|
||||
&*sep + 1);
|
||||
memmove(const_cast<char*>(pathSpan.data()), &*sep + 1, sep - pathSpan.rbegin() + 1);
|
||||
} else {
|
||||
logging::I("{} Leaving pdb path unchanged: {}", LogTag, pathSpan);
|
||||
}
|
||||
} else {
|
||||
logging::I("{} Leaving pdb path unchanged: {}", LogTag, pathSpan);
|
||||
}
|
||||
} else {
|
||||
logging::I("{} CODEVIEW struct signature mismatch: got {:08X} instead.", LogTag, pdbref.Signature);
|
||||
}
|
||||
} else {
|
||||
logging::I("{} Debug directory: type {} is unsupported.", LogTag, ddir.Type);
|
||||
}
|
||||
};
|
||||
|
||||
if (bApply) {
|
||||
if (!g_startInfo.BootEnabledGameFixes.contains("symbol_load_patches")) {
|
||||
logging::I("{} Turned off via environment variable.", LogTag);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& mod : utils::loaded_module::all_modules())
|
||||
RemoveFullPathPdbInfo(mod);
|
||||
|
||||
if (!s_dllNotificationCookie) {
|
||||
const auto res = LdrRegisterDllNotification(
|
||||
0,
|
||||
[](ULONG notiReason, const LDR_DLL_NOTIFICATION_DATA* pData, void* /* context */) {
|
||||
if (notiReason == LDR_DLL_NOTIFICATION_REASON_LOADED)
|
||||
RemoveFullPathPdbInfo(pData->Loaded.DllBase);
|
||||
},
|
||||
nullptr,
|
||||
&s_dllNotificationCookie);
|
||||
|
||||
if (res != STATUS_SUCCESS) {
|
||||
logging::E("{} LdrRegisterDllNotification failure: 0x{:08X}", LogTag, res);
|
||||
s_dllNotificationCookie = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
s_hookSymInitialize.emplace("dbghelp.dll!SymInitialize (import, symbol_load_patches)", "dbghelp.dll", "SymInitialize", 0);
|
||||
s_hookSymInitialize->set_detour([](HANDLE hProcess, PCSTR UserSearchPath, BOOL fInvadeProcess) noexcept {
|
||||
logging::I("{} Suppressed SymInitialize.", LogTag);
|
||||
SetLastError(ERROR_NOT_SUPPORTED);
|
||||
return FALSE;
|
||||
});
|
||||
|
||||
logging::I("{} Enable", LogTag);
|
||||
}
|
||||
else {
|
||||
if (s_hookSymInitialize) {
|
||||
logging::I("{} Disable", LogTag);
|
||||
s_hookSymInitialize.reset();
|
||||
}
|
||||
|
||||
if (s_dllNotificationCookie) {
|
||||
(void)LdrUnregisterDllNotification(s_dllNotificationCookie);
|
||||
s_dllNotificationCookie = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void xivfixes::apply_all(bool bApply) {
|
||||
for (const auto& [taskName, taskFunction] : std::initializer_list<std::pair<const char*, void(*)(bool)>>
|
||||
{
|
||||
|
|
@ -598,8 +656,8 @@ void xivfixes::apply_all(bool bApply) {
|
|||
{ "disable_game_openprocess_access_check", &disable_game_openprocess_access_check },
|
||||
{ "redirect_openprocess", &redirect_openprocess },
|
||||
{ "backup_userdata_save", &backup_userdata_save },
|
||||
{ "clr_failfast_hijack", &clr_failfast_hijack },
|
||||
{ "prevent_icmphandle_crashes", &prevent_icmphandle_crashes }
|
||||
{ "prevent_icmphandle_crashes", &prevent_icmphandle_crashes },
|
||||
{ "symbol_load_patches", &symbol_load_patches },
|
||||
}
|
||||
) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ namespace xivfixes {
|
|||
void disable_game_openprocess_access_check(bool bApply);
|
||||
void redirect_openprocess(bool bApply);
|
||||
void backup_userdata_save(bool bApply);
|
||||
void clr_failfast_hijack(bool bApply);
|
||||
void prevent_icmphandle_crashes(bool bApply);
|
||||
void symbol_load_patches(bool bApply);
|
||||
|
||||
void apply_all(bool bApply);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ namespace Dalamud.Common;
|
|||
|
||||
/// <summary>
|
||||
/// Struct containing information needed to initialize Dalamud.
|
||||
/// Modify DalamudStartInfo.h and DalamudStartInfo.cpp along with this record.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public record DalamudStartInfo
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Lumina" Version="3.15.2" />
|
||||
<PackageReference Include="Lumina" Version="3.16.0" />
|
||||
<PackageReference Include="Lumina.Excel" Version="6.5.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ int wmain(int argc, wchar_t** argv)
|
|||
// =========================================================================== //
|
||||
|
||||
void* entrypoint_vfn;
|
||||
int result = InitializeClrAndGetEntryPoint(
|
||||
const auto result = InitializeClrAndGetEntryPoint(
|
||||
GetModuleHandleW(nullptr),
|
||||
false,
|
||||
runtimeconfig_path,
|
||||
|
|
@ -33,15 +33,15 @@ int wmain(int argc, wchar_t** argv)
|
|||
L"Dalamud.Injector.EntryPoint+MainDelegate, Dalamud.Injector",
|
||||
&entrypoint_vfn);
|
||||
|
||||
if (result != 0)
|
||||
if (FAILED(result))
|
||||
return result;
|
||||
|
||||
typedef void (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(int, wchar_t**);
|
||||
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...");
|
||||
entrypoint_fn(argc, argv);
|
||||
const auto ret = entrypoint_fn(argc, argv);
|
||||
logging::I("Done!");
|
||||
|
||||
return 0;
|
||||
return ret;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,89 +31,100 @@ namespace Dalamud.Injector
|
|||
/// </summary>
|
||||
/// <param name="argc">Count of arguments.</param>
|
||||
/// <param name="argvPtr">char** string arguments.</param>
|
||||
public delegate void MainDelegate(int argc, IntPtr argvPtr);
|
||||
/// <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>
|
||||
public static void Main(int argc, IntPtr argvPtr)
|
||||
/// <returns>Return value (HRESULT).</returns>
|
||||
public static int Main(int argc, IntPtr argvPtr)
|
||||
{
|
||||
List<string> args = new(argc);
|
||||
|
||||
unsafe
|
||||
try
|
||||
{
|
||||
var argv = (IntPtr*)argvPtr;
|
||||
for (var i = 0; i < argc; i++)
|
||||
args.Add(Marshal.PtrToStringUni(argv[i]));
|
||||
}
|
||||
List<string> args = new(argc);
|
||||
|
||||
Init(args);
|
||||
args.Remove("-v"); // Remove "verbose" flag
|
||||
|
||||
if (args.Count >= 2 && args[1].ToLowerInvariant() == "launch-test")
|
||||
{
|
||||
Environment.Exit(ProcessLaunchTestCommand(args));
|
||||
return;
|
||||
}
|
||||
|
||||
DalamudStartInfo startInfo = null;
|
||||
if (args.Count == 1)
|
||||
{
|
||||
// No command defaults to inject
|
||||
args.Add("inject");
|
||||
args.Add("--all");
|
||||
|
||||
#if !DEBUG
|
||||
args.Add("--warn");
|
||||
#endif
|
||||
|
||||
}
|
||||
else if (int.TryParse(args[1], out var _))
|
||||
{
|
||||
// Assume that PID has been passed.
|
||||
args.Insert(1, "inject");
|
||||
|
||||
// If originally second parameter exists, then assume that it's a base64 encoded start info.
|
||||
// Dalamud.Injector.exe inject [pid] [base64]
|
||||
if (args.Count == 4)
|
||||
unsafe
|
||||
{
|
||||
startInfo = JsonConvert.DeserializeObject<DalamudStartInfo>(Encoding.UTF8.GetString(Convert.FromBase64String(args[3])));
|
||||
args.RemoveAt(3);
|
||||
var argv = (IntPtr*)argvPtr;
|
||||
for (var i = 0; i < argc; i++)
|
||||
args.Add(Marshal.PtrToStringUni(argv[i]));
|
||||
}
|
||||
|
||||
Init(args);
|
||||
args.Remove("-v"); // Remove "verbose" flag
|
||||
|
||||
if (args.Count >= 2 && args[1].ToLowerInvariant() == "launch-test")
|
||||
{
|
||||
return ProcessLaunchTestCommand(args);
|
||||
}
|
||||
|
||||
DalamudStartInfo startInfo = null;
|
||||
if (args.Count == 1)
|
||||
{
|
||||
// No command defaults to inject
|
||||
args.Add("inject");
|
||||
args.Add("--all");
|
||||
|
||||
#if !DEBUG
|
||||
args.Add("--warn");
|
||||
#endif
|
||||
|
||||
}
|
||||
else if (int.TryParse(args[1], out var _))
|
||||
{
|
||||
// Assume that PID has been passed.
|
||||
args.Insert(1, "inject");
|
||||
|
||||
// If originally second parameter exists, then assume that it's a base64 encoded start info.
|
||||
// Dalamud.Injector.exe inject [pid] [base64]
|
||||
if (args.Count == 4)
|
||||
{
|
||||
startInfo = JsonConvert.DeserializeObject<DalamudStartInfo>(Encoding.UTF8.GetString(Convert.FromBase64String(args[3])));
|
||||
args.RemoveAt(3);
|
||||
}
|
||||
}
|
||||
|
||||
startInfo = ExtractAndInitializeStartInfoFromArguments(startInfo, args);
|
||||
// Remove already handled arguments
|
||||
args.Remove("--console");
|
||||
args.Remove("--msgbox1");
|
||||
args.Remove("--msgbox2");
|
||||
args.Remove("--msgbox3");
|
||||
args.Remove("--etw");
|
||||
args.Remove("--veh");
|
||||
args.Remove("--veh-full");
|
||||
args.Remove("--no-plugin");
|
||||
args.Remove("--no-3rd-plugin");
|
||||
args.Remove("--crash-handler-console");
|
||||
args.Remove("--no-exception-handlers");
|
||||
|
||||
var mainCommand = args[1].ToLowerInvariant();
|
||||
if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "inject"[..mainCommand.Length] == mainCommand)
|
||||
{
|
||||
return ProcessInjectCommand(args, startInfo);
|
||||
}
|
||||
else if (mainCommand.Length > 0 && mainCommand.Length <= 6 &&
|
||||
"launch"[..mainCommand.Length] == mainCommand)
|
||||
{
|
||||
return ProcessLaunchCommand(args, startInfo);
|
||||
}
|
||||
else if (mainCommand.Length > 0 && mainCommand.Length <= 4 &&
|
||||
"help"[..mainCommand.Length] == mainCommand)
|
||||
{
|
||||
return ProcessHelpCommand(args, args.Count >= 3 ? args[2] : null);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new CommandLineException($"\"{mainCommand}\" is not a valid command.");
|
||||
}
|
||||
}
|
||||
|
||||
startInfo = ExtractAndInitializeStartInfoFromArguments(startInfo, args);
|
||||
// Remove already handled arguments
|
||||
args.Remove("--console");
|
||||
args.Remove("--msgbox1");
|
||||
args.Remove("--msgbox2");
|
||||
args.Remove("--msgbox3");
|
||||
args.Remove("--etw");
|
||||
args.Remove("--veh");
|
||||
args.Remove("--veh-full");
|
||||
args.Remove("--no-plugin");
|
||||
args.Remove("--no-3rd-plugin");
|
||||
args.Remove("--crash-handler-console");
|
||||
args.Remove("--no-exception-handlers");
|
||||
|
||||
var mainCommand = args[1].ToLowerInvariant();
|
||||
if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "inject"[..mainCommand.Length] == mainCommand)
|
||||
catch (Exception e)
|
||||
{
|
||||
Environment.Exit(ProcessInjectCommand(args, startInfo));
|
||||
}
|
||||
else if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "launch"[..mainCommand.Length] == mainCommand)
|
||||
{
|
||||
Environment.Exit(ProcessLaunchCommand(args, startInfo));
|
||||
}
|
||||
else if (mainCommand.Length > 0 && mainCommand.Length <= 4 && "help"[..mainCommand.Length] == mainCommand)
|
||||
{
|
||||
Environment.Exit(ProcessHelpCommand(args, args.Count >= 3 ? args[2] : null));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new CommandLineException($"\"{mainCommand}\" is not a valid command.");
|
||||
Log.Error(e, "Operation failed.");
|
||||
return e.HResult;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -189,6 +200,7 @@ namespace Dalamud.Injector
|
|||
CullLogFile(logPath, 1 * 1024 * 1024);
|
||||
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.WriteTo.Console(standardErrorFromLevel: LogEventLevel.Debug)
|
||||
.WriteTo.File(logPath, fileSizeLimitBytes: null)
|
||||
.MinimumLevel.ControlledBy(levelSwitch)
|
||||
.CreateLogger();
|
||||
|
|
@ -377,12 +389,22 @@ namespace Dalamud.Injector
|
|||
#else
|
||||
startInfo.LogPath ??= xivlauncherDir;
|
||||
#endif
|
||||
startInfo.LogName ??= string.Empty;
|
||||
|
||||
// Set boot defaults
|
||||
startInfo.BootShowConsole = args.Contains("--console");
|
||||
startInfo.BootEnableEtw = args.Contains("--etw");
|
||||
startInfo.BootLogPath = GetLogPath(startInfo.LogPath, "dalamud.boot", startInfo.LogName);
|
||||
startInfo.BootEnabledGameFixes = new List<string> { "prevent_devicechange_crashes", "disable_game_openprocess_access_check", "redirect_openprocess", "backup_userdata_save", "prevent_icmphandle_crashes" };
|
||||
startInfo.BootEnabledGameFixes = new()
|
||||
{
|
||||
// See: xivfixes.h, xivfixes.cpp
|
||||
"prevent_devicechange_crashes",
|
||||
"disable_game_openprocess_access_check",
|
||||
"redirect_openprocess",
|
||||
"backup_userdata_save",
|
||||
"prevent_icmphandle_crashes",
|
||||
"symbol_load_patches",
|
||||
};
|
||||
startInfo.BootDotnetOpenProcessHookMode = 0;
|
||||
startInfo.BootWaitMessageBox |= args.Contains("--msgbox1") ? 1 : 0;
|
||||
startInfo.BootWaitMessageBox |= args.Contains("--msgbox2") ? 2 : 0;
|
||||
|
|
@ -800,12 +822,8 @@ namespace Dalamud.Injector
|
|||
{
|
||||
var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath);
|
||||
Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo));
|
||||
if (RewriteRemoteEntryPointW(p.Handle, gamePath, JsonConvert.SerializeObject(startInfo)) != 0)
|
||||
{
|
||||
Log.Error("[HOOKS] RewriteRemoteEntryPointW failed");
|
||||
throw new Exception("RewriteRemoteEntryPointW failed");
|
||||
}
|
||||
|
||||
Marshal.ThrowExceptionForHR(
|
||||
RewriteRemoteEntryPointW(p.Handle, gamePath, JsonConvert.SerializeObject(startInfo)));
|
||||
Log.Verbose("RewriteRemoteEntryPointW called!");
|
||||
}
|
||||
},
|
||||
|
|
|
|||
118
Dalamud.sln
118
Dalamud.sln
|
|
@ -6,8 +6,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
.gitignore = .gitignore
|
||||
targets\Dalamud.Plugin.targets = targets\Dalamud.Plugin.targets
|
||||
targets\Dalamud.Plugin.Bootstrap.targets = targets\Dalamud.Plugin.Bootstrap.targets
|
||||
targets\Dalamud.Plugin.targets = targets\Dalamud.Plugin.targets
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "build", "build\build.csproj", "{94E5B016-02B1-459B-97D9-E783F28764B2}"
|
||||
|
|
@ -38,184 +38,70 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs.InteropS
|
|||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DalamudCrashHandler", "DalamudCrashHandler\DalamudCrashHandler.vcxproj", "{317A264C-920B-44A1-8A34-F3A6827B0705}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.Common", "Dalamud.Common\Dalamud.Common.csproj", "{F21B13D2-D7D0-4456-B70F-3F8D695064E2}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Common", "Dalamud.Common\Dalamud.Common.csproj", "{F21B13D2-D7D0-4456-B70F-3F8D695064E2}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{94E5B016-02B1-459B-97D9-E783F28764B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{94E5B016-02B1-459B-97D9-E783F28764B2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{94E5B016-02B1-459B-97D9-E783F28764B2}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{94E5B016-02B1-459B-97D9-E783F28764B2}.Release|x64.Build.0 = Release|Any CPU
|
||||
{94E5B016-02B1-459B-97D9-E783F28764B2}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x64.Build.0 = Debug|x64
|
||||
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.Build.0 = Release|x64
|
||||
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x64.ActiveCfg = Release|x64
|
||||
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x64.Build.0 = Release|x64
|
||||
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x86.Build.0 = Release|Any CPU
|
||||
{55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|x64.Build.0 = Debug|x64
|
||||
{55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|x86.ActiveCfg = Debug|x64
|
||||
{55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|x86.Build.0 = Debug|x64
|
||||
{55198DC3-A03D-408E-A8EB-2077780C8576}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{55198DC3-A03D-408E-A8EB-2077780C8576}.Release|Any CPU.Build.0 = Release|x64
|
||||
{55198DC3-A03D-408E-A8EB-2077780C8576}.Release|x64.ActiveCfg = Release|x64
|
||||
{55198DC3-A03D-408E-A8EB-2077780C8576}.Release|x64.Build.0 = Release|x64
|
||||
{55198DC3-A03D-408E-A8EB-2077780C8576}.Release|x86.ActiveCfg = Release|x64
|
||||
{55198DC3-A03D-408E-A8EB-2077780C8576}.Release|x86.Build.0 = Release|x64
|
||||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x64.Build.0 = Debug|x64
|
||||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.Build.0 = Release|x64
|
||||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x64.ActiveCfg = Release|x64
|
||||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x64.Build.0 = Release|x64
|
||||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x86.Build.0 = Release|Any CPU
|
||||
{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}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|x64.Build.0 = Debug|x64
|
||||
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|x86.ActiveCfg = Debug|x64
|
||||
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|x86.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
|
||||
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|x64.ActiveCfg = Release|x64
|
||||
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|x64.Build.0 = Release|x64
|
||||
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|x86.ActiveCfg = Release|x64
|
||||
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|x64.Build.0 = Debug|x64
|
||||
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.Build.0 = Release|x64
|
||||
{C8004563-1806-4329-844F-0EF6274291FC}.Release|x64.ActiveCfg = Release|x64
|
||||
{C8004563-1806-4329-844F-0EF6274291FC}.Release|x64.Build.0 = Release|x64
|
||||
{C8004563-1806-4329-844F-0EF6274291FC}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{C8004563-1806-4329-844F-0EF6274291FC}.Release|x86.Build.0 = Release|Any CPU
|
||||
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x64.Build.0 = Debug|x64
|
||||
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x86.ActiveCfg = Debug|x64
|
||||
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x86.Build.0 = Debug|x64
|
||||
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.Build.0 = Release|x64
|
||||
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x64.ActiveCfg = Release|x64
|
||||
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x64.Build.0 = Release|x64
|
||||
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x86.ActiveCfg = Release|x64
|
||||
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x86.Build.0 = Release|x64
|
||||
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x64.Build.0 = Debug|x64
|
||||
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x86.ActiveCfg = Debug|x64
|
||||
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x86.Build.0 = Debug|x64
|
||||
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.Build.0 = Release|x64
|
||||
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x64.ActiveCfg = Release|x64
|
||||
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x64.Build.0 = Release|x64
|
||||
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x86.ActiveCfg = Release|x64
|
||||
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x86.Build.0 = Release|x64
|
||||
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|x64.Build.0 = Debug|x64
|
||||
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|x86.ActiveCfg = Debug|x64
|
||||
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|x86.Build.0 = Debug|x64
|
||||
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|Any CPU.Build.0 = Release|x64
|
||||
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|x64.ActiveCfg = Release|x64
|
||||
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|x64.Build.0 = Release|x64
|
||||
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|x86.ActiveCfg = Release|x64
|
||||
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|x86.Build.0 = Release|x64
|
||||
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|x64.Build.0 = Debug|x64
|
||||
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|x86.ActiveCfg = Debug|x64
|
||||
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|x86.Build.0 = Debug|x64
|
||||
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|Any CPU.Build.0 = Release|x64
|
||||
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|x64.ActiveCfg = Release|x64
|
||||
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|x64.Build.0 = Release|x64
|
||||
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|x86.ActiveCfg = Release|x64
|
||||
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|x86.Build.0 = Release|x64
|
||||
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|x64.Build.0 = Release|Any CPU
|
||||
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|x86.Build.0 = Release|Any CPU
|
||||
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|x64.Build.0 = Release|Any CPU
|
||||
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|x86.Build.0 = Release|Any CPU
|
||||
{317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|x64.Build.0 = Debug|x64
|
||||
{317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|x86.ActiveCfg = Debug|x64
|
||||
{317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|x86.Build.0 = Debug|x64
|
||||
{317A264C-920B-44A1-8A34-F3A6827B0705}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{317A264C-920B-44A1-8A34-F3A6827B0705}.Release|Any CPU.Build.0 = Release|x64
|
||||
{317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x64.ActiveCfg = Release|x64
|
||||
{317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x64.Build.0 = Release|x64
|
||||
{317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x86.ActiveCfg = Release|x64
|
||||
{317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x86.Build.0 = Release|x64
|
||||
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|x64.Build.0 = Release|Any CPU
|
||||
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System.IO;
|
|||
using System.Linq;
|
||||
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Interface.Internal.Windows.PluginInstaller;
|
||||
using Dalamud.Interface.Style;
|
||||
using Dalamud.IoC.Internal;
|
||||
|
|
@ -145,7 +146,13 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable
|
|||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use AXIS fonts from the game.
|
||||
/// </summary>
|
||||
public bool UseAxisFontsFromGame { get; set; } = false;
|
||||
[Obsolete($"See {nameof(DefaultFontSpec)}")]
|
||||
public bool UseAxisFontsFromGame { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default font spec.
|
||||
/// </summary>
|
||||
public IFontSpec? DefaultFontSpec { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the gamma value to apply for Dalamud fonts. Do not use.
|
||||
|
|
@ -208,6 +215,11 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable
|
|||
/// </summary>
|
||||
public bool LogOpenAtStartup { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of lines to keep for the Dalamud Console window.
|
||||
/// </summary>
|
||||
public int LogLinesLimit { get; set; } = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not the dev bar should open at startup.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Feature">
|
||||
<DalamudVersion>9.0.0.17</DalamudVersion>
|
||||
<DalamudVersion>9.0.0.21</DalamudVersion>
|
||||
<Description>XIV Launcher addon framework</Description>
|
||||
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
|
||||
<Version>$(DalamudVersion)</Version>
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
<PackageReference Include="goaaats.Reloaded.Hooks" Version="4.2.0-goat.4" />
|
||||
<PackageReference Include="goaaats.Reloaded.Assembler" Version="1.0.14-goat.2" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2021.2.0" />
|
||||
<PackageReference Include="Lumina" Version="3.15.2" />
|
||||
<PackageReference Include="Lumina" Version="3.16.0" />
|
||||
<PackageReference Include="Lumina.Excel" Version="6.5.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.46-beta">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using Dalamud.Hooking;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
|
@ -15,6 +17,11 @@ namespace Dalamud.Game.Config;
|
|||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
|
||||
{
|
||||
private readonly TaskCompletionSource tcsInitialization = new();
|
||||
private readonly TaskCompletionSource<GameConfigSection> tcsSystem = new();
|
||||
private readonly TaskCompletionSource<GameConfigSection> tcsUiConfig = new();
|
||||
private readonly TaskCompletionSource<GameConfigSection> tcsUiControl = new();
|
||||
|
||||
private readonly GameConfigAddressResolver address = new();
|
||||
private Hook<ConfigChangeDelegate>? configChangeHook;
|
||||
|
||||
|
|
@ -23,16 +30,32 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
|
|||
{
|
||||
framework.RunOnTick(() =>
|
||||
{
|
||||
Log.Verbose("[GameConfig] Initializing");
|
||||
var csFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance();
|
||||
var commonConfig = &csFramework->SystemConfig.CommonSystemConfig;
|
||||
this.System = new GameConfigSection("System", framework, &commonConfig->ConfigBase);
|
||||
this.UiConfig = new GameConfigSection("UiConfig", framework, &commonConfig->UiConfig);
|
||||
this.UiControl = new GameConfigSection("UiControl", framework, () => this.UiConfig.TryGetBool("PadMode", out var padMode) && padMode ? &commonConfig->UiControlGamepadConfig : &commonConfig->UiControlConfig);
|
||||
|
||||
this.address.Setup(sigScanner);
|
||||
this.configChangeHook = Hook<ConfigChangeDelegate>.FromAddress(this.address.ConfigChangeAddress, this.OnConfigChanged);
|
||||
this.configChangeHook.Enable();
|
||||
try
|
||||
{
|
||||
Log.Verbose("[GameConfig] Initializing");
|
||||
var csFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance();
|
||||
var commonConfig = &csFramework->SystemConfig.CommonSystemConfig;
|
||||
this.tcsSystem.SetResult(new("System", framework, &commonConfig->ConfigBase));
|
||||
this.tcsUiConfig.SetResult(new("UiConfig", framework, &commonConfig->UiConfig));
|
||||
this.tcsUiControl.SetResult(
|
||||
new(
|
||||
"UiControl",
|
||||
framework,
|
||||
() => this.UiConfig.TryGetBool("PadMode", out var padMode) && padMode
|
||||
? &commonConfig->UiControlGamepadConfig
|
||||
: &commonConfig->UiControlConfig));
|
||||
|
||||
this.address.Setup(sigScanner);
|
||||
this.configChangeHook = Hook<ConfigChangeDelegate>.FromAddress(
|
||||
this.address.ConfigChangeAddress,
|
||||
this.OnConfigChanged);
|
||||
this.configChangeHook.Enable();
|
||||
this.tcsInitialization.SetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.tcsInitialization.SetExceptionIfIncomplete(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -58,14 +81,19 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
|
|||
public event EventHandler<ConfigChangeEvent>? UiControlChanged;
|
||||
#pragma warning restore 67
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameConfigSection System { get; private set; }
|
||||
/// <summary>
|
||||
/// Gets a task representing the initialization state of this class.
|
||||
/// </summary>
|
||||
public Task InitializationTask => this.tcsInitialization.Task;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameConfigSection UiConfig { get; private set; }
|
||||
public GameConfigSection System => this.tcsSystem.Task.Result;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameConfigSection UiControl { get; private set; }
|
||||
public GameConfigSection UiConfig => this.tcsUiConfig.Task.Result;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameConfigSection UiControl => this.tcsUiControl.Task.Result;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryGet(SystemConfigOption option, out bool value) => this.System.TryGet(option.GetName(), out value);
|
||||
|
|
@ -169,6 +197,11 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
|
|||
/// <inheritdoc/>
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
var ode = new ObjectDisposedException(nameof(GameConfig));
|
||||
this.tcsInitialization.SetExceptionIfIncomplete(ode);
|
||||
this.tcsSystem.SetExceptionIfIncomplete(ode);
|
||||
this.tcsUiConfig.SetExceptionIfIncomplete(ode);
|
||||
this.tcsUiControl.SetExceptionIfIncomplete(ode);
|
||||
this.configChangeHook?.Disable();
|
||||
this.configChangeHook?.Dispose();
|
||||
}
|
||||
|
|
@ -220,15 +253,24 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig
|
|||
[ServiceManager.ServiceDependency]
|
||||
private readonly GameConfig gameConfigService = Service<GameConfig>.Get();
|
||||
|
||||
private readonly Task initializationTask;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameConfigPluginScoped"/> class.
|
||||
/// </summary>
|
||||
internal GameConfigPluginScoped()
|
||||
{
|
||||
this.gameConfigService.Changed += this.ConfigChangedForward;
|
||||
this.gameConfigService.System.Changed += this.SystemConfigChangedForward;
|
||||
this.gameConfigService.UiConfig.Changed += this.UiConfigConfigChangedForward;
|
||||
this.gameConfigService.UiControl.Changed += this.UiControlConfigChangedForward;
|
||||
this.initializationTask = this.gameConfigService.InitializationTask.ContinueWith(
|
||||
r =>
|
||||
{
|
||||
if (!r.IsCompletedSuccessfully)
|
||||
return r;
|
||||
this.gameConfigService.System.Changed += this.SystemConfigChangedForward;
|
||||
this.gameConfigService.UiConfig.Changed += this.UiConfigConfigChangedForward;
|
||||
this.gameConfigService.UiControl.Changed += this.UiControlConfigChangedForward;
|
||||
return Task.CompletedTask;
|
||||
}).Unwrap();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -256,9 +298,15 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig
|
|||
public void Dispose()
|
||||
{
|
||||
this.gameConfigService.Changed -= this.ConfigChangedForward;
|
||||
this.gameConfigService.System.Changed -= this.SystemConfigChangedForward;
|
||||
this.gameConfigService.UiConfig.Changed -= this.UiConfigConfigChangedForward;
|
||||
this.gameConfigService.UiControl.Changed -= this.UiControlConfigChangedForward;
|
||||
this.initializationTask.ContinueWith(
|
||||
r =>
|
||||
{
|
||||
if (!r.IsCompletedSuccessfully)
|
||||
return;
|
||||
this.gameConfigService.System.Changed -= this.SystemConfigChangedForward;
|
||||
this.gameConfigService.UiConfig.Changed -= this.UiConfigConfigChangedForward;
|
||||
this.gameConfigService.UiControl.Changed -= this.UiControlConfigChangedForward;
|
||||
});
|
||||
|
||||
this.Changed = null;
|
||||
this.SystemChanged = null;
|
||||
|
|
|
|||
560
Dalamud/Game/Gui/ContextMenu/ContextMenu.cs
Normal file
560
Dalamud/Game/Gui/ContextMenu/ContextMenu.cs
Normal file
|
|
@ -0,0 +1,560 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Memory;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Memory;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using FFXIVClientStructs.Interop;
|
||||
|
||||
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// This class handles interacting with the game's (right-click) context menu.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed unsafe class ContextMenu : IDisposable, IServiceType, IContextMenu
|
||||
{
|
||||
private static readonly ModuleLog Log = new("ContextMenu");
|
||||
|
||||
private readonly Hook<RaptureAtkModuleOpenAddonByAgentDelegate> raptureAtkModuleOpenAddonByAgentHook;
|
||||
private readonly Hook<AddonContextMenuOnMenuSelectedDelegate> addonContextMenuOnMenuSelectedHook;
|
||||
private readonly RaptureAtkModuleOpenAddonDelegate raptureAtkModuleOpenAddon;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private ContextMenu()
|
||||
{
|
||||
this.raptureAtkModuleOpenAddonByAgentHook = Hook<RaptureAtkModuleOpenAddonByAgentDelegate>.FromAddress((nint)RaptureAtkModule.Addresses.OpenAddonByAgent.Value, this.RaptureAtkModuleOpenAddonByAgentDetour);
|
||||
this.addonContextMenuOnMenuSelectedHook = Hook<AddonContextMenuOnMenuSelectedDelegate>.FromAddress((nint)AddonContextMenu.StaticVTable.OnMenuSelected, this.AddonContextMenuOnMenuSelectedDetour);
|
||||
this.raptureAtkModuleOpenAddon = Marshal.GetDelegateForFunctionPointer<RaptureAtkModuleOpenAddonDelegate>((nint)RaptureAtkModule.Addresses.OpenAddon.Value);
|
||||
|
||||
this.raptureAtkModuleOpenAddonByAgentHook.Enable();
|
||||
this.addonContextMenuOnMenuSelectedHook.Enable();
|
||||
}
|
||||
|
||||
private unsafe delegate ushort RaptureAtkModuleOpenAddonByAgentDelegate(RaptureAtkModule* module, byte* addonName, AtkUnitBase* addon, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, ushort parentAddonId);
|
||||
|
||||
private unsafe delegate bool AddonContextMenuOnMenuSelectedDelegate(AddonContextMenu* addon, int selectedIdx, byte a3);
|
||||
|
||||
private unsafe delegate ushort RaptureAtkModuleOpenAddonDelegate(RaptureAtkModule* a1, uint addonNameId, uint valueCount, AtkValue* values, AgentInterface* parentAgent, ulong unk, ushort parentAddonId, int unk2);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IContextMenu.OnMenuOpenedDelegate OnMenuOpened;
|
||||
|
||||
private Dictionary<ContextMenuType, List<MenuItem>> MenuItems { get; } = new();
|
||||
|
||||
private object MenuItemsLock { get; } = new();
|
||||
|
||||
private AgentInterface* SelectedAgent { get; set; }
|
||||
|
||||
private ContextMenuType? SelectedMenuType { get; set; }
|
||||
|
||||
private List<MenuItem>? SelectedItems { get; set; }
|
||||
|
||||
private HashSet<nint> SelectedEventInterfaces { get; } = new();
|
||||
|
||||
private AtkUnitBase* SelectedParentAddon { get; set; }
|
||||
|
||||
// -1 -> -inf: native items
|
||||
// 0 -> inf: selected items
|
||||
private List<int> MenuCallbackIds { get; } = new();
|
||||
|
||||
private IReadOnlyList<MenuItem>? SubmenuItems { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
var manager = RaptureAtkUnitManager.Instance();
|
||||
var menu = manager->GetAddonByName("ContextMenu");
|
||||
var submenu = manager->GetAddonByName("AddonContextSub");
|
||||
if (menu->IsVisible)
|
||||
menu->FireCallbackInt(-1);
|
||||
if (submenu->IsVisible)
|
||||
submenu->FireCallbackInt(-1);
|
||||
|
||||
this.raptureAtkModuleOpenAddonByAgentHook.Dispose();
|
||||
this.addonContextMenuOnMenuSelectedHook.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AddMenuItem(ContextMenuType menuType, MenuItem item)
|
||||
{
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
if (!this.MenuItems.TryGetValue(menuType, out var items))
|
||||
this.MenuItems[menuType] = items = new();
|
||||
items.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool RemoveMenuItem(ContextMenuType menuType, MenuItem item)
|
||||
{
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
if (!this.MenuItems.TryGetValue(menuType, out var items))
|
||||
return false;
|
||||
return items.Remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
private AtkValue* ExpandContextMenuArray(Span<AtkValue> oldValues, int newSize)
|
||||
{
|
||||
// if the array has enough room, don't reallocate
|
||||
if (oldValues.Length >= newSize)
|
||||
return (AtkValue*)Unsafe.AsPointer(ref oldValues[0]);
|
||||
|
||||
var size = (sizeof(AtkValue) * newSize) + 8;
|
||||
var newArray = (nint)IMemorySpace.GetUISpace()->Malloc((ulong)size, 0);
|
||||
if (newArray == nint.Zero)
|
||||
throw new OutOfMemoryException();
|
||||
NativeMemory.Fill((void*)newArray, (nuint)size, 0);
|
||||
|
||||
*(ulong*)newArray = (ulong)newSize;
|
||||
|
||||
// copy old memory if existing
|
||||
if (!oldValues.IsEmpty)
|
||||
oldValues.CopyTo(new((void*)(newArray + 8), oldValues.Length));
|
||||
|
||||
return (AtkValue*)(newArray + 8);
|
||||
}
|
||||
|
||||
private void FreeExpandedContextMenuArray(AtkValue* newValues, int newSize) =>
|
||||
IMemorySpace.Free((void*)((nint)newValues - 8), (ulong)((newSize * sizeof(AtkValue)) + 8));
|
||||
|
||||
private AtkValue* CreateEmptySubmenuContextMenuArray(SeString name, int x, int y, out int valueCount)
|
||||
{
|
||||
// 0: UInt = ContextItemCount
|
||||
// 1: String = Name
|
||||
// 2: Int = PositionX
|
||||
// 3: Int = PositionY
|
||||
// 4: Bool = false
|
||||
// 5: UInt = ContextItemSubmenuMask
|
||||
// 6: UInt = ReturnArrowMask (_gap_0x6BC ? 1 << (ContextItemCount - 1) : 0)
|
||||
// 7: UInt = 1
|
||||
|
||||
valueCount = 8;
|
||||
var values = this.ExpandContextMenuArray(Span<AtkValue>.Empty, valueCount);
|
||||
values[0].ChangeType(ValueType.UInt);
|
||||
values[0].UInt = 0;
|
||||
values[1].ChangeType(ValueType.String);
|
||||
values[1].SetString(name.Encode().NullTerminate());
|
||||
values[2].ChangeType(ValueType.Int);
|
||||
values[2].Int = x;
|
||||
values[3].ChangeType(ValueType.Int);
|
||||
values[3].Int = y;
|
||||
values[4].ChangeType(ValueType.Bool);
|
||||
values[4].Byte = 0;
|
||||
values[5].ChangeType(ValueType.UInt);
|
||||
values[5].UInt = 0;
|
||||
values[6].ChangeType(ValueType.UInt);
|
||||
values[6].UInt = 0;
|
||||
values[7].ChangeType(ValueType.UInt);
|
||||
values[7].UInt = 1;
|
||||
return values;
|
||||
}
|
||||
|
||||
private void SetupGenericMenu(int headerCount, int sizeHeaderIdx, int returnHeaderIdx, int submenuHeaderIdx, IReadOnlyList<MenuItem> items, ref int valueCount, ref AtkValue* values)
|
||||
{
|
||||
var itemsWithIdx = items.Select((item, idx) => (item, idx)).OrderBy(i => i.item.Priority);
|
||||
var prefixItems = itemsWithIdx.Where(i => i.item.Priority < 0).ToArray();
|
||||
var suffixItems = itemsWithIdx.Where(i => i.item.Priority >= 0).ToArray();
|
||||
|
||||
var nativeMenuSize = (int)values[sizeHeaderIdx].UInt;
|
||||
var prefixMenuSize = prefixItems.Length;
|
||||
var suffixMenuSize = suffixItems.Length;
|
||||
|
||||
var hasGameDisabled = valueCount - headerCount - nativeMenuSize > 0;
|
||||
|
||||
var hasCustomDisabled = items.Any(item => !item.IsEnabled);
|
||||
var hasAnyDisabled = hasGameDisabled || hasCustomDisabled;
|
||||
|
||||
values = this.ExpandContextMenuArray(
|
||||
new(values, valueCount),
|
||||
valueCount = (nativeMenuSize + items.Count) * (hasAnyDisabled ? 2 : 1) + headerCount);
|
||||
var offsetData = new Span<AtkValue>(values, headerCount);
|
||||
var nameData = new Span<AtkValue>(values + headerCount, nativeMenuSize + items.Count);
|
||||
var disabledData = hasAnyDisabled ? new Span<AtkValue>(values + headerCount + nativeMenuSize + items.Count, nativeMenuSize + items.Count) : Span<AtkValue>.Empty;
|
||||
|
||||
var returnMask = offsetData[returnHeaderIdx].UInt;
|
||||
var submenuMask = offsetData[submenuHeaderIdx].UInt;
|
||||
|
||||
nameData[..nativeMenuSize].CopyTo(nameData.Slice(prefixMenuSize, nativeMenuSize));
|
||||
if (hasAnyDisabled)
|
||||
{
|
||||
if (hasGameDisabled)
|
||||
{
|
||||
// copy old disabled data
|
||||
var oldDisabledData = new Span<AtkValue>(values + headerCount + nativeMenuSize, nativeMenuSize);
|
||||
oldDisabledData.CopyTo(disabledData.Slice(prefixMenuSize, nativeMenuSize));
|
||||
}
|
||||
else
|
||||
{
|
||||
// enable all
|
||||
for (var i = prefixMenuSize; i < prefixMenuSize + nativeMenuSize; ++i)
|
||||
{
|
||||
disabledData[i].ChangeType(ValueType.Int);
|
||||
disabledData[i].Int = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
returnMask <<= prefixMenuSize;
|
||||
submenuMask <<= prefixMenuSize;
|
||||
|
||||
void FillData(Span<AtkValue> disabledData, Span<AtkValue> nameData, int i, MenuItem item, int idx)
|
||||
{
|
||||
this.MenuCallbackIds.Add(idx);
|
||||
|
||||
if (hasAnyDisabled)
|
||||
{
|
||||
disabledData[i].ChangeType(ValueType.Int);
|
||||
disabledData[i].Int = item.IsEnabled ? 0 : 1;
|
||||
}
|
||||
|
||||
if (item.IsReturn)
|
||||
returnMask |= 1u << i;
|
||||
if (item.IsSubmenu)
|
||||
submenuMask |= 1u << i;
|
||||
|
||||
nameData[i].ChangeType(ValueType.String);
|
||||
nameData[i].SetString(item.PrefixedName.Encode().NullTerminate());
|
||||
}
|
||||
|
||||
for (var i = 0; i < prefixMenuSize; ++i)
|
||||
{
|
||||
var (item, idx) = prefixItems[i];
|
||||
FillData(disabledData, nameData, i, item, idx);
|
||||
}
|
||||
|
||||
this.MenuCallbackIds.AddRange(Enumerable.Range(0, nativeMenuSize).Select(i => -i - 1));
|
||||
|
||||
for (var i = prefixMenuSize + nativeMenuSize; i < prefixMenuSize + nativeMenuSize + suffixMenuSize; ++i)
|
||||
{
|
||||
var (item, idx) = suffixItems[i - prefixMenuSize - nativeMenuSize];
|
||||
FillData(disabledData, nameData, i, item, idx);
|
||||
}
|
||||
|
||||
offsetData[returnHeaderIdx].UInt = returnMask;
|
||||
offsetData[submenuHeaderIdx].UInt = submenuMask;
|
||||
|
||||
offsetData[sizeHeaderIdx].UInt += (uint)items.Count;
|
||||
}
|
||||
|
||||
private void SetupContextMenu(IReadOnlyList<MenuItem> items, ref int valueCount, ref AtkValue* values)
|
||||
{
|
||||
// 0: UInt = Item Count
|
||||
// 1: UInt = 0 (probably window name, just unused)
|
||||
// 2: UInt = Return Mask (?)
|
||||
// 3: UInt = Submenu Mask
|
||||
// 4: UInt = OpenAtCursorPosition ? 2 : 1
|
||||
// 5: UInt = 0
|
||||
// 6: UInt = 0
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (!item.Prefix.HasValue)
|
||||
{
|
||||
item.PrefixChar = 'D';
|
||||
item.PrefixColor = 539;
|
||||
Log.Warning($"Menu item \"{item.Name}\" has no prefix, defaulting to Dalamud's. Menu items outside of a submenu must have a prefix.");
|
||||
}
|
||||
}
|
||||
|
||||
this.SetupGenericMenu(7, 0, 2, 3, items, ref valueCount, ref values);
|
||||
}
|
||||
|
||||
private void SetupContextSubMenu(IReadOnlyList<MenuItem> items, ref int valueCount, ref AtkValue* values)
|
||||
{
|
||||
// 0: UInt = ContextItemCount
|
||||
// 1: skipped?
|
||||
// 2: Int = PositionX
|
||||
// 3: Int = PositionY
|
||||
// 4: Bool = false
|
||||
// 5: UInt = ContextItemSubmenuMask
|
||||
// 6: UInt = _gap_0x6BC ? 1 << (ContextItemCount - 1) : 0
|
||||
// 7: UInt = 1
|
||||
|
||||
this.SetupGenericMenu(8, 0, 6, 5, items, ref valueCount, ref values);
|
||||
}
|
||||
|
||||
private ushort RaptureAtkModuleOpenAddonByAgentDetour(RaptureAtkModule* module, byte* addonName, AtkUnitBase* addon, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, ushort parentAddonId)
|
||||
{
|
||||
var oldValues = values;
|
||||
|
||||
if (MemoryHelper.EqualsZeroTerminatedString("ContextMenu", (nint)addonName))
|
||||
{
|
||||
this.MenuCallbackIds.Clear();
|
||||
this.SelectedAgent = agent;
|
||||
this.SelectedParentAddon = module->RaptureAtkUnitManager.GetAddonById(parentAddonId);
|
||||
this.SelectedEventInterfaces.Clear();
|
||||
if (this.SelectedAgent == AgentInventoryContext.Instance())
|
||||
{
|
||||
this.SelectedMenuType = ContextMenuType.Inventory;
|
||||
}
|
||||
else if (this.SelectedAgent == AgentContext.Instance())
|
||||
{
|
||||
this.SelectedMenuType = ContextMenuType.Default;
|
||||
|
||||
var menu = AgentContext.Instance()->CurrentContextMenu;
|
||||
var handlers = new Span<Pointer<AtkEventInterface>>(menu->EventHandlerArray, 32);
|
||||
var ids = new Span<byte>(menu->EventIdArray, 32);
|
||||
var count = (int)values[0].UInt;
|
||||
handlers = handlers.Slice(7, count);
|
||||
ids = ids.Slice(7, count);
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
if (ids[i] <= 106)
|
||||
continue;
|
||||
this.SelectedEventInterfaces.Add((nint)handlers[i].Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.SelectedMenuType = null;
|
||||
}
|
||||
|
||||
this.SubmenuItems = null;
|
||||
|
||||
if (this.SelectedMenuType is { } menuType)
|
||||
{
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
if (this.MenuItems.TryGetValue(menuType, out var items))
|
||||
this.SelectedItems = new(items);
|
||||
else
|
||||
this.SelectedItems = new();
|
||||
}
|
||||
|
||||
var args = new MenuOpenedArgs(this.SelectedItems.Add, this.SelectedParentAddon, this.SelectedAgent, this.SelectedMenuType.Value, this.SelectedEventInterfaces);
|
||||
this.OnMenuOpened?.InvokeSafely(args);
|
||||
this.SelectedItems = this.FixupMenuList(this.SelectedItems, (int)values[0].UInt);
|
||||
this.SetupContextMenu(this.SelectedItems, ref valueCount, ref values);
|
||||
Log.Verbose($"Opening {this.SelectedMenuType} context menu with {this.SelectedItems.Count} custom items.");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.SelectedItems = null;
|
||||
}
|
||||
}
|
||||
else if (MemoryHelper.EqualsZeroTerminatedString("AddonContextSub", (nint)addonName))
|
||||
{
|
||||
this.MenuCallbackIds.Clear();
|
||||
if (this.SubmenuItems != null)
|
||||
{
|
||||
this.SubmenuItems = this.FixupMenuList(this.SubmenuItems.ToList(), (int)values[0].UInt);
|
||||
|
||||
this.SetupContextSubMenu(this.SubmenuItems, ref valueCount, ref values);
|
||||
Log.Verbose($"Opening {this.SelectedMenuType} submenu with {this.SubmenuItems.Count} custom items.");
|
||||
}
|
||||
}
|
||||
|
||||
var ret = this.raptureAtkModuleOpenAddonByAgentHook.Original(module, addonName, addon, valueCount, values, agent, a7, parentAddonId);
|
||||
if (values != oldValues)
|
||||
this.FreeExpandedContextMenuArray(values, valueCount);
|
||||
return ret;
|
||||
}
|
||||
|
||||
private List<MenuItem> FixupMenuList(List<MenuItem> items, int nativeMenuSize)
|
||||
{
|
||||
// The in game menu actually supports 32 items, but the last item can't have a visible submenu arrow.
|
||||
// As such, we'll only work with 31 items.
|
||||
const int MaxMenuItems = 31;
|
||||
if (items.Count + nativeMenuSize > MaxMenuItems)
|
||||
{
|
||||
Log.Warning($"Menu size exceeds {MaxMenuItems} items, truncating.");
|
||||
var orderedItems = items.OrderBy(i => i.Priority).ToArray();
|
||||
var newItems = orderedItems[..(MaxMenuItems - nativeMenuSize - 1)];
|
||||
var submenuItems = orderedItems[(MaxMenuItems - nativeMenuSize - 1)..];
|
||||
return newItems.Append(new MenuItem
|
||||
{
|
||||
Prefix = SeIconChar.BoxedLetterD,
|
||||
PrefixColor = 539,
|
||||
IsSubmenu = true,
|
||||
Priority = int.MaxValue,
|
||||
Name = $"See More ({submenuItems.Length})",
|
||||
OnClicked = a => a.OpenSubmenu(submenuItems),
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private void OpenSubmenu(SeString name, IReadOnlyList<MenuItem> submenuItems, int posX, int posY)
|
||||
{
|
||||
if (submenuItems.Count == 0)
|
||||
throw new ArgumentException("Submenu must not be empty", nameof(submenuItems));
|
||||
|
||||
this.SubmenuItems = submenuItems;
|
||||
|
||||
var module = RaptureAtkModule.Instance();
|
||||
var values = this.CreateEmptySubmenuContextMenuArray(name, posX, posY, out var valueCount);
|
||||
|
||||
switch (this.SelectedMenuType)
|
||||
{
|
||||
case ContextMenuType.Default:
|
||||
{
|
||||
var ownerAddonId = ((AgentContext*)this.SelectedAgent)->OwnerAddon;
|
||||
this.raptureAtkModuleOpenAddon(module, 445, (uint)valueCount, values, this.SelectedAgent, 71, checked((ushort)ownerAddonId), 4);
|
||||
break;
|
||||
}
|
||||
|
||||
case ContextMenuType.Inventory:
|
||||
{
|
||||
var ownerAddonId = ((AgentInventoryContext*)this.SelectedAgent)->OwnerAddonId;
|
||||
this.raptureAtkModuleOpenAddon(module, 445, (uint)valueCount, values, this.SelectedAgent, 0, checked((ushort)ownerAddonId), 4);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
Log.Warning($"Unknown context menu type (agent: {(nint)this.SelectedAgent}, cannot open submenu");
|
||||
break;
|
||||
}
|
||||
|
||||
this.FreeExpandedContextMenuArray(values, valueCount);
|
||||
}
|
||||
|
||||
private bool AddonContextMenuOnMenuSelectedDetour(AddonContextMenu* addon, int selectedIdx, byte a3)
|
||||
{
|
||||
var items = this.SubmenuItems ?? this.SelectedItems;
|
||||
if (items == null)
|
||||
goto original;
|
||||
if (this.MenuCallbackIds.Count == 0)
|
||||
goto original;
|
||||
if (selectedIdx < 0)
|
||||
goto original;
|
||||
if (selectedIdx >= this.MenuCallbackIds.Count)
|
||||
goto original;
|
||||
|
||||
var callbackId = this.MenuCallbackIds[selectedIdx];
|
||||
|
||||
if (callbackId < 0)
|
||||
{
|
||||
selectedIdx = -callbackId - 1;
|
||||
goto original;
|
||||
}
|
||||
else
|
||||
{
|
||||
var item = items[callbackId];
|
||||
var openedSubmenu = false;
|
||||
|
||||
try
|
||||
{
|
||||
if (item.OnClicked == null)
|
||||
throw new InvalidOperationException("Item has no OnClicked handler");
|
||||
item.OnClicked.InvokeSafely(new(
|
||||
(name, items) =>
|
||||
{
|
||||
short x, y;
|
||||
addon->AtkUnitBase.GetPosition(&x, &y);
|
||||
this.OpenSubmenu(name ?? item.Name, items, x, y);
|
||||
openedSubmenu = true;
|
||||
},
|
||||
this.SelectedParentAddon,
|
||||
this.SelectedAgent,
|
||||
this.SelectedMenuType.Value,
|
||||
this.SelectedEventInterfaces));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Error while handling context menu click");
|
||||
}
|
||||
|
||||
// Close with clicky sound
|
||||
if (!openedSubmenu)
|
||||
addon->AtkUnitBase.FireCallbackInt(-2);
|
||||
return false;
|
||||
}
|
||||
|
||||
original:
|
||||
// Eventually handled by inventorycontext here: 14022BBD0 (6.51)
|
||||
return this.addonContextMenuOnMenuSelectedHook.Original(addon, selectedIdx, a3);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plugin-scoped version of a <see cref="ContextMenu"/> service.
|
||||
/// </summary>
|
||||
[PluginInterface]
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.ScopedService]
|
||||
#pragma warning disable SA1015
|
||||
[ResolveVia<IContextMenu>]
|
||||
#pragma warning restore SA1015
|
||||
internal class ContextMenuPluginScoped : IDisposable, IServiceType, IContextMenu
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly ContextMenu parentService = Service<ContextMenu>.Get();
|
||||
|
||||
private ContextMenuPluginScoped()
|
||||
{
|
||||
this.parentService.OnMenuOpened += this.OnMenuOpenedForward;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IContextMenu.OnMenuOpenedDelegate OnMenuOpened;
|
||||
|
||||
private Dictionary<ContextMenuType, List<MenuItem>> MenuItems { get; } = new();
|
||||
|
||||
private object MenuItemsLock { get; } = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.parentService.OnMenuOpened -= this.OnMenuOpenedForward;
|
||||
|
||||
this.OnMenuOpened = null;
|
||||
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
foreach (var (menuType, items) in this.MenuItems)
|
||||
{
|
||||
foreach (var item in items)
|
||||
this.parentService.RemoveMenuItem(menuType, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AddMenuItem(ContextMenuType menuType, MenuItem item)
|
||||
{
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
if (!this.MenuItems.TryGetValue(menuType, out var items))
|
||||
this.MenuItems[menuType] = items = new();
|
||||
items.Add(item);
|
||||
}
|
||||
|
||||
this.parentService.AddMenuItem(menuType, item);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool RemoveMenuItem(ContextMenuType menuType, MenuItem item)
|
||||
{
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
if (this.MenuItems.TryGetValue(menuType, out var items))
|
||||
items.Remove(item);
|
||||
}
|
||||
|
||||
return this.parentService.RemoveMenuItem(menuType, item);
|
||||
}
|
||||
|
||||
private void OnMenuOpenedForward(MenuOpenedArgs args) =>
|
||||
this.OnMenuOpened?.Invoke(args);
|
||||
}
|
||||
18
Dalamud/Game/Gui/ContextMenu/ContextMenuType.cs
Normal file
18
Dalamud/Game/Gui/ContextMenu/ContextMenuType.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// The type of context menu.
|
||||
/// Each one has a different associated <see cref="MenuTarget"/>.
|
||||
/// </summary>
|
||||
public enum ContextMenuType
|
||||
{
|
||||
/// <summary>
|
||||
/// The default context menu.
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// The inventory context menu. Used when right-clicked on an item.
|
||||
/// </summary>
|
||||
Inventory,
|
||||
}
|
||||
77
Dalamud/Game/Gui/ContextMenu/MenuArgs.cs
Normal file
77
Dalamud/Game/Gui/ContextMenu/MenuArgs.cs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Memory;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for <see cref="IContextMenu"/> menu args.
|
||||
/// </summary>
|
||||
public abstract unsafe class MenuArgs
|
||||
{
|
||||
private IReadOnlySet<nint>? eventInterfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MenuArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="addon">Addon associated with the context menu.</param>
|
||||
/// <param name="agent">Agent associated with the context menu.</param>
|
||||
/// <param name="type">The type of context menu.</param>
|
||||
/// <param name="eventInterfaces">List of AtkEventInterfaces associated with the context menu.</param>
|
||||
protected internal MenuArgs(AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet<nint>? eventInterfaces)
|
||||
{
|
||||
this.AddonName = addon != null ? MemoryHelper.ReadString((nint)addon->Name, 32) : null;
|
||||
this.AddonPtr = (nint)addon;
|
||||
this.AgentPtr = (nint)agent;
|
||||
this.MenuType = type;
|
||||
this.eventInterfaces = eventInterfaces;
|
||||
this.Target = type switch
|
||||
{
|
||||
ContextMenuType.Default => new MenuTargetDefault((AgentContext*)agent),
|
||||
ContextMenuType.Inventory => new MenuTargetInventory((AgentInventoryContext*)agent),
|
||||
_ => throw new ArgumentException("Invalid context menu type", nameof(type)),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the addon that opened the context menu.
|
||||
/// </summary>
|
||||
public string? AddonName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the memory pointer of the addon that opened the context menu.
|
||||
/// </summary>
|
||||
public nint AddonPtr { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the memory pointer of the agent that opened the context menu.
|
||||
/// </summary>
|
||||
public nint AgentPtr { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the context menu.
|
||||
/// </summary>
|
||||
public ContextMenuType MenuType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target info of the context menu. The actual type depends on <see cref="MenuType"/>.
|
||||
/// <see cref="ContextMenuType.Default"/> signifies a <see cref="MenuTargetDefault"/>.
|
||||
/// <see cref="ContextMenuType.Inventory"/> signifies a <see cref="MenuTargetInventory"/>.
|
||||
/// </summary>
|
||||
public MenuTarget Target { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of AtkEventInterface pointers associated with the context menu.
|
||||
/// Only available with <see cref="ContextMenuType.Default"/>.
|
||||
/// Almost always an agent pointer. You can use this to find out what type of context menu it is.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the context menu is not a <see cref="ContextMenuType.Default"/>.</exception>
|
||||
public IReadOnlySet<nint> EventInterfaces =>
|
||||
this.MenuType != ContextMenuType.Default ?
|
||||
this.eventInterfaces :
|
||||
throw new InvalidOperationException("Not a default context menu");
|
||||
}
|
||||
91
Dalamud/Game/Gui/ContextMenu/MenuItem.cs
Normal file
91
Dalamud/Game/Gui/ContextMenu/MenuItem.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// A menu item that can be added to a context menu.
|
||||
/// </summary>
|
||||
public sealed record MenuItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the display name of the menu item.
|
||||
/// </summary>
|
||||
public SeString Name { get; set; } = SeString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the prefix attached to the beginning of <see cref="Name"/>.
|
||||
/// </summary>
|
||||
public SeIconChar? Prefix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the character to prefix the <see cref="Name"/> with. Will be converted into a fancy boxed letter icon. Must be an uppercase letter.
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentException"><paramref name="value"/> must be an uppercase letter.</exception>
|
||||
public char? PrefixChar
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value is { } prefix)
|
||||
{
|
||||
if (!char.IsAsciiLetterUpper(prefix))
|
||||
throw new ArgumentException("Prefix must be an uppercase letter", nameof(value));
|
||||
|
||||
this.Prefix = SeIconChar.BoxedLetterA + prefix - 'A';
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Prefix = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color of the <see cref="Prefix"/>. Specifies a <see cref="UIColor"/> row id.
|
||||
/// </summary>
|
||||
public ushort PrefixColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callback to be invoked when the menu item is clicked.
|
||||
/// </summary>
|
||||
public Action<MenuItemClickedArgs>? OnClicked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the priority (or order) with which the menu item should be displayed in descending order.
|
||||
/// Priorities below 0 will be displayed above the native menu items.
|
||||
/// Other priorities will be displayed below the native menu items.
|
||||
/// </summary>
|
||||
public int Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the menu item is enabled.
|
||||
/// Disabled items will be faded and cannot be clicked on.
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the menu item is a submenu.
|
||||
/// This value is purely visual. Submenu items will have an arrow to its right.
|
||||
/// </summary>
|
||||
public bool IsSubmenu { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the menu item is a return item.
|
||||
/// This value is purely visual. Return items will have a back arrow to its left.
|
||||
/// If both <see cref="IsSubmenu"/> and <see cref="IsReturn"/> are true, the return arrow will take precedence.
|
||||
/// </summary>
|
||||
public bool IsReturn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name with the given prefix.
|
||||
/// </summary>
|
||||
internal SeString PrefixedName =>
|
||||
this.Prefix is { } prefix
|
||||
? new SeStringBuilder()
|
||||
.AddUiForeground($"{prefix.ToIconString()} ", this.PrefixColor)
|
||||
.Append(this.Name)
|
||||
.Build()
|
||||
: this.Name;
|
||||
}
|
||||
44
Dalamud/Game/Gui/ContextMenu/MenuItemClickedArgs.cs
Normal file
44
Dalamud/Game/Gui/ContextMenu/MenuItemClickedArgs.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Callback args used when a menu item is clicked.
|
||||
/// </summary>
|
||||
public sealed unsafe class MenuItemClickedArgs : MenuArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MenuItemClickedArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="openSubmenu">Callback for opening a submenu.</param>
|
||||
/// <param name="addon">Addon associated with the context menu.</param>
|
||||
/// <param name="agent">Agent associated with the context menu.</param>
|
||||
/// <param name="type">The type of context menu.</param>
|
||||
/// <param name="eventInterfaces">List of AtkEventInterfaces associated with the context menu.</param>
|
||||
internal MenuItemClickedArgs(Action<SeString?, IReadOnlyList<MenuItem>> openSubmenu, AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet<nint> eventInterfaces)
|
||||
: base(addon, agent, type, eventInterfaces)
|
||||
{
|
||||
this.OnOpenSubmenu = openSubmenu;
|
||||
}
|
||||
|
||||
private Action<SeString?, IReadOnlyList<MenuItem>> OnOpenSubmenu { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Opens a submenu with the given name and items.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the submenu, displayed at the top.</param>
|
||||
/// <param name="items">The items to display in the submenu.</param>
|
||||
public void OpenSubmenu(SeString name, IReadOnlyList<MenuItem> items) =>
|
||||
this.OnOpenSubmenu(name, items);
|
||||
|
||||
/// <summary>
|
||||
/// Opens a submenu with the given items.
|
||||
/// </summary>
|
||||
/// <param name="items">The items to display in the submenu.</param>
|
||||
public void OpenSubmenu(IReadOnlyList<MenuItem> items) =>
|
||||
this.OnOpenSubmenu(null, items);
|
||||
}
|
||||
34
Dalamud/Game/Gui/ContextMenu/MenuOpenedArgs.cs
Normal file
34
Dalamud/Game/Gui/ContextMenu/MenuOpenedArgs.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Callback args used when a menu item is opened.
|
||||
/// </summary>
|
||||
public sealed unsafe class MenuOpenedArgs : MenuArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MenuOpenedArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="addMenuItem">Callback for adding a custom menu item.</param>
|
||||
/// <param name="addon">Addon associated with the context menu.</param>
|
||||
/// <param name="agent">Agent associated with the context menu.</param>
|
||||
/// <param name="type">The type of context menu.</param>
|
||||
/// <param name="eventInterfaces">List of AtkEventInterfaces associated with the context menu.</param>
|
||||
internal MenuOpenedArgs(Action<MenuItem> addMenuItem, AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet<nint> eventInterfaces)
|
||||
: base(addon, agent, type, eventInterfaces)
|
||||
{
|
||||
this.OnAddMenuItem = addMenuItem;
|
||||
}
|
||||
|
||||
private Action<MenuItem> OnAddMenuItem { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds a custom menu item to the context menu.
|
||||
/// </summary>
|
||||
/// <param name="item">The menu item to add.</param>
|
||||
public void AddMenuItem(MenuItem item) =>
|
||||
this.OnAddMenuItem(item);
|
||||
}
|
||||
9
Dalamud/Game/Gui/ContextMenu/MenuTarget.cs
Normal file
9
Dalamud/Game/Gui/ContextMenu/MenuTarget.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for <see cref="MenuArgs"/> contexts.
|
||||
/// Discriminated based on <see cref="ContextMenuType"/>.
|
||||
/// </summary>
|
||||
public abstract class MenuTarget
|
||||
{
|
||||
}
|
||||
67
Dalamud/Game/Gui/ContextMenu/MenuTargetDefault.cs
Normal file
67
Dalamud/Game/Gui/ContextMenu/MenuTargetDefault.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Game.ClientState.Resolvers;
|
||||
using Dalamud.Game.Network.Structures.InfoProxy;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Target information on a default context menu.
|
||||
/// </summary>
|
||||
public sealed unsafe class MenuTargetDefault : MenuTarget
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MenuTargetDefault"/> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The agent associated with the context menu.</param>
|
||||
internal MenuTargetDefault(AgentContext* context)
|
||||
{
|
||||
this.Context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the target.
|
||||
/// </summary>
|
||||
public string TargetName => this.Context->TargetName.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the object id of the target.
|
||||
/// </summary>
|
||||
public ulong TargetObjectId => this.Context->TargetObjectId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target object.
|
||||
/// </summary>
|
||||
public GameObject? TargetObject => Service<ObjectTable>.Get().SearchById(this.TargetObjectId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content id of the target.
|
||||
/// </summary>
|
||||
public ulong TargetContentId => this.Context->TargetContentId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the home world id of the target.
|
||||
/// </summary>
|
||||
public ExcelResolver<World> TargetHomeWorld => new((uint)this.Context->TargetHomeWorldId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently targeted character. Only shows up for specific targets, like friends, party finder listings, or party members.
|
||||
/// Just because this is <see langword="null"/> doesn't mean the target isn't a character.
|
||||
/// </summary>
|
||||
public CharacterData? TargetCharacter
|
||||
{
|
||||
get
|
||||
{
|
||||
var target = this.Context->CurrentContextMenuTarget;
|
||||
if (target != null)
|
||||
return new(target);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private AgentContext* Context { get; }
|
||||
}
|
||||
36
Dalamud/Game/Gui/ContextMenu/MenuTargetInventory.cs
Normal file
36
Dalamud/Game/Gui/ContextMenu/MenuTargetInventory.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
using Dalamud.Game.Inventory;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Target information on an inventory context menu.
|
||||
/// </summary>
|
||||
public sealed unsafe class MenuTargetInventory : MenuTarget
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MenuTargetInventory"/> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The agent associated with the context menu.</param>
|
||||
internal MenuTargetInventory(AgentInventoryContext* context)
|
||||
{
|
||||
this.Context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target item.
|
||||
/// </summary>
|
||||
public GameInventoryItem? TargetItem
|
||||
{
|
||||
get
|
||||
{
|
||||
var target = this.Context->TargetInventorySlot;
|
||||
if (target != null)
|
||||
return new(*target);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private AgentInventoryContext* Context { get; }
|
||||
}
|
||||
|
|
@ -107,7 +107,7 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType
|
|||
|
||||
private IntPtr AtkUnitBaseReceiveGlobalEventDetour(AtkUnitBase* thisPtr, ushort cmd, uint a3, IntPtr a4, uint* arg)
|
||||
{
|
||||
// Log.Information("{0}: cmd#{1} a3#{2} - HasAnyFocus:{3}", Marshal.PtrToStringAnsi(new IntPtr(thisPtr->Name)), cmd, a3, WindowSystem.HasAnyWindowSystemFocus);
|
||||
// Log.Information("{0}: cmd#{1} a3#{2} - HasAnyFocus:{3}", MemoryHelper.ReadSeStringAsString(out _, new IntPtr(thisPtr->Name)), cmd, a3, WindowSystem.HasAnyWindowSystemFocus);
|
||||
|
||||
// "SendHotkey"
|
||||
// 3 == Close
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
using System.Diagnostics;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
|
||||
namespace Dalamud.Game.Inventory;
|
||||
|
|
@ -103,8 +106,10 @@ public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
|
|||
/// <summary>
|
||||
/// Gets the array of materia grades.
|
||||
/// </summary>
|
||||
// TODO: Replace with MateriaGradeBytes
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
public ReadOnlySpan<ushort> MateriaGrade =>
|
||||
new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.MateriaGrade[0])), 5);
|
||||
this.MateriaGradeBytes.ToArray().Select(g => (ushort)g).ToArray().AsSpan();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of native inventory item in the game.<br />
|
||||
|
|
@ -146,6 +151,9 @@ public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
|
|||
/// </summary>
|
||||
internal ulong CrafterContentId => this.InternalItem.CrafterContentID;
|
||||
|
||||
private ReadOnlySpan<byte> MateriaGradeBytes =>
|
||||
new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.MateriaGrade[0])), 5);
|
||||
|
||||
public static bool operator ==(in GameInventoryItem l, in GameInventoryItem r) => l.Equals(r);
|
||||
|
||||
public static bool operator !=(in GameInventoryItem l, in GameInventoryItem r) => !l.Equals(r);
|
||||
|
|
|
|||
197
Dalamud/Game/Network/Structures/InfoProxy/CharacterData.cs
Normal file
197
Dalamud/Game/Network/Structures/InfoProxy/CharacterData.cs
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Game.ClientState.Resolvers;
|
||||
using Dalamud.Memory;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
||||
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
namespace Dalamud.Game.Network.Structures.InfoProxy;
|
||||
|
||||
/// <summary>
|
||||
/// Dalamud wrapper around a client structs <see cref="InfoProxyCommonList.CharacterData"/>.
|
||||
/// </summary>
|
||||
public unsafe class CharacterData
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CharacterData"/> class.
|
||||
/// </summary>
|
||||
/// <param name="data">Character data to wrap.</param>
|
||||
internal CharacterData(InfoProxyCommonList.CharacterData* data)
|
||||
{
|
||||
this.Address = (nint)data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the <see cref="InfoProxyCommonList.CharacterData"/> in memory.
|
||||
/// </summary>
|
||||
public nint Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content id of the character.
|
||||
/// </summary>
|
||||
public ulong ContentId => this.Struct->ContentId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the status mask of the character.
|
||||
/// </summary>
|
||||
public ulong StatusMask => (ulong)this.Struct->State;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the applicable statues of the character.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ExcelResolver<OnlineStatus>> Statuses
|
||||
{
|
||||
get
|
||||
{
|
||||
var statuses = new List<ExcelResolver<OnlineStatus>>();
|
||||
for (var i = 0; i < 64; i++)
|
||||
{
|
||||
if ((this.StatusMask & (1UL << i)) != 0)
|
||||
statuses.Add(new((uint)i));
|
||||
}
|
||||
|
||||
return statuses;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display group of the character.
|
||||
/// </summary>
|
||||
public DisplayGroup DisplayGroup => (DisplayGroup)this.Struct->Group;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the character's home world is different from the current world.
|
||||
/// </summary>
|
||||
public bool IsFromOtherServer => this.Struct->IsOtherServer;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sort order of the character.
|
||||
/// </summary>
|
||||
public byte Sort => this.Struct->Sort;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current world of the character.
|
||||
/// </summary>
|
||||
public ExcelResolver<World> CurrentWorld => new(this.Struct->CurrentWorld);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the home world of the character.
|
||||
/// </summary>
|
||||
public ExcelResolver<World> HomeWorld => new(this.Struct->HomeWorld);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the location of the character.
|
||||
/// </summary>
|
||||
public ExcelResolver<TerritoryType> Location => new(this.Struct->Location);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the grand company of the character.
|
||||
/// </summary>
|
||||
public ExcelResolver<GrandCompany> GrandCompany => new((uint)this.Struct->GrandCompany);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the primary client language of the character.
|
||||
/// </summary>
|
||||
public ClientLanguage ClientLanguage => (ClientLanguage)this.Struct->ClientLanguage;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the supported language mask of the character.
|
||||
/// </summary>
|
||||
public byte LanguageMask => (byte)this.Struct->Languages;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the supported languages the character supports.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ClientLanguage> Languages
|
||||
{
|
||||
get
|
||||
{
|
||||
var languages = new List<ClientLanguage>();
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
if ((this.LanguageMask & (1 << i)) != 0)
|
||||
languages.Add((ClientLanguage)i);
|
||||
}
|
||||
|
||||
return languages;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the gender of the character.
|
||||
/// </summary>
|
||||
public byte Gender => this.Struct->Sex;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the job of the character.
|
||||
/// </summary>
|
||||
public ExcelResolver<ClassJob> ClassJob => new(this.Struct->Job);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the character.
|
||||
/// </summary>
|
||||
public string Name => MemoryHelper.ReadString((nint)this.Struct->Name, 32);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the free company tag of the character.
|
||||
/// </summary>
|
||||
public string FCTag => MemoryHelper.ReadString((nint)this.Struct->Name, 6);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying <see cref="InfoProxyCommonList.CharacterData"/> struct.
|
||||
/// </summary>
|
||||
internal InfoProxyCommonList.CharacterData* Struct => (InfoProxyCommonList.CharacterData*)this.Address;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display group of a character. Used for friends.
|
||||
/// </summary>
|
||||
public enum DisplayGroup : sbyte
|
||||
{
|
||||
/// <summary>
|
||||
/// All display groups.
|
||||
/// </summary>
|
||||
All = -1,
|
||||
|
||||
/// <summary>
|
||||
/// No display group.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Star display group.
|
||||
/// </summary>
|
||||
Star,
|
||||
|
||||
/// <summary>
|
||||
/// Circle display group.
|
||||
/// </summary>
|
||||
Circle,
|
||||
|
||||
/// <summary>
|
||||
/// Triangle display group.
|
||||
/// </summary>
|
||||
Triangle,
|
||||
|
||||
/// <summary>
|
||||
/// Diamond display group.
|
||||
/// </summary>
|
||||
Diamond,
|
||||
|
||||
/// <summary>
|
||||
/// Heart display group.
|
||||
/// </summary>
|
||||
Heart,
|
||||
|
||||
/// <summary>
|
||||
/// Spade display group.
|
||||
/// </summary>
|
||||
Spade,
|
||||
|
||||
/// <summary>
|
||||
/// Club display group.
|
||||
/// </summary>
|
||||
Club,
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Storage.Assets;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using TerraFX.Interop.DirectX;
|
||||
|
||||
namespace Dalamud.Interface.FontIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a font from Dalamud assets.
|
||||
/// </summary>
|
||||
public sealed class DalamudAssetFontAndFamilyId : IFontFamilyId, IFontId
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DalamudAssetFontAndFamilyId"/> class.
|
||||
/// </summary>
|
||||
/// <param name="asset">The font asset.</param>
|
||||
public DalamudAssetFontAndFamilyId(DalamudAsset asset)
|
||||
{
|
||||
if (asset.GetPurpose() != DalamudAssetPurpose.Font)
|
||||
throw new ArgumentOutOfRangeException(nameof(asset), asset, "The specified asset is not a font asset.");
|
||||
this.Asset = asset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the font asset.
|
||||
/// </summary>
|
||||
[JsonProperty]
|
||||
public DalamudAsset Asset { get; init; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public string EnglishName => $"Dalamud: {this.Asset}";
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public IReadOnlyDictionary<string, string>? LocaleNames => null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public IReadOnlyList<IFontId> Fonts => new List<IFontId> { this }.AsReadOnly();
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public IFontFamilyId Family => this;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public int Weight => (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public int Stretch => (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public int Style => (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL;
|
||||
|
||||
public static bool operator ==(DalamudAssetFontAndFamilyId? left, DalamudAssetFontAndFamilyId? right) =>
|
||||
Equals(left, right);
|
||||
|
||||
public static bool operator !=(DalamudAssetFontAndFamilyId? left, DalamudAssetFontAndFamilyId? right) =>
|
||||
!Equals(left, right);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) => obj is DalamudAssetFontAndFamilyId other && this.Equals(other);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => (int)this.Asset;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => $"{nameof(DalamudAssetFontAndFamilyId)}:{this.Asset}";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int FindBestMatch(int weight, int stretch, int style) => 0;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config) =>
|
||||
tk.AddDalamudAssetFont(this.Asset, config);
|
||||
|
||||
private bool Equals(DalamudAssetFontAndFamilyId other) => this.Asset == other.Asset;
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using TerraFX.Interop.DirectX;
|
||||
|
||||
namespace Dalamud.Interface.FontIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the default Dalamud font.
|
||||
/// </summary>
|
||||
public sealed class DalamudDefaultFontAndFamilyId : IFontId, IFontFamilyId
|
||||
{
|
||||
/// <summary>
|
||||
/// The shared instance of <see cref="DalamudDefaultFontAndFamilyId"/>.
|
||||
/// </summary>
|
||||
public static readonly DalamudDefaultFontAndFamilyId Instance = new();
|
||||
|
||||
private DalamudDefaultFontAndFamilyId()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public string EnglishName => "(Default)";
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public IReadOnlyDictionary<string, string>? LocaleNames => null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public IFontFamilyId Family => this;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public int Weight => (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public int Stretch => (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public int Style => (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public IReadOnlyList<IFontId> Fonts => new List<IFontId> { this }.AsReadOnly();
|
||||
|
||||
public static bool operator ==(DalamudDefaultFontAndFamilyId? left, DalamudDefaultFontAndFamilyId? right) =>
|
||||
left is null == right is null;
|
||||
|
||||
public static bool operator !=(DalamudDefaultFontAndFamilyId? left, DalamudDefaultFontAndFamilyId? right) =>
|
||||
left is null != right is null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) => obj is DalamudDefaultFontAndFamilyId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => 12345678;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => nameof(DalamudDefaultFontAndFamilyId);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config)
|
||||
=> tk.AddDalamudDefaultFont(config.SizePx, config.GlyphRanges);
|
||||
// TODO: mergeFont
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int FindBestMatch(int weight, int stretch, int style) => 0;
|
||||
}
|
||||
81
Dalamud/Interface/FontIdentifier/GameFontAndFamilyId.cs
Normal file
81
Dalamud/Interface/FontIdentifier/GameFontAndFamilyId.cs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using TerraFX.Interop.DirectX;
|
||||
|
||||
namespace Dalamud.Interface.FontIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a font from the game.
|
||||
/// </summary>
|
||||
public sealed class GameFontAndFamilyId : IFontId, IFontFamilyId
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameFontAndFamilyId"/> class.
|
||||
/// </summary>
|
||||
/// <param name="family">The game font family.</param>
|
||||
public GameFontAndFamilyId(GameFontFamily family) => this.GameFontFamily = family;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the game font family.
|
||||
/// </summary>
|
||||
[JsonProperty]
|
||||
public GameFontFamily GameFontFamily { get; init; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public string EnglishName => $"Game: {Enum.GetName(this.GameFontFamily) ?? throw new NotSupportedException()}";
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public IReadOnlyDictionary<string, string>? LocaleNames => null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public IFontFamilyId Family => this;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public int Weight => (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public int Stretch => (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public int Style => (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public IReadOnlyList<IFontId> Fonts => new List<IFontId> { this }.AsReadOnly();
|
||||
|
||||
public static bool operator ==(GameFontAndFamilyId? left, GameFontAndFamilyId? right) => Equals(left, right);
|
||||
|
||||
public static bool operator !=(GameFontAndFamilyId? left, GameFontAndFamilyId? right) => !Equals(left, right);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) =>
|
||||
ReferenceEquals(this, obj) || (obj is GameFontAndFamilyId other && this.Equals(other));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => (int)this.GameFontFamily;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int FindBestMatch(int weight, int stretch, int style) => 0;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => $"{nameof(GameFontAndFamilyId)}:{this.GameFontFamily}";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config) =>
|
||||
tk.AddGameGlyphs(new(this.GameFontFamily, config.SizePx), config.GlyphRanges, config.MergeFont);
|
||||
|
||||
private bool Equals(GameFontAndFamilyId other) => this.GameFontFamily == other.GameFontFamily;
|
||||
}
|
||||
102
Dalamud/Interface/FontIdentifier/IFontFamilyId.cs
Normal file
102
Dalamud/Interface/FontIdentifier/IFontFamilyId.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using TerraFX.Interop.DirectX;
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
namespace Dalamud.Interface.FontIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a font family identifier.<br />
|
||||
/// Not intended for plugins to implement.
|
||||
/// </summary>
|
||||
public interface IFontFamilyId : IObjectWithLocalizableName
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the list of fonts under this family.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
IReadOnlyList<IFontId> Fonts { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Finds the index of the font inside <see cref="Fonts"/> that best matches the given parameters.
|
||||
/// </summary>
|
||||
/// <param name="weight">The weight of the font.</param>
|
||||
/// <param name="stretch">The stretch of the font.</param>
|
||||
/// <param name="style">The style of the font.</param>
|
||||
/// <returns>The index of the font. Guaranteed to be a valid index.</returns>
|
||||
int FindBestMatch(int weight, int stretch, int style);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of Dalamud-provided fonts.
|
||||
/// </summary>
|
||||
/// <returns>The list of fonts.</returns>
|
||||
public static List<IFontFamilyId> ListDalamudFonts() =>
|
||||
new()
|
||||
{
|
||||
new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansJpMedium),
|
||||
new DalamudAssetFontAndFamilyId(DalamudAsset.InconsolataRegular),
|
||||
new DalamudAssetFontAndFamilyId(DalamudAsset.FontAwesomeFreeSolid),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of Game-provided fonts.
|
||||
/// </summary>
|
||||
/// <returns>The list of fonts.</returns>
|
||||
public static List<IFontFamilyId> ListGameFonts() => new()
|
||||
{
|
||||
new GameFontAndFamilyId(GameFontFamily.Axis),
|
||||
new GameFontAndFamilyId(GameFontFamily.Jupiter),
|
||||
new GameFontAndFamilyId(GameFontFamily.JupiterNumeric),
|
||||
new GameFontAndFamilyId(GameFontFamily.Meidinger),
|
||||
new GameFontAndFamilyId(GameFontFamily.MiedingerMid),
|
||||
new GameFontAndFamilyId(GameFontFamily.TrumpGothic),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of System-provided fonts.
|
||||
/// </summary>
|
||||
/// <param name="refresh">If <c>true</c>, try to refresh the list.</param>
|
||||
/// <returns>The list of fonts.</returns>
|
||||
public static unsafe List<IFontFamilyId> ListSystemFonts(bool refresh)
|
||||
{
|
||||
using var dwf = default(ComPtr<IDWriteFactory>);
|
||||
fixed (Guid* piid = &IID.IID_IDWriteFactory)
|
||||
{
|
||||
DirectX.DWriteCreateFactory(
|
||||
DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_SHARED,
|
||||
piid,
|
||||
(IUnknown**)dwf.GetAddressOf()).ThrowOnError();
|
||||
}
|
||||
|
||||
using var sfc = default(ComPtr<IDWriteFontCollection>);
|
||||
dwf.Get()->GetSystemFontCollection(sfc.GetAddressOf(), refresh).ThrowOnError();
|
||||
|
||||
var count = (int)sfc.Get()->GetFontFamilyCount();
|
||||
var result = new List<IFontFamilyId>(count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
using var ff = default(ComPtr<IDWriteFontFamily>);
|
||||
if (sfc.Get()->GetFontFamily((uint)i, ff.GetAddressOf()).FAILED)
|
||||
{
|
||||
// Ignore errors, if any
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
result.Add(SystemFontFamilyId.FromDWriteFamily(ff));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
40
Dalamud/Interface/FontIdentifier/IFontId.cs
Normal file
40
Dalamud/Interface/FontIdentifier/IFontId.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.FontIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a font identifier.<br />
|
||||
/// Not intended for plugins to implement.
|
||||
/// </summary>
|
||||
public interface IFontId : IObjectWithLocalizableName
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the associated font family.
|
||||
/// </summary>
|
||||
IFontFamilyId Family { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the font weight, ranging from 1 to 999.
|
||||
/// </summary>
|
||||
int Weight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the font stretch, ranging from 1 to 9.
|
||||
/// </summary>
|
||||
int Stretch { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the font style. Treat as an opaque value.
|
||||
/// </summary>
|
||||
int Style { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds this font to the given font build toolkit.
|
||||
/// </summary>
|
||||
/// <param name="tk">The font build toolkit.</param>
|
||||
/// <param name="config">The font configuration. Some parameters may be ignored.</param>
|
||||
/// <returns>The added font.</returns>
|
||||
ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config);
|
||||
}
|
||||
52
Dalamud/Interface/FontIdentifier/IFontSpec.cs
Normal file
52
Dalamud/Interface/FontIdentifier/IFontSpec.cs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.FontIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a user's choice of font(s).<br />
|
||||
/// Not intended for plugins to implement.
|
||||
/// </summary>
|
||||
public interface IFontSpec
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the font size in pixels.
|
||||
/// </summary>
|
||||
float SizePx { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the font size in points.
|
||||
/// </summary>
|
||||
float SizePt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the line height in pixels.
|
||||
/// </summary>
|
||||
float LineHeightPx { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a font handle corresponding to this font specification.
|
||||
/// </summary>
|
||||
/// <param name="atlas">The atlas to bind this font handle to.</param>
|
||||
/// <param name="callback">Optional callback to be called after creating the font handle.</param>
|
||||
/// <returns>The new font handle.</returns>
|
||||
/// <remarks><see cref="IFontAtlasBuildToolkit.Font"/> will be set when <paramref name="callback"/> is invoked.
|
||||
/// </remarks>
|
||||
IFontHandle CreateFontHandle(IFontAtlas atlas, FontAtlasBuildStepDelegate? callback = null);
|
||||
|
||||
/// <summary>
|
||||
/// Adds this font to the given font build toolkit.
|
||||
/// </summary>
|
||||
/// <param name="tk">The font build toolkit.</param>
|
||||
/// <param name="mergeFont">The font to merge to.</param>
|
||||
/// <returns>The added font.</returns>
|
||||
ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, ImFontPtr mergeFont = default);
|
||||
|
||||
/// <summary>
|
||||
/// Represents this font specification, preferrably in the requested locale.
|
||||
/// </summary>
|
||||
/// <param name="localeCode">The locale code. Must be in lowercase(invariant).</param>
|
||||
/// <returns>The value.</returns>
|
||||
string ToLocalizedString(string localeCode);
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Utility;
|
||||
|
||||
using TerraFX.Interop.DirectX;
|
||||
|
||||
namespace Dalamud.Interface.FontIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an object with localizable names.
|
||||
/// </summary>
|
||||
public interface IObjectWithLocalizableName
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name, preferrably in English.
|
||||
/// </summary>
|
||||
string EnglishName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the names per locales.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<string, string>? LocaleNames { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name in the requested locale if available; otherwise, <see cref="EnglishName"/>.
|
||||
/// </summary>
|
||||
/// <param name="localeCode">The locale code. Must be in lowercase(invariant).</param>
|
||||
/// <returns>The value.</returns>
|
||||
string GetLocalizedName(string localeCode)
|
||||
{
|
||||
if (this.LocaleNames is null)
|
||||
return this.EnglishName;
|
||||
if (this.LocaleNames.TryGetValue(localeCode, out var v))
|
||||
return v;
|
||||
foreach (var (a, b) in this.LocaleNames)
|
||||
{
|
||||
if (a.StartsWith(localeCode))
|
||||
return b;
|
||||
}
|
||||
|
||||
return this.EnglishName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves all names per locales.
|
||||
/// </summary>
|
||||
/// <param name="fn">The names.</param>
|
||||
/// <returns>A new dictionary mapping from locale code to localized names.</returns>
|
||||
internal static unsafe IReadOnlyDictionary<string, string> GetLocaleNames(IDWriteLocalizedStrings* fn)
|
||||
{
|
||||
var count = fn->GetCount();
|
||||
var maxStrLen = 0u;
|
||||
for (var i = 0u; i < count; i++)
|
||||
{
|
||||
var length = 0u;
|
||||
fn->GetStringLength(i, &length).ThrowOnError();
|
||||
maxStrLen = Math.Max(maxStrLen, length);
|
||||
fn->GetLocaleNameLength(i, &length).ThrowOnError();
|
||||
maxStrLen = Math.Max(maxStrLen, length);
|
||||
}
|
||||
|
||||
maxStrLen++;
|
||||
var buf = stackalloc char[(int)maxStrLen];
|
||||
var result = new Dictionary<string, string>((int)count);
|
||||
for (var i = 0u; i < count; i++)
|
||||
{
|
||||
fn->GetLocaleName(i, (ushort*)buf, maxStrLen).ThrowOnError();
|
||||
var key = new string(buf);
|
||||
fn->GetString(i, (ushort*)buf, maxStrLen).ThrowOnError();
|
||||
var value = new string(buf);
|
||||
result[key.ToLowerInvariant()] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
154
Dalamud/Interface/FontIdentifier/SingleFontSpec.cs
Normal file
154
Dalamud/Interface/FontIdentifier/SingleFontSpec.cs
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
using System.Collections;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Dalamud.Interface.FontIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a user's choice of a single font.
|
||||
/// </summary>
|
||||
[SuppressMessage(
|
||||
"StyleCop.CSharp.OrderingRules",
|
||||
"SA1206:Declaration keywords should follow order",
|
||||
Justification = "public required")]
|
||||
public record SingleFontSpec : IFontSpec
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the font id.
|
||||
/// </summary>
|
||||
[JsonProperty]
|
||||
public required IFontId FontId { get; init; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonProperty]
|
||||
public float SizePx { get; init; } = 16;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public float SizePt
|
||||
{
|
||||
get => (this.SizePx * 3) / 4;
|
||||
init => this.SizePx = (value * 4) / 3;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public float LineHeightPx => MathF.Round(this.SizePx * this.LineHeight);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the line height ratio to the font size.
|
||||
/// </summary>
|
||||
[JsonProperty]
|
||||
public float LineHeight { get; init; } = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the glyph offset in pixels.
|
||||
/// </summary>
|
||||
[JsonProperty]
|
||||
public Vector2 GlyphOffset { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the letter spacing in pixels.
|
||||
/// </summary>
|
||||
[JsonProperty]
|
||||
public float LetterSpacing { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the glyph ranges.
|
||||
/// </summary>
|
||||
[JsonProperty]
|
||||
public ushort[]? GlyphRanges { get; init; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string ToLocalizedString(string localeCode)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(this.FontId.Family.GetLocalizedName(localeCode));
|
||||
sb.Append($"({this.FontId.GetLocalizedName(localeCode)}, {this.SizePt}pt");
|
||||
if (Math.Abs(this.LineHeight - 1f) > 0.000001f)
|
||||
sb.Append($", LH={this.LineHeight:0.##}");
|
||||
if (this.GlyphOffset != default)
|
||||
sb.Append($", O={this.GlyphOffset.X:0.##},{this.GlyphOffset.Y:0.##}");
|
||||
if (this.LetterSpacing != 0f)
|
||||
sb.Append($", LS={this.LetterSpacing:0.##}");
|
||||
sb.Append(')');
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => this.ToLocalizedString("en");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IFontHandle CreateFontHandle(IFontAtlas atlas, FontAtlasBuildStepDelegate? callback = null) =>
|
||||
atlas.NewDelegateFontHandle(tk =>
|
||||
{
|
||||
tk.OnPreBuild(e => e.Font = this.AddToBuildToolkit(e));
|
||||
callback?.Invoke(tk);
|
||||
});
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, ImFontPtr mergeFont = default)
|
||||
{
|
||||
var font = this.FontId.AddToBuildToolkit(
|
||||
tk,
|
||||
new()
|
||||
{
|
||||
SizePx = this.SizePx,
|
||||
GlyphRanges = this.GlyphRanges,
|
||||
MergeFont = mergeFont,
|
||||
});
|
||||
|
||||
tk.RegisterPostBuild(
|
||||
() =>
|
||||
{
|
||||
// Multiplication by scale will be done with global scale, outside of this handling.
|
||||
var scale = tk.GetFontScaleMode(font) == FontScaleMode.UndoGlobalScale ? 1 / tk.Scale : 1;
|
||||
var roundUnit = tk.GetFontScaleMode(font) == FontScaleMode.SkipHandling ? 1 : 1 / tk.Scale;
|
||||
var newAscent = MathF.Round((font.Ascent * this.LineHeight) / roundUnit) * roundUnit;
|
||||
var newFontSize = MathF.Round((font.FontSize * this.LineHeight) / roundUnit) * roundUnit;
|
||||
var shiftDown = MathF.Round((newFontSize - font.FontSize) / 2f / roundUnit) * roundUnit;
|
||||
|
||||
font.Ascent = newAscent;
|
||||
font.FontSize = newFontSize;
|
||||
font.Descent = newFontSize - font.Ascent;
|
||||
|
||||
var lookup = new BitArray(ushort.MaxValue + 1, this.GlyphRanges is null);
|
||||
if (this.GlyphRanges is not null)
|
||||
{
|
||||
for (var i = 0; i < this.GlyphRanges.Length && this.GlyphRanges[i] != 0; i += 2)
|
||||
{
|
||||
var to = (int)this.GlyphRanges[i + 1];
|
||||
for (var j = this.GlyphRanges[i]; j <= to; j++)
|
||||
lookup[j] = true;
|
||||
}
|
||||
}
|
||||
|
||||
var dax = MathF.Round((this.LetterSpacing * scale) / roundUnit) * roundUnit;
|
||||
var dxy0 = this.GlyphOffset * scale;
|
||||
dxy0 /= roundUnit;
|
||||
dxy0 = new(MathF.Round(dxy0.X), MathF.Round(dxy0.Y));
|
||||
dxy0 *= roundUnit;
|
||||
|
||||
dxy0.Y += shiftDown;
|
||||
var dxy = new Vector4(dxy0, dxy0.X, dxy0.Y);
|
||||
foreach (ref var glyphReal in font.GlyphsWrapped().DataSpan)
|
||||
{
|
||||
if (!lookup[glyphReal.Codepoint])
|
||||
continue;
|
||||
|
||||
glyphReal.XY += dxy;
|
||||
glyphReal.AdvanceX += dax;
|
||||
}
|
||||
});
|
||||
|
||||
return font;
|
||||
}
|
||||
}
|
||||
181
Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs
Normal file
181
Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
using Dalamud.Utility;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using TerraFX.Interop.DirectX;
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
namespace Dalamud.Interface.FontIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a font from system.
|
||||
/// </summary>
|
||||
public sealed class SystemFontFamilyId : IFontFamilyId
|
||||
{
|
||||
[JsonIgnore]
|
||||
private IReadOnlyList<IFontId>? fontsLazy;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SystemFontFamilyId"/> class.
|
||||
/// </summary>
|
||||
/// <param name="englishName">The font name in English.</param>
|
||||
/// <param name="localeNames">The localized font name for display purposes.</param>
|
||||
[JsonConstructor]
|
||||
internal SystemFontFamilyId(string englishName, IReadOnlyDictionary<string, string> localeNames)
|
||||
{
|
||||
this.EnglishName = englishName;
|
||||
this.LocaleNames = localeNames;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SystemFontFamilyId"/> class.
|
||||
/// </summary>
|
||||
/// <param name="localeNames">The localized font name for display purposes.</param>
|
||||
internal SystemFontFamilyId(IReadOnlyDictionary<string, string> localeNames)
|
||||
{
|
||||
if (localeNames.TryGetValue("en-us", out var name))
|
||||
this.EnglishName = name;
|
||||
else if (localeNames.TryGetValue("en", out name))
|
||||
this.EnglishName = name;
|
||||
else
|
||||
this.EnglishName = localeNames.Values.First();
|
||||
this.LocaleNames = localeNames;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonProperty]
|
||||
public string EnglishName { get; init; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonProperty]
|
||||
public IReadOnlyDictionary<string, string>? LocaleNames { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public IReadOnlyList<IFontId> Fonts => this.fontsLazy ??= this.GetFonts();
|
||||
|
||||
public static bool operator ==(SystemFontFamilyId? left, SystemFontFamilyId? right) => Equals(left, right);
|
||||
|
||||
public static bool operator !=(SystemFontFamilyId? left, SystemFontFamilyId? right) => !Equals(left, right);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int FindBestMatch(int weight, int stretch, int style)
|
||||
{
|
||||
using var matchingFont = default(ComPtr<IDWriteFont>);
|
||||
|
||||
var candidates = this.Fonts.ToList();
|
||||
var minGap = int.MaxValue;
|
||||
foreach (var c in candidates)
|
||||
minGap = Math.Min(minGap, Math.Abs(c.Weight - weight));
|
||||
candidates.RemoveAll(c => Math.Abs(c.Weight - weight) != minGap);
|
||||
|
||||
minGap = int.MaxValue;
|
||||
foreach (var c in candidates)
|
||||
minGap = Math.Min(minGap, Math.Abs(c.Stretch - stretch));
|
||||
candidates.RemoveAll(c => Math.Abs(c.Stretch - stretch) != minGap);
|
||||
|
||||
if (candidates.Any(x => x.Style == style))
|
||||
candidates.RemoveAll(x => x.Style != style);
|
||||
else if (candidates.Any(x => x.Style == (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL))
|
||||
candidates.RemoveAll(x => x.Style != (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL);
|
||||
|
||||
if (!candidates.Any())
|
||||
return 0;
|
||||
|
||||
for (var i = 0; i < this.Fonts.Count; i++)
|
||||
{
|
||||
if (Equals(this.Fonts[i], candidates[0]))
|
||||
return i;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => $"{nameof(SystemFontFamilyId)}:{this.EnglishName}";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) =>
|
||||
ReferenceEquals(this, obj) || (obj is SystemFontFamilyId other && this.Equals(other));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => this.EnglishName.GetHashCode();
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of <see cref="SystemFontFamilyId"/> from an <see cref="IDWriteFontFamily"/>.
|
||||
/// </summary>
|
||||
/// <param name="family">The family.</param>
|
||||
/// <returns>The new instance.</returns>
|
||||
internal static unsafe SystemFontFamilyId FromDWriteFamily(ComPtr<IDWriteFontFamily> family)
|
||||
{
|
||||
using var fn = default(ComPtr<IDWriteLocalizedStrings>);
|
||||
family.Get()->GetFamilyNames(fn.GetAddressOf()).ThrowOnError();
|
||||
return new(IObjectWithLocalizableName.GetLocaleNames(fn));
|
||||
}
|
||||
|
||||
private unsafe IReadOnlyList<IFontId> GetFonts()
|
||||
{
|
||||
using var dwf = default(ComPtr<IDWriteFactory>);
|
||||
fixed (Guid* piid = &IID.IID_IDWriteFactory)
|
||||
{
|
||||
DirectX.DWriteCreateFactory(
|
||||
DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_SHARED,
|
||||
piid,
|
||||
(IUnknown**)dwf.GetAddressOf()).ThrowOnError();
|
||||
}
|
||||
|
||||
using var sfc = default(ComPtr<IDWriteFontCollection>);
|
||||
dwf.Get()->GetSystemFontCollection(sfc.GetAddressOf(), false).ThrowOnError();
|
||||
|
||||
var familyIndex = 0u;
|
||||
BOOL exists = false;
|
||||
fixed (void* pName = this.EnglishName)
|
||||
sfc.Get()->FindFamilyName((ushort*)pName, &familyIndex, &exists).ThrowOnError();
|
||||
if (!exists)
|
||||
throw new FileNotFoundException($"Font \"{this.EnglishName}\" not found.");
|
||||
|
||||
using var family = default(ComPtr<IDWriteFontFamily>);
|
||||
sfc.Get()->GetFontFamily(familyIndex, family.GetAddressOf()).ThrowOnError();
|
||||
|
||||
var fontCount = (int)family.Get()->GetFontCount();
|
||||
var fonts = new List<IFontId>(fontCount);
|
||||
for (var i = 0; i < fontCount; i++)
|
||||
{
|
||||
using var font = default(ComPtr<IDWriteFont>);
|
||||
if (family.Get()->GetFont((uint)i, font.GetAddressOf()).FAILED)
|
||||
{
|
||||
// Ignore errors, if any
|
||||
continue;
|
||||
}
|
||||
|
||||
if (font.Get()->GetSimulations() != DWRITE_FONT_SIMULATIONS.DWRITE_FONT_SIMULATIONS_NONE)
|
||||
{
|
||||
// No simulation support
|
||||
continue;
|
||||
}
|
||||
|
||||
fonts.Add(new SystemFontId(this, font));
|
||||
}
|
||||
|
||||
fonts.Sort(
|
||||
(a, b) =>
|
||||
{
|
||||
var comp = a.Weight.CompareTo(b.Weight);
|
||||
if (comp != 0)
|
||||
return comp;
|
||||
|
||||
comp = a.Stretch.CompareTo(b.Stretch);
|
||||
if (comp != 0)
|
||||
return comp;
|
||||
|
||||
return a.Style.CompareTo(b.Style);
|
||||
});
|
||||
return fonts;
|
||||
}
|
||||
|
||||
private bool Equals(SystemFontFamilyId other) => this.EnglishName == other.EnglishName;
|
||||
}
|
||||
163
Dalamud/Interface/FontIdentifier/SystemFontId.cs
Normal file
163
Dalamud/Interface/FontIdentifier/SystemFontId.cs
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using TerraFX.Interop.DirectX;
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
namespace Dalamud.Interface.FontIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a font installed in the system.
|
||||
/// </summary>
|
||||
public sealed class SystemFontId : IFontId
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SystemFontId"/> class.
|
||||
/// </summary>
|
||||
/// <param name="family">The parent font family.</param>
|
||||
/// <param name="font">The font.</param>
|
||||
internal unsafe SystemFontId(SystemFontFamilyId family, ComPtr<IDWriteFont> font)
|
||||
{
|
||||
this.Family = family;
|
||||
this.Weight = (int)font.Get()->GetWeight();
|
||||
this.Stretch = (int)font.Get()->GetStretch();
|
||||
this.Style = (int)font.Get()->GetStyle();
|
||||
|
||||
using var fn = default(ComPtr<IDWriteLocalizedStrings>);
|
||||
font.Get()->GetFaceNames(fn.GetAddressOf()).ThrowOnError();
|
||||
this.LocaleNames = IObjectWithLocalizableName.GetLocaleNames(fn);
|
||||
if (this.LocaleNames.TryGetValue("en-us", out var name))
|
||||
this.EnglishName = name;
|
||||
else if (this.LocaleNames.TryGetValue("en", out name))
|
||||
this.EnglishName = name;
|
||||
else
|
||||
this.EnglishName = this.LocaleNames.Values.First();
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
private SystemFontId(string englishName, IReadOnlyDictionary<string, string> localeNames, IFontFamilyId family)
|
||||
{
|
||||
this.EnglishName = englishName;
|
||||
this.LocaleNames = localeNames;
|
||||
this.Family = family;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonProperty]
|
||||
public string EnglishName { get; init; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonProperty]
|
||||
public IReadOnlyDictionary<string, string>? LocaleNames { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonProperty]
|
||||
public IFontFamilyId Family { get; init; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonProperty]
|
||||
public int Weight { get; init; } = (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonProperty]
|
||||
public int Stretch { get; init; } = (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonProperty]
|
||||
public int Style { get; init; } = (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL;
|
||||
|
||||
public static bool operator ==(SystemFontId? left, SystemFontId? right) => Equals(left, right);
|
||||
|
||||
public static bool operator !=(SystemFontId? left, SystemFontId? right) => !Equals(left, right);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) =>
|
||||
ReferenceEquals(this, obj) || (obj is SystemFontId other && this.Equals(other));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => HashCode.Combine(this.Family, this.Weight, this.Stretch, this.Style);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() =>
|
||||
$"{nameof(SystemFontId)}:{this.Weight}:{this.Stretch}:{this.Style}:{this.Family}";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config)
|
||||
{
|
||||
var (path, index) = this.GetFileAndIndex();
|
||||
return tk.AddFontFromFile(path, config with { FontNo = index });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file containing this font, and the font index within.
|
||||
/// </summary>
|
||||
/// <returns>The path and index.</returns>
|
||||
public unsafe (string Path, int Index) GetFileAndIndex()
|
||||
{
|
||||
using var dwf = default(ComPtr<IDWriteFactory>);
|
||||
fixed (Guid* piid = &IID.IID_IDWriteFactory)
|
||||
{
|
||||
DirectX.DWriteCreateFactory(
|
||||
DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_SHARED,
|
||||
piid,
|
||||
(IUnknown**)dwf.GetAddressOf()).ThrowOnError();
|
||||
}
|
||||
|
||||
using var sfc = default(ComPtr<IDWriteFontCollection>);
|
||||
dwf.Get()->GetSystemFontCollection(sfc.GetAddressOf(), false).ThrowOnError();
|
||||
|
||||
var familyIndex = 0u;
|
||||
BOOL exists = false;
|
||||
fixed (void* name = this.Family.EnglishName)
|
||||
sfc.Get()->FindFamilyName((ushort*)name, &familyIndex, &exists).ThrowOnError();
|
||||
if (!exists)
|
||||
throw new FileNotFoundException($"Font \"{this.Family.EnglishName}\" not found.");
|
||||
|
||||
using var family = default(ComPtr<IDWriteFontFamily>);
|
||||
sfc.Get()->GetFontFamily(familyIndex, family.GetAddressOf()).ThrowOnError();
|
||||
|
||||
using var font = default(ComPtr<IDWriteFont>);
|
||||
family.Get()->GetFirstMatchingFont(
|
||||
(DWRITE_FONT_WEIGHT)this.Weight,
|
||||
(DWRITE_FONT_STRETCH)this.Stretch,
|
||||
(DWRITE_FONT_STYLE)this.Style,
|
||||
font.GetAddressOf()).ThrowOnError();
|
||||
|
||||
using var fface = default(ComPtr<IDWriteFontFace>);
|
||||
font.Get()->CreateFontFace(fface.GetAddressOf()).ThrowOnError();
|
||||
var fileCount = 0;
|
||||
fface.Get()->GetFiles((uint*)&fileCount, null).ThrowOnError();
|
||||
if (fileCount != 1)
|
||||
throw new NotSupportedException();
|
||||
|
||||
using var ffile = default(ComPtr<IDWriteFontFile>);
|
||||
fface.Get()->GetFiles((uint*)&fileCount, ffile.GetAddressOf()).ThrowOnError();
|
||||
void* refKey;
|
||||
var refKeySize = 0u;
|
||||
ffile.Get()->GetReferenceKey(&refKey, &refKeySize).ThrowOnError();
|
||||
|
||||
using var floader = default(ComPtr<IDWriteFontFileLoader>);
|
||||
ffile.Get()->GetLoader(floader.GetAddressOf()).ThrowOnError();
|
||||
|
||||
using var flocal = default(ComPtr<IDWriteLocalFontFileLoader>);
|
||||
floader.As(&flocal).ThrowOnError();
|
||||
|
||||
var pathSize = 0u;
|
||||
flocal.Get()->GetFilePathLengthFromKey(refKey, refKeySize, &pathSize).ThrowOnError();
|
||||
|
||||
var path = stackalloc char[(int)pathSize + 1];
|
||||
flocal.Get()->GetFilePathFromKey(refKey, refKeySize, (ushort*)path, pathSize + 1).ThrowOnError();
|
||||
return (new(path, 0, (int)pathSize), (int)fface.Get()->GetIndex());
|
||||
}
|
||||
|
||||
private bool Equals(SystemFontId other) => this.Family.Equals(other.Family) && this.Weight == other.Weight &&
|
||||
this.Stretch == other.Stretch && this.Style == other.Style;
|
||||
}
|
||||
1115
Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs
Normal file
1115
Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -141,6 +141,13 @@ internal class DalamudCommands : IServiceType
|
|||
"Toggle Dalamud UI display modes. Native UI modifications may also be affected by this, but that depends on the plugin."),
|
||||
});
|
||||
|
||||
commandManager.AddHandler("/xlprofiler", new CommandInfo(this.OnOpenProfilerCommand)
|
||||
{
|
||||
HelpMessage = Loc.Localize(
|
||||
"DalamudProfilerHelp",
|
||||
"Open Dalamud's startup timing profiler."),
|
||||
});
|
||||
|
||||
commandManager.AddHandler("/imdebug", new CommandInfo(this.OnDebugImInfoCommand)
|
||||
{
|
||||
HelpMessage = "ImGui DEBUG",
|
||||
|
|
@ -409,4 +416,9 @@ internal class DalamudCommands : IServiceType
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOpenProfilerCommand(string command, string arguments)
|
||||
{
|
||||
Service<DalamudInterface>.Get().ToggleProfilerWindow();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ using System.Text.Unicode;
|
|||
using Dalamud.Game.Text;
|
||||
using Dalamud.Hooking.WndProcHook;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.Internal.ManagedAsserts;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Interface.Utility;
|
||||
|
||||
|
|
@ -28,7 +29,6 @@ namespace Dalamud.Interface.Internal;
|
|||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal sealed unsafe class DalamudIme : IDisposable, IServiceType
|
||||
{
|
||||
private const int ImGuiContextTextStateOffset = 0x4588;
|
||||
private const int CImGuiStbTextCreateUndoOffset = 0xB57A0;
|
||||
private const int CImGuiStbTextUndoOffset = 0xB59C0;
|
||||
|
||||
|
|
@ -178,7 +178,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
|
|||
internal char InputModeIcon { get; private set; }
|
||||
|
||||
private static ImGuiInputTextState* TextState =>
|
||||
(ImGuiInputTextState*)(ImGui.GetCurrentContext() + ImGuiContextTextStateOffset);
|
||||
(ImGuiInputTextState*)(ImGui.GetCurrentContext() + ImGuiContextOffsets.TextStateOffset);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ using Dalamud.Game.ClientState.Conditions;
|
|||
using Dalamud.Game.ClientState.Keys;
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Game.Internal;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Interface.Animation.EasingFunctions;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Internal.ManagedAsserts;
|
||||
|
|
@ -89,7 +90,7 @@ internal class DalamudInterface : IDisposable, IServiceType
|
|||
private bool isImPlotDrawDemoWindow = false;
|
||||
private bool isImGuiTestWindowsInMonospace = false;
|
||||
private bool isImGuiDrawMetricsWindow = false;
|
||||
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private DalamudInterface(
|
||||
Dalamud dalamud,
|
||||
|
|
@ -188,7 +189,9 @@ internal class DalamudInterface : IDisposable, IServiceType
|
|||
this.creditsDarkeningAnimation.Point1 = Vector2.Zero;
|
||||
this.creditsDarkeningAnimation.Point2 = new Vector2(CreditsDarkeningMaxAlpha);
|
||||
}
|
||||
|
||||
|
||||
private delegate nint CrashDebugDelegate(nint self);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of frames since Dalamud has loaded.
|
||||
/// </summary>
|
||||
|
|
@ -744,28 +747,48 @@ internal class DalamudInterface : IDisposable, IServiceType
|
|||
}
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
if (ImGui.MenuItem("Access Violation"))
|
||||
|
||||
if (ImGui.BeginMenu("Crash game"))
|
||||
{
|
||||
Marshal.ReadByte(IntPtr.Zero);
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Crash game (nullptr)"))
|
||||
{
|
||||
unsafe
|
||||
if (ImGui.MenuItem("Access Violation"))
|
||||
{
|
||||
var framework = Framework.Instance();
|
||||
framework->UIModule = (UIModule*)0;
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Crash game (non-nullptr)"))
|
||||
{
|
||||
unsafe
|
||||
Marshal.ReadByte(IntPtr.Zero);
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Set UiModule to NULL"))
|
||||
{
|
||||
var framework = Framework.Instance();
|
||||
framework->UIModule = (UIModule*)0x12345678;
|
||||
unsafe
|
||||
{
|
||||
var framework = Framework.Instance();
|
||||
framework->UIModule = (UIModule*)0;
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Set UiModule to invalid ptr"))
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var framework = Framework.Instance();
|
||||
framework->UIModule = (UIModule*)0x12345678;
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Deref nullptr in Hook"))
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var hook = Hook<CrashDebugDelegate>.FromAddress(
|
||||
(nint)UIModule.StaticVTable.GetUIInputData,
|
||||
self =>
|
||||
{
|
||||
_ = *(byte*)0;
|
||||
return (nint)UIModule.Instance()->GetUIInputData();
|
||||
});
|
||||
hook.Enable();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndMenu();
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Report crashes at shutdown", null, this.configuration.ReportShutdownCrashes))
|
||||
|
|
|
|||
|
|
@ -1,41 +1,14 @@
|
|||
using System.Numerics;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiScene;
|
||||
|
||||
namespace Dalamud.Interface.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Base TextureWrap interface for all Dalamud-owned texture wraps.
|
||||
/// Used to avoid referencing ImGuiScene.
|
||||
/// </summary>
|
||||
public interface IDalamudTextureWrap : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a texture handle suitable for direct use with ImGui functions.
|
||||
/// </summary>
|
||||
IntPtr ImGuiHandle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the width of the texture.
|
||||
/// </summary>
|
||||
int Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the height of the texture.
|
||||
/// </summary>
|
||||
int Height { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size vector of the texture using Width, Height.
|
||||
/// </summary>
|
||||
Vector2 Size => new(this.Width, this.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safety harness for ImGuiScene textures that will defer destruction until
|
||||
/// the end of the frame.
|
||||
/// </summary>
|
||||
public class DalamudTextureWrap : IDalamudTextureWrap
|
||||
public class DalamudTextureWrap : IDalamudTextureWrap, IDeferredDisposable
|
||||
{
|
||||
private readonly TextureWrap wrappedWrap;
|
||||
|
||||
|
|
@ -83,7 +56,7 @@ public class DalamudTextureWrap : IDalamudTextureWrap
|
|||
/// <summary>
|
||||
/// Actually dispose the wrapped texture.
|
||||
/// </summary>
|
||||
internal void RealDispose()
|
||||
void IDeferredDisposable.RealDispose()
|
||||
{
|
||||
this.wrappedWrap.Dispose();
|
||||
}
|
||||
|
|
|
|||
55
Dalamud/Interface/Internal/IDalamudTextureWrap.cs
Normal file
55
Dalamud/Interface/Internal/IDalamudTextureWrap.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
using System.Numerics;
|
||||
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
namespace Dalamud.Interface.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Base TextureWrap interface for all Dalamud-owned texture wraps.
|
||||
/// Used to avoid referencing ImGuiScene.
|
||||
/// </summary>
|
||||
public interface IDalamudTextureWrap : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a texture handle suitable for direct use with ImGui functions.
|
||||
/// </summary>
|
||||
IntPtr ImGuiHandle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the width of the texture.
|
||||
/// </summary>
|
||||
int Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the height of the texture.
|
||||
/// </summary>
|
||||
int Height { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size vector of the texture using Width, Height.
|
||||
/// </summary>
|
||||
Vector2 Size => new(this.Width, this.Height);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new reference to the resource being pointed by this instance of <see cref="IDalamudTextureWrap"/>.
|
||||
/// </summary>
|
||||
/// <returns>The new reference to this texture wrap.</returns>
|
||||
/// <remarks>
|
||||
/// On calling this function, a new instance of <see cref="IDalamudTextureWrap"/> will be returned, but with
|
||||
/// the same <see cref="ImGuiHandle"/>. The new instance must be <see cref="IDisposable.Dispose"/>d, as the backing
|
||||
/// resource will stay alive until all the references are released. The old instance may be disposed as needed,
|
||||
/// once this function returns; the new instance will stay alive regardless of whether the old instance has been
|
||||
/// disposed.<br />
|
||||
/// Primary purpose of this function is to share textures across plugin boundaries. When texture wraps get passed
|
||||
/// across plugin boundaries for use for an indeterminate duration, the receiver should call this function to
|
||||
/// obtain a new reference to the texture received, so that it gets its own "copy" of the texture and the caller
|
||||
/// may dispose the texture anytime without any care for the receiver.<br />
|
||||
/// The default implementation will treat <see cref="ImGuiHandle"/> as an <see cref="IUnknown"/>.
|
||||
/// </remarks>
|
||||
unsafe IDalamudTextureWrap CreateWrapSharingLowLevelResource()
|
||||
{
|
||||
// Dalamud specific: IDalamudTextureWrap always points to an ID3D11ShaderResourceView.
|
||||
var handle = (IUnknown*)this.ImGuiHandle;
|
||||
return new UnknownTextureWrap(handle, this.Width, this.Height, true);
|
||||
}
|
||||
}
|
||||
|
|
@ -52,7 +52,6 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis
|
|||
private ImGuiClipboardFunctionProvider(InterfaceManager.InterfaceManagerWithScene imws)
|
||||
{
|
||||
// Effectively waiting for ImGui to become available.
|
||||
_ = imws;
|
||||
Debug.Assert(ImGuiHelpers.IsImGuiInitialized, "IMWS initialized but IsImGuiInitialized is false?");
|
||||
|
||||
var io = ImGui.GetIO();
|
||||
|
|
|
|||
133
Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs
Normal file
133
Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
using Dalamud.Hooking;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Fixes ImDrawList not correctly dealing with the current texture for that draw list not in tune with the global
|
||||
/// state. Currently, ImDrawList::AddPolyLine and ImDrawList::AddRectFilled are affected.
|
||||
///
|
||||
/// * The implementation for AddRectFilled is entirely replaced with the hook below.
|
||||
/// * The implementation for AddPolyLine is wrapped with Push/PopTextureID.
|
||||
///
|
||||
/// TODO:
|
||||
/// * imgui_draw.cpp:1433 ImDrawList::AddRectFilled
|
||||
/// The if block needs a PushTextureID(_Data->TexIdCommon)/PopTextureID() block,
|
||||
/// if _Data->TexIdCommon != _CmdHeader.TextureId.
|
||||
/// * imgui_draw.cpp:729 ImDrawList::AddPolyLine
|
||||
/// The if block always needs to call PushTextureID if the abovementioned condition is not met.
|
||||
/// Change push_texture_id to only have one condition.
|
||||
/// </summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed unsafe class ImGuiDrawListFixProvider : IServiceType, IDisposable
|
||||
{
|
||||
private const int CImGuiImDrawListAddPolyLineOffset = 0x589B0;
|
||||
private const int CImGuiImDrawListAddRectFilled = 0x59FD0;
|
||||
private const int CImGuiImDrawListSharedDataTexIdCommonOffset = 0;
|
||||
|
||||
private readonly Hook<ImDrawListAddPolyLine> hookImDrawListAddPolyline;
|
||||
private readonly Hook<ImDrawListAddRectFilled> hookImDrawListAddRectFilled;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private ImGuiDrawListFixProvider(InterfaceManager.InterfaceManagerWithScene imws)
|
||||
{
|
||||
// Force cimgui.dll to be loaded.
|
||||
_ = ImGui.GetCurrentContext();
|
||||
var cimgui = Process.GetCurrentProcess().Modules.Cast<ProcessModule>()
|
||||
.First(x => x.ModuleName == "cimgui.dll")
|
||||
.BaseAddress;
|
||||
|
||||
this.hookImDrawListAddPolyline = Hook<ImDrawListAddPolyLine>.FromAddress(
|
||||
cimgui + CImGuiImDrawListAddPolyLineOffset,
|
||||
this.ImDrawListAddPolylineDetour);
|
||||
this.hookImDrawListAddRectFilled = Hook<ImDrawListAddRectFilled>.FromAddress(
|
||||
cimgui + CImGuiImDrawListAddRectFilled,
|
||||
this.ImDrawListAddRectFilledDetour);
|
||||
this.hookImDrawListAddPolyline.Enable();
|
||||
this.hookImDrawListAddRectFilled.Enable();
|
||||
}
|
||||
|
||||
private delegate void ImDrawListAddPolyLine(
|
||||
ImDrawListPtr drawListPtr,
|
||||
ref Vector2 points,
|
||||
int pointsCount,
|
||||
uint color,
|
||||
ImDrawFlags flags,
|
||||
float thickness);
|
||||
|
||||
private delegate void ImDrawListAddRectFilled(
|
||||
ImDrawListPtr drawListPtr,
|
||||
ref Vector2 min,
|
||||
ref Vector2 max,
|
||||
uint col,
|
||||
float rounding,
|
||||
ImDrawFlags flags);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.hookImDrawListAddPolyline.Dispose();
|
||||
this.hookImDrawListAddRectFilled.Dispose();
|
||||
}
|
||||
|
||||
private void ImDrawListAddRectFilledDetour(
|
||||
ImDrawListPtr drawListPtr,
|
||||
ref Vector2 min,
|
||||
ref Vector2 max,
|
||||
uint col,
|
||||
float rounding,
|
||||
ImDrawFlags flags)
|
||||
{
|
||||
// Skip drawing if we're drawing something with alpha value of 0.
|
||||
if ((col & 0xFF000000) == 0)
|
||||
return;
|
||||
|
||||
if (rounding < 0.5f || (flags & ImDrawFlags.RoundCornersMask) == ImDrawFlags.RoundCornersMask)
|
||||
{
|
||||
// Take the fast path of drawing two triangles if no rounded corners are required.
|
||||
|
||||
var texIdCommon = *(nint*)(drawListPtr._Data + CImGuiImDrawListSharedDataTexIdCommonOffset);
|
||||
var pushTextureId = texIdCommon != drawListPtr._CmdHeader.TextureId;
|
||||
if (pushTextureId)
|
||||
drawListPtr.PushTextureID(texIdCommon);
|
||||
|
||||
drawListPtr.PrimReserve(6, 4);
|
||||
drawListPtr.PrimRect(min, max, col);
|
||||
|
||||
if (pushTextureId)
|
||||
drawListPtr.PopTextureID();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Defer drawing rectangle with rounded corners to path drawing operations.
|
||||
// Note that this may have a slightly different extent behaviors from the above if case.
|
||||
// This is how it is in imgui_draw.cpp.
|
||||
drawListPtr.PathRect(min, max, rounding, flags);
|
||||
drawListPtr.PathFillConvex(col);
|
||||
}
|
||||
}
|
||||
|
||||
private void ImDrawListAddPolylineDetour(
|
||||
ImDrawListPtr drawListPtr,
|
||||
ref Vector2 points,
|
||||
int pointsCount,
|
||||
uint color,
|
||||
ImDrawFlags flags,
|
||||
float thickness)
|
||||
{
|
||||
var texIdCommon = *(nint*)(drawListPtr._Data + CImGuiImDrawListSharedDataTexIdCommonOffset);
|
||||
var pushTextureId = texIdCommon != drawListPtr._CmdHeader.TextureId;
|
||||
if (pushTextureId)
|
||||
drawListPtr.PushTextureID(texIdCommon);
|
||||
|
||||
this.hookImDrawListAddPolyline.Original(drawListPtr, ref points, pointsCount, color, flags, thickness);
|
||||
|
||||
if (pushTextureId)
|
||||
drawListPtr.PopTextureID();
|
||||
}
|
||||
}
|
||||
|
|
@ -62,7 +62,7 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
/// </summary>
|
||||
public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f;
|
||||
|
||||
private readonly ConcurrentBag<DalamudTextureWrap> deferredDisposeTextures = new();
|
||||
private readonly ConcurrentBag<IDeferredDisposable> deferredDisposeTextures = new();
|
||||
private readonly ConcurrentBag<ILockedImFont> deferredDisposeImFontLockeds = new();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
|
|
@ -402,7 +402,7 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
/// Enqueue a texture to be disposed at the end of the frame.
|
||||
/// </summary>
|
||||
/// <param name="wrap">The texture.</param>
|
||||
public void EnqueueDeferredDispose(DalamudTextureWrap wrap)
|
||||
public void EnqueueDeferredDispose(IDeferredDisposable wrap)
|
||||
{
|
||||
this.deferredDisposeTextures.Add(wrap);
|
||||
}
|
||||
|
|
@ -705,13 +705,13 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
using (this.dalamudAtlas.SuppressAutoRebuild())
|
||||
{
|
||||
this.DefaultFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
|
||||
e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx)));
|
||||
e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(-1)));
|
||||
this.IconFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
|
||||
e => e.OnPreBuild(
|
||||
tk => tk.AddFontAwesomeIconFont(
|
||||
new()
|
||||
{
|
||||
SizePx = DefaultFontSizePx,
|
||||
SizePx = Service<FontAtlasFactory>.Get().DefaultFontSpec.SizePx,
|
||||
GlyphMinAdvanceX = DefaultFontSizePx,
|
||||
GlyphMaxAdvanceX = DefaultFontSizePx,
|
||||
})));
|
||||
|
|
@ -719,7 +719,10 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
e => e.OnPreBuild(
|
||||
tk => tk.AddDalamudAssetFont(
|
||||
DalamudAsset.InconsolataRegular,
|
||||
new() { SizePx = DefaultFontSizePx })));
|
||||
new()
|
||||
{
|
||||
SizePx = Service<FontAtlasFactory>.Get().DefaultFontSpec.SizePx,
|
||||
})));
|
||||
this.dalamudAtlas.BuildStepChange += e => e.OnPostBuild(
|
||||
tk =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,4 +18,6 @@ internal static class ImGuiContextOffsets
|
|||
public const int FontStackOffset = 0x7A4;
|
||||
|
||||
public const int BeginPopupStackOffset = 0x7B8;
|
||||
|
||||
public const int TextStateOffset = 0x4588;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Runtime.InteropServices;
|
|||
using Dalamud.Game;
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Memory;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using ImGuiNET;
|
||||
|
|
@ -82,7 +83,7 @@ internal unsafe class UiDebug
|
|||
private void DrawUnitBase(AtkUnitBase* atkUnitBase)
|
||||
{
|
||||
var isVisible = (atkUnitBase->Flags & 0x20) == 0x20;
|
||||
var addonName = Marshal.PtrToStringAnsi(new IntPtr(atkUnitBase->Name));
|
||||
var addonName = MemoryHelper.ReadSeStringAsString(out _, new IntPtr(atkUnitBase->Name));
|
||||
var agent = Service<GameGui>.Get().FindAgentInterface(atkUnitBase);
|
||||
|
||||
ImGui.Text($"{addonName}");
|
||||
|
|
@ -204,7 +205,7 @@ internal unsafe class UiDebug
|
|||
{
|
||||
case NodeType.Text:
|
||||
var textNode = (AtkTextNode*)node;
|
||||
ImGui.Text($"text: {Marshal.PtrToStringAnsi(new IntPtr(textNode->NodeText.StringPtr))}");
|
||||
ImGui.Text($"text: {MemoryHelper.ReadSeStringAsString(out _, (nint)textNode->NodeText.StringPtr)}");
|
||||
|
||||
ImGui.InputText($"Replace Text##{(ulong)textNode:X}", new IntPtr(textNode->NodeText.StringPtr), (uint)textNode->NodeText.BufSize);
|
||||
|
||||
|
|
@ -231,7 +232,7 @@ internal unsafe class UiDebug
|
|||
break;
|
||||
case NodeType.Counter:
|
||||
var counterNode = (AtkCounterNode*)node;
|
||||
ImGui.Text($"text: {Marshal.PtrToStringAnsi(new IntPtr(counterNode->NodeText.StringPtr))}");
|
||||
ImGui.Text($"text: {MemoryHelper.ReadSeStringAsString(out _, (nint)counterNode->NodeText.StringPtr)}");
|
||||
break;
|
||||
case NodeType.Image:
|
||||
var imageNode = (AtkImageNode*)node;
|
||||
|
|
@ -250,8 +251,8 @@ internal unsafe class UiDebug
|
|||
{
|
||||
var texFileNameStdString = &textureInfo->AtkTexture.Resource->TexFileResourceHandle->ResourceHandle.FileName;
|
||||
var texString = texFileNameStdString->Length < 16
|
||||
? Marshal.PtrToStringAnsi((IntPtr)texFileNameStdString->Buffer)
|
||||
: Marshal.PtrToStringAnsi((IntPtr)texFileNameStdString->BufferPtr);
|
||||
? MemoryHelper.ReadSeStringAsString(out _, (nint)texFileNameStdString->Buffer)
|
||||
: MemoryHelper.ReadSeStringAsString(out _, (nint)texFileNameStdString->BufferPtr);
|
||||
|
||||
ImGui.Text($"texture path: {texString}");
|
||||
var kernelTexture = textureInfo->AtkTexture.Resource->KernelTextureObject;
|
||||
|
|
@ -352,13 +353,13 @@ internal unsafe class UiDebug
|
|||
{
|
||||
case ComponentType.TextInput:
|
||||
var textInputComponent = (AtkComponentTextInput*)compNode->Component;
|
||||
ImGui.Text($"InputBase Text1: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->AtkComponentInputBase.UnkText1.StringPtr))}");
|
||||
ImGui.Text($"InputBase Text2: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->AtkComponentInputBase.UnkText2.StringPtr))}");
|
||||
ImGui.Text($"Text1: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->UnkText1.StringPtr))}");
|
||||
ImGui.Text($"Text2: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->UnkText2.StringPtr))}");
|
||||
ImGui.Text($"Text3: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->UnkText3.StringPtr))}");
|
||||
ImGui.Text($"Text4: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->UnkText4.StringPtr))}");
|
||||
ImGui.Text($"Text5: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->UnkText5.StringPtr))}");
|
||||
ImGui.Text($"InputBase Text1: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->AtkComponentInputBase.UnkText1.StringPtr))}");
|
||||
ImGui.Text($"InputBase Text2: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->AtkComponentInputBase.UnkText2.StringPtr))}");
|
||||
ImGui.Text($"Text1: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText1.StringPtr))}");
|
||||
ImGui.Text($"Text2: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText2.StringPtr))}");
|
||||
ImGui.Text($"Text3: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText3.StringPtr))}");
|
||||
ImGui.Text($"Text4: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText4.StringPtr))}");
|
||||
ImGui.Text($"Text5: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText5.StringPtr))}");
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -474,7 +475,7 @@ internal unsafe class UiDebug
|
|||
foundSelected = true;
|
||||
}
|
||||
|
||||
var name = Marshal.PtrToStringAnsi(new IntPtr(unitBase->Name));
|
||||
var name = MemoryHelper.ReadSeStringAsString(out _, new IntPtr(unitBase->Name));
|
||||
if (searching)
|
||||
{
|
||||
if (name == null || !name.ToLower().Contains(searchStr.ToLower())) continue;
|
||||
|
|
|
|||
77
Dalamud/Interface/Internal/UnknownTextureWrap.cs
Normal file
77
Dalamud/Interface/Internal/UnknownTextureWrap.cs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
using System.Threading;
|
||||
|
||||
using Dalamud.Utility;
|
||||
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
namespace Dalamud.Interface.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// A texture wrap that is created by cloning the underlying <see cref="IDalamudTextureWrap.ImGuiHandle"/>.
|
||||
/// </summary>
|
||||
internal sealed unsafe class UnknownTextureWrap : IDalamudTextureWrap, IDeferredDisposable
|
||||
{
|
||||
private IntPtr imGuiHandle;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UnknownTextureWrap"/> class.
|
||||
/// </summary>
|
||||
/// <param name="unknown">The pointer to <see cref="IUnknown"/> that is suitable for use with
|
||||
/// <see cref="IDalamudTextureWrap.ImGuiHandle"/>.</param>
|
||||
/// <param name="width">The width of the texture.</param>
|
||||
/// <param name="height">The height of the texture.</param>
|
||||
/// <param name="callAddRef">If <c>true</c>, call <see cref="IUnknown.AddRef"/>.</param>
|
||||
public UnknownTextureWrap(IUnknown* unknown, int width, int height, bool callAddRef)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(unknown is null, typeof(IUnknown));
|
||||
this.imGuiHandle = (nint)unknown;
|
||||
this.Width = width;
|
||||
this.Height = height;
|
||||
if (callAddRef)
|
||||
unknown->AddRef();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="UnknownTextureWrap"/> class.
|
||||
/// </summary>
|
||||
~UnknownTextureWrap() => this.Dispose(false);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public nint ImGuiHandle =>
|
||||
this.imGuiHandle == nint.Zero
|
||||
? throw new ObjectDisposedException(nameof(UnknownTextureWrap))
|
||||
: this.imGuiHandle;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Width { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Height { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Queue the texture to be disposed once the frame ends.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
this.Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Actually dispose the wrapped texture.
|
||||
/// </summary>
|
||||
void IDeferredDisposable.RealDispose()
|
||||
{
|
||||
var handle = Interlocked.Exchange(ref this.imGuiHandle, nint.Zero);
|
||||
if (handle != nint.Zero)
|
||||
((IUnknown*)handle)->Release();
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
Service<InterfaceManager>.GetNullable()?.EnqueueDeferredDispose(this);
|
||||
else
|
||||
((IDeferredDisposable)this).RealDispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ using System.Numerics;
|
|||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Game.Command;
|
||||
|
|
@ -28,7 +29,11 @@ namespace Dalamud.Interface.Internal.Windows;
|
|||
/// </summary>
|
||||
internal class ConsoleWindow : Window, IDisposable
|
||||
{
|
||||
private readonly List<LogEntry> logText = new();
|
||||
private const int LogLinesMinimum = 100;
|
||||
private const int LogLinesMaximum = 1000000;
|
||||
|
||||
private readonly RollingList<LogEntry> logText;
|
||||
private volatile int newRolledLines;
|
||||
private readonly object renderLock = new();
|
||||
|
||||
private readonly List<string> history = new();
|
||||
|
|
@ -42,12 +47,14 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
private string pluginFilter = string.Empty;
|
||||
|
||||
private bool filterShowUncaughtExceptions;
|
||||
private bool settingsPopupWasOpen;
|
||||
private bool showFilterToolbar;
|
||||
private bool clearLog;
|
||||
private bool copyLog;
|
||||
private bool copyMode;
|
||||
private bool killGameArmed;
|
||||
private bool autoScroll;
|
||||
private int logLinesLimit;
|
||||
private bool autoOpen;
|
||||
private bool regexError;
|
||||
|
||||
|
|
@ -71,13 +78,20 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
this.SizeConstraints = new WindowSizeConstraints
|
||||
{
|
||||
MinimumSize = new Vector2(600.0f, 200.0f),
|
||||
MaximumSize = new Vector2(9999.0f, 9999.0f),
|
||||
};
|
||||
|
||||
this.RespectCloseHotkey = false;
|
||||
|
||||
this.logLinesLimit = configuration.LogLinesLimit;
|
||||
|
||||
var limit = Math.Max(LogLinesMinimum, this.logLinesLimit);
|
||||
this.logText = new(limit);
|
||||
this.FilteredLogEntries = new(limit);
|
||||
|
||||
configuration.DalamudConfigurationSaved += this.OnDalamudConfigurationSaved;
|
||||
}
|
||||
|
||||
private List<LogEntry> FilteredLogEntries { get; set; } = new();
|
||||
private RollingList<LogEntry> FilteredLogEntries { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnOpen()
|
||||
|
|
@ -92,6 +106,7 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
public void Dispose()
|
||||
{
|
||||
SerilogEventSink.Instance.LogLine -= this.OnLogLine;
|
||||
Service<DalamudConfiguration>.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -152,8 +167,11 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X / 2.0f - ImGui.CalcTextSize(regexErrorString).X / 2.0f);
|
||||
ImGui.TextColored(ImGuiColors.DalamudRed, regexErrorString);
|
||||
}
|
||||
|
||||
ImGui.BeginChild("scrolling", new Vector2(0, ImGui.GetFrameHeightWithSpacing() - 55 * ImGuiHelpers.GlobalScale), false, ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar);
|
||||
|
||||
var sendButtonSize = ImGui.CalcTextSize("Send") +
|
||||
((new Vector2(16, 0) + (ImGui.GetStyle().FramePadding * 2)) * ImGuiHelpers.GlobalScale);
|
||||
var scrollingHeight = ImGui.GetContentRegionAvail().Y - sendButtonSize.Y;
|
||||
ImGui.BeginChild("scrolling", new Vector2(0, scrollingHeight), false, ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar);
|
||||
|
||||
if (this.clearLog) this.Clear();
|
||||
|
||||
|
|
@ -173,9 +191,13 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
var childDrawList = ImGui.GetWindowDrawList();
|
||||
var childSize = ImGui.GetWindowSize();
|
||||
|
||||
var cursorDiv = ImGuiHelpers.GlobalScale * 93;
|
||||
var cursorLogLevel = ImGuiHelpers.GlobalScale * 100;
|
||||
var cursorLogLine = ImGuiHelpers.GlobalScale * 135;
|
||||
var cursorDiv = ImGui.CalcTextSize("00:00:00.000 ").X;
|
||||
var cursorLogLevel = ImGui.CalcTextSize("00:00:00.000 | ").X;
|
||||
var dividerOffset = ImGui.CalcTextSize("00:00:00.000 | AAA ").X + (ImGui.CalcTextSize(" ").X / 2);
|
||||
var cursorLogLine = ImGui.CalcTextSize("00:00:00.000 | AAA | ").X;
|
||||
|
||||
var lastLinePosY = 0.0f;
|
||||
var logLineHeight = 0.0f;
|
||||
|
||||
lock (this.renderLock)
|
||||
{
|
||||
|
|
@ -184,7 +206,8 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
{
|
||||
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
|
||||
{
|
||||
var line = this.FilteredLogEntries[i];
|
||||
var index = Math.Max(i - this.newRolledLines, 0); // Prevents flicker effect. Also workaround to avoid negative indexes.
|
||||
var line = this.FilteredLogEntries[index];
|
||||
|
||||
if (!line.IsMultiline && !this.copyLog)
|
||||
ImGui.Separator();
|
||||
|
|
@ -225,6 +248,10 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
|
||||
ImGui.SetCursorPosX(cursorLogLine);
|
||||
ImGui.TextUnformatted(line.Line);
|
||||
|
||||
var currentLinePosY = ImGui.GetCursorPosY();
|
||||
logLineHeight = currentLinePosY - lastLinePosY;
|
||||
lastLinePosY = currentLinePosY;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -236,14 +263,19 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
|
||||
ImGui.PopStyleVar();
|
||||
|
||||
var newRolledLinesCount = Interlocked.Exchange(ref this.newRolledLines, 0);
|
||||
if (!this.autoScroll || ImGui.GetScrollY() < ImGui.GetScrollMaxY())
|
||||
{
|
||||
ImGui.SetScrollY(ImGui.GetScrollY() - (logLineHeight * newRolledLinesCount));
|
||||
}
|
||||
|
||||
if (this.autoScroll && ImGui.GetScrollY() >= ImGui.GetScrollMaxY())
|
||||
{
|
||||
ImGui.SetScrollHereY(1.0f);
|
||||
}
|
||||
|
||||
// Draw dividing line
|
||||
var offset = ImGuiHelpers.GlobalScale * 127;
|
||||
childDrawList.AddLine(new Vector2(childPos.X + offset, childPos.Y), new Vector2(childPos.X + offset, childPos.Y + childSize.Y), 0x4FFFFFFF, 1.0f);
|
||||
childDrawList.AddLine(new Vector2(childPos.X + dividerOffset, childPos.Y), new Vector2(childPos.X + dividerOffset, childPos.Y + childSize.Y), 0x4FFFFFFF, 1.0f);
|
||||
|
||||
ImGui.EndChild();
|
||||
|
||||
|
|
@ -261,7 +293,7 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - (80.0f * ImGuiHelpers.GlobalScale) - (ImGui.GetStyle().ItemSpacing.X * ImGuiHelpers.GlobalScale));
|
||||
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - sendButtonSize.X - (ImGui.GetStyle().ItemSpacing.X * ImGuiHelpers.GlobalScale));
|
||||
|
||||
var getFocus = false;
|
||||
unsafe
|
||||
|
|
@ -280,7 +312,7 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
|
||||
if (hadColor) ImGui.PopStyleColor();
|
||||
|
||||
if (ImGui.Button("Send", ImGuiHelpers.ScaledVector2(80.0f, 23.0f)))
|
||||
if (ImGui.Button("Send", sendButtonSize))
|
||||
{
|
||||
this.ProcessCommand();
|
||||
}
|
||||
|
|
@ -361,21 +393,21 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
|
||||
ImGui.SameLine();
|
||||
|
||||
this.autoScroll = configuration.LogAutoScroll;
|
||||
if (this.DrawToggleButtonWithTooltip("auto_scroll", "Auto-scroll", FontAwesomeIcon.Sync, ref this.autoScroll))
|
||||
var settingsPopup = ImGui.BeginPopup("##console_settings");
|
||||
if (settingsPopup)
|
||||
{
|
||||
configuration.LogAutoScroll = !configuration.LogAutoScroll;
|
||||
configuration.QueueSave();
|
||||
this.DrawSettingsPopup(configuration);
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
else if (this.settingsPopupWasOpen)
|
||||
{
|
||||
// Prevent side effects in case Apply wasn't clicked
|
||||
this.logLinesLimit = configuration.LogLinesLimit;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
this.settingsPopupWasOpen = settingsPopup;
|
||||
|
||||
this.autoOpen = configuration.LogOpenAtStartup;
|
||||
if (this.DrawToggleButtonWithTooltip("auto_open", "Open at startup", FontAwesomeIcon.WindowRestore, ref this.autoOpen))
|
||||
{
|
||||
configuration.LogOpenAtStartup = !configuration.LogOpenAtStartup;
|
||||
configuration.QueueSave();
|
||||
}
|
||||
if (this.DrawToggleButtonWithTooltip("show_settings", "Show settings", FontAwesomeIcon.List, ref settingsPopup)) ImGui.OpenPopup("##console_settings");
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
|
|
@ -445,6 +477,33 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private void DrawSettingsPopup(DalamudConfiguration configuration)
|
||||
{
|
||||
if (ImGui.Checkbox("Open at startup", ref this.autoOpen))
|
||||
{
|
||||
configuration.LogOpenAtStartup = this.autoOpen;
|
||||
configuration.QueueSave();
|
||||
}
|
||||
|
||||
if (ImGui.Checkbox("Auto-scroll", ref this.autoScroll))
|
||||
{
|
||||
configuration.LogAutoScroll = this.autoScroll;
|
||||
configuration.QueueSave();
|
||||
}
|
||||
|
||||
ImGui.TextUnformatted("Logs buffer");
|
||||
ImGui.SliderInt("lines", ref this.logLinesLimit, LogLinesMinimum, LogLinesMaximum);
|
||||
if (ImGui.Button("Apply"))
|
||||
{
|
||||
this.logLinesLimit = Math.Max(LogLinesMinimum, this.logLinesLimit);
|
||||
|
||||
configuration.LogLinesLimit = this.logLinesLimit;
|
||||
configuration.QueueSave();
|
||||
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawFilterToolbar()
|
||||
{
|
||||
if (!this.showFilterToolbar) return;
|
||||
|
|
@ -684,8 +743,12 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
|
||||
this.logText.Add(entry);
|
||||
|
||||
var avoidScroll = this.FilteredLogEntries.Count == this.FilteredLogEntries.Size;
|
||||
if (this.IsFilterApplicable(entry))
|
||||
{
|
||||
this.FilteredLogEntries.Add(entry);
|
||||
if (avoidScroll) Interlocked.Increment(ref this.newRolledLines);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsFilterApplicable(LogEntry entry)
|
||||
|
|
@ -729,8 +792,6 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
return false;
|
||||
}
|
||||
|
||||
this.regexError = false;
|
||||
|
||||
// else we couldn't find a filter for this entry, if we have any filters, we need to block this entry.
|
||||
return !this.pluginFilters.Any();
|
||||
}
|
||||
|
|
@ -739,7 +800,8 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
{
|
||||
lock (this.renderLock)
|
||||
{
|
||||
this.FilteredLogEntries = this.logText.Where(this.IsFilterApplicable).ToList();
|
||||
this.regexError = false;
|
||||
this.FilteredLogEntries = new RollingList<LogEntry>(this.logText.Where(this.IsFilterApplicable), Math.Max(LogLinesMinimum, this.logLinesLimit));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -788,6 +850,14 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
return result;
|
||||
}
|
||||
|
||||
private void OnDalamudConfigurationSaved(DalamudConfiguration dalamudConfiguration)
|
||||
{
|
||||
this.logLinesLimit = dalamudConfiguration.LogLinesLimit;
|
||||
var limit = Math.Max(LogLinesMinimum, this.logLinesLimit);
|
||||
this.logText.Size = limit;
|
||||
this.FilteredLogEntries.Size = limit;
|
||||
}
|
||||
|
||||
private class LogEntry
|
||||
{
|
||||
public string Line { get; init; } = string.Empty;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ using System.Numerics;
|
|||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.ImGuiFontChooserDialog;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Interface.Utility;
|
||||
|
|
@ -22,10 +25,20 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
|||
/// </summary>
|
||||
internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
|
||||
{
|
||||
private static readonly string[] FontScaleModes =
|
||||
{
|
||||
nameof(FontScaleMode.Default),
|
||||
nameof(FontScaleMode.SkipHandling),
|
||||
nameof(FontScaleMode.UndoGlobalScale),
|
||||
};
|
||||
|
||||
private ImVectorWrapper<byte> testStringBuffer;
|
||||
private IFontAtlas? privateAtlas;
|
||||
private SingleFontSpec fontSpec = new() { FontId = DalamudDefaultFontAndFamilyId.Instance };
|
||||
private IFontHandle? fontDialogHandle;
|
||||
private IReadOnlyDictionary<GameFontFamily, (GameFontStyle Size, Lazy<IFontHandle> Handle)[]>? fontHandles;
|
||||
private bool useGlobalScale;
|
||||
private bool atlasScaleMode = true;
|
||||
private int fontScaleMode = (int)FontScaleMode.UndoGlobalScale;
|
||||
private bool useWordWrap;
|
||||
private bool useItalic;
|
||||
private bool useBold;
|
||||
|
|
@ -47,12 +60,14 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
|
|||
public unsafe void Draw()
|
||||
{
|
||||
ImGui.AlignTextToFramePadding();
|
||||
fixed (byte* labelPtr = "Global Scale"u8)
|
||||
if (ImGui.Combo("Global Scale per Font", ref this.fontScaleMode, FontScaleModes, FontScaleModes.Length))
|
||||
this.ClearAtlas();
|
||||
fixed (byte* labelPtr = "Global Scale for Atlas"u8)
|
||||
{
|
||||
var v = (byte)(this.useGlobalScale ? 1 : 0);
|
||||
var v = (byte)(this.atlasScaleMode ? 1 : 0);
|
||||
if (ImGuiNative.igCheckbox(labelPtr, &v) != 0)
|
||||
{
|
||||
this.useGlobalScale = v != 0;
|
||||
this.atlasScaleMode = v != 0;
|
||||
this.ClearAtlas();
|
||||
}
|
||||
}
|
||||
|
|
@ -111,36 +126,76 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
|
|||
if (ImGui.Button("Test Lock"))
|
||||
Task.Run(this.TestLock);
|
||||
|
||||
fixed (byte* labelPtr = "Test Input"u8)
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Choose Editor Font"))
|
||||
{
|
||||
if (ImGuiNative.igInputTextMultiline(
|
||||
labelPtr,
|
||||
this.testStringBuffer.Data,
|
||||
(uint)this.testStringBuffer.Capacity,
|
||||
new(ImGui.GetContentRegionAvail().X, 32 * ImGuiHelpers.GlobalScale),
|
||||
0,
|
||||
null,
|
||||
null) != 0)
|
||||
{
|
||||
var len = this.testStringBuffer.StorageSpan.IndexOf((byte)0);
|
||||
if (len + 4 >= this.testStringBuffer.Capacity)
|
||||
this.testStringBuffer.EnsureCapacityExponential(len + 4);
|
||||
if (len < this.testStringBuffer.Capacity)
|
||||
{
|
||||
this.testStringBuffer.LengthUnsafe = len;
|
||||
this.testStringBuffer.StorageSpan[len] = default;
|
||||
}
|
||||
var fcd = new SingleFontChooserDialog(
|
||||
Service<FontAtlasFactory>.Get().CreateFontAtlas(
|
||||
$"{nameof(GamePrebakedFontsTestWidget)}:EditorFont",
|
||||
FontAtlasAutoRebuildMode.Async));
|
||||
fcd.SelectedFont = this.fontSpec;
|
||||
fcd.IgnorePreviewGlobalScale = !this.atlasScaleMode;
|
||||
Service<InterfaceManager>.Get().Draw += fcd.Draw;
|
||||
fcd.ResultTask.ContinueWith(
|
||||
r => Service<Framework>.Get().RunOnFrameworkThread(
|
||||
() =>
|
||||
{
|
||||
Service<InterfaceManager>.Get().Draw -= fcd.Draw;
|
||||
fcd.Dispose();
|
||||
|
||||
if (this.useMinimumBuild)
|
||||
_ = this.privateAtlas?.BuildFontsAsync();
|
||||
}
|
||||
_ = r.Exception;
|
||||
if (!r.IsCompletedSuccessfully)
|
||||
return;
|
||||
|
||||
this.fontSpec = r.Result;
|
||||
Log.Information("Selected font: {font}", this.fontSpec);
|
||||
this.fontDialogHandle?.Dispose();
|
||||
this.fontDialogHandle = null;
|
||||
}));
|
||||
}
|
||||
|
||||
this.privateAtlas ??=
|
||||
Service<FontAtlasFactory>.Get().CreateFontAtlas(
|
||||
nameof(GamePrebakedFontsTestWidget),
|
||||
FontAtlasAutoRebuildMode.Async,
|
||||
this.useGlobalScale);
|
||||
this.atlasScaleMode);
|
||||
this.fontDialogHandle ??= this.fontSpec.CreateFontHandle(
|
||||
this.privateAtlas,
|
||||
e => e.OnPreBuild(tk => tk.SetFontScaleMode(tk.Font, (FontScaleMode)this.fontScaleMode)));
|
||||
|
||||
fixed (byte* labelPtr = "Test Input"u8)
|
||||
{
|
||||
if (!this.atlasScaleMode)
|
||||
ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale);
|
||||
using (this.fontDialogHandle.Push())
|
||||
{
|
||||
if (ImGuiNative.igInputTextMultiline(
|
||||
labelPtr,
|
||||
this.testStringBuffer.Data,
|
||||
(uint)this.testStringBuffer.Capacity,
|
||||
new(ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight() * 3),
|
||||
0,
|
||||
null,
|
||||
null) != 0)
|
||||
{
|
||||
var len = this.testStringBuffer.StorageSpan.IndexOf((byte)0);
|
||||
if (len + 4 >= this.testStringBuffer.Capacity)
|
||||
this.testStringBuffer.EnsureCapacityExponential(len + 4);
|
||||
if (len < this.testStringBuffer.Capacity)
|
||||
{
|
||||
this.testStringBuffer.LengthUnsafe = len;
|
||||
this.testStringBuffer.StorageSpan[len] = default;
|
||||
}
|
||||
|
||||
if (this.useMinimumBuild)
|
||||
_ = this.privateAtlas?.BuildFontsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.atlasScaleMode)
|
||||
ImGuiNative.igSetWindowFontScale(1);
|
||||
}
|
||||
|
||||
this.fontHandles ??=
|
||||
Enum.GetValues<GameFontFamilyAndSize>()
|
||||
.Where(x => x.GetAttribute<GameFontFamilyAndSizeAttribute>() is not null)
|
||||
|
|
@ -149,17 +204,29 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
|
|||
.ToImmutableDictionary(
|
||||
x => x.Key,
|
||||
x => x.Select(
|
||||
y => (y, new Lazy<IFontHandle>(
|
||||
() => this.useMinimumBuild
|
||||
? this.privateAtlas.NewDelegateFontHandle(
|
||||
e =>
|
||||
e.OnPreBuild(
|
||||
tk => tk.AddGameGlyphs(
|
||||
y,
|
||||
Encoding.UTF8.GetString(
|
||||
this.testStringBuffer.DataSpan).ToGlyphRange(),
|
||||
default)))
|
||||
: this.privateAtlas.NewGameFontHandle(y))))
|
||||
y =>
|
||||
{
|
||||
var range = Encoding.UTF8.GetString(this.testStringBuffer.DataSpan).ToGlyphRange();
|
||||
|
||||
Lazy<IFontHandle> l;
|
||||
if (this.useMinimumBuild
|
||||
|| (this.atlasScaleMode && this.fontScaleMode != (int)FontScaleMode.Default))
|
||||
{
|
||||
l = new(
|
||||
() => this.privateAtlas!.NewDelegateFontHandle(
|
||||
e =>
|
||||
e.OnPreBuild(
|
||||
tk => tk.SetFontScaleMode(
|
||||
tk.AddGameGlyphs(y, range, default),
|
||||
(FontScaleMode)this.fontScaleMode))));
|
||||
}
|
||||
else
|
||||
{
|
||||
l = new(() => this.privateAtlas!.NewGameFontHandle(y));
|
||||
}
|
||||
|
||||
return (y, l);
|
||||
})
|
||||
.ToArray());
|
||||
|
||||
var offsetX = ImGui.CalcTextSize("99.9pt").X + (ImGui.GetStyle().FramePadding.X * 2);
|
||||
|
|
@ -187,7 +254,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
|
|||
}
|
||||
else
|
||||
{
|
||||
if (!this.useGlobalScale)
|
||||
if (!this.atlasScaleMode)
|
||||
ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale);
|
||||
if (counter++ % 2 == 0)
|
||||
{
|
||||
|
|
@ -208,8 +275,8 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
|
|||
}
|
||||
finally
|
||||
{
|
||||
ImGuiNative.igPopTextWrapPos();
|
||||
ImGuiNative.igSetWindowFontScale(1);
|
||||
ImGuiNative.igPopTextWrapPos();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -227,6 +294,8 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
|
|||
this.fontHandles?.Values.SelectMany(x => x.Where(y => y.Handle.IsValueCreated).Select(y => y.Handle.Value))
|
||||
.AggregateToDisposable().Dispose();
|
||||
this.fontHandles = null;
|
||||
this.fontDialogHandle?.Dispose();
|
||||
this.fontDialogHandle = null;
|
||||
this.privateAtlas?.Dispose();
|
||||
this.privateAtlas = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,6 +119,10 @@ internal class TexWidget : IDataWindowWidget
|
|||
|
||||
if (ImGui.Button($"X##{i}"))
|
||||
toRemove = tex;
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button($"Clone##{i}"))
|
||||
this.addedTextures.Add(tex.CreateWrapSharingLowLevelResource());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -148,7 +148,6 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
this.SizeConstraints = new WindowSizeConstraints
|
||||
{
|
||||
MinimumSize = this.Size.Value,
|
||||
MaximumSize = new Vector2(5000, 5000),
|
||||
};
|
||||
|
||||
Service<PluginManager>.GetAsync().ContinueWith(pluginManagerTask =>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
/*using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.Gui.ContextMenu;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Utility;
|
||||
using ImGuiNET;
|
||||
using Lumina.Excel;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using Serilog;*/
|
||||
using Serilog;
|
||||
|
||||
namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps;
|
||||
|
||||
|
|
@ -13,31 +20,22 @@ namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps;
|
|||
/// </summary>
|
||||
internal class ContextMenuAgingStep : IAgingStep
|
||||
{
|
||||
/*
|
||||
private SubStep currentSubStep;
|
||||
|
||||
private uint clickedItemId;
|
||||
private bool clickedItemHq;
|
||||
private uint clickedItemCount;
|
||||
private bool? targetInventorySubmenuOpened;
|
||||
private PlayerCharacter? targetCharacter;
|
||||
|
||||
private string? clickedPlayerName;
|
||||
private ushort? clickedPlayerWorld;
|
||||
private ulong? clickedPlayerCid;
|
||||
private uint? clickedPlayerId;
|
||||
|
||||
private bool multipleTriggerOne;
|
||||
private bool multipleTriggerTwo;
|
||||
private ExcelSheet<Item> itemSheet;
|
||||
private ExcelSheet<Materia> materiaSheet;
|
||||
private ExcelSheet<Stain> stainSheet;
|
||||
|
||||
private enum SubStep
|
||||
{
|
||||
Start,
|
||||
TestItem,
|
||||
TestGameObject,
|
||||
TestSubMenu,
|
||||
TestMultiple,
|
||||
TestInventoryAndSubmenu,
|
||||
TestDefault,
|
||||
Finish,
|
||||
}
|
||||
*/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "Test Context Menu";
|
||||
|
|
@ -45,23 +43,24 @@ internal class ContextMenuAgingStep : IAgingStep
|
|||
/// <inheritdoc/>
|
||||
public SelfTestStepResult RunStep()
|
||||
{
|
||||
/*
|
||||
var contextMenu = Service<ContextMenu>.Get();
|
||||
var dataMgr = Service<DataManager>.Get();
|
||||
this.itemSheet = dataMgr.GetExcelSheet<Item>()!;
|
||||
this.materiaSheet = dataMgr.GetExcelSheet<Materia>()!;
|
||||
this.stainSheet = dataMgr.GetExcelSheet<Stain>()!;
|
||||
|
||||
ImGui.Text(this.currentSubStep.ToString());
|
||||
|
||||
switch (this.currentSubStep)
|
||||
{
|
||||
case SubStep.Start:
|
||||
contextMenu.ContextMenuOpened += this.ContextMenuOnContextMenuOpened;
|
||||
contextMenu.OnMenuOpened += this.OnMenuOpened;
|
||||
this.currentSubStep++;
|
||||
break;
|
||||
case SubStep.TestItem:
|
||||
if (this.clickedItemId != 0)
|
||||
case SubStep.TestInventoryAndSubmenu:
|
||||
if (this.targetInventorySubmenuOpened == true)
|
||||
{
|
||||
var item = dataMgr.GetExcelSheet<Item>()!.GetRow(this.clickedItemId);
|
||||
ImGui.Text($"Did you click \"{item!.Name.RawString}\", hq:{this.clickedItemHq}, count:{this.clickedItemCount}?");
|
||||
ImGui.Text($"Is the data in the submenu correct?");
|
||||
|
||||
if (ImGui.Button("Yes"))
|
||||
this.currentSubStep++;
|
||||
|
|
@ -73,7 +72,7 @@ internal class ContextMenuAgingStep : IAgingStep
|
|||
}
|
||||
else
|
||||
{
|
||||
ImGui.Text("Right-click an item.");
|
||||
ImGui.Text("Right-click an item and select \"Self Test\".");
|
||||
|
||||
if (ImGui.Button("Skip"))
|
||||
this.currentSubStep++;
|
||||
|
|
@ -81,10 +80,10 @@ internal class ContextMenuAgingStep : IAgingStep
|
|||
|
||||
break;
|
||||
|
||||
case SubStep.TestGameObject:
|
||||
if (!this.clickedPlayerName.IsNullOrEmpty())
|
||||
case SubStep.TestDefault:
|
||||
if (this.targetCharacter is { } character)
|
||||
{
|
||||
ImGui.Text($"Did you click \"{this.clickedPlayerName}\", world:{this.clickedPlayerWorld}, cid:{this.clickedPlayerCid}, id:{this.clickedPlayerId}?");
|
||||
ImGui.Text($"Did you click \"{character.Name}\" ({character.ClassJob.GameData!.Abbreviation.ToDalamudString()})?");
|
||||
|
||||
if (ImGui.Button("Yes"))
|
||||
this.currentSubStep++;
|
||||
|
|
@ -103,149 +102,195 @@ internal class ContextMenuAgingStep : IAgingStep
|
|||
}
|
||||
|
||||
break;
|
||||
case SubStep.TestSubMenu:
|
||||
if (this.multipleTriggerOne && this.multipleTriggerTwo)
|
||||
{
|
||||
this.currentSubStep++;
|
||||
this.multipleTriggerOne = this.multipleTriggerTwo = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.Text("Right-click a character and select both options in the submenu.");
|
||||
case SubStep.Finish:
|
||||
return SelfTestStepResult.Pass;
|
||||
|
||||
if (ImGui.Button("Skip"))
|
||||
this.currentSubStep++;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case SubStep.TestMultiple:
|
||||
if (this.multipleTriggerOne && this.multipleTriggerTwo)
|
||||
{
|
||||
this.currentSubStep = SubStep.Finish;
|
||||
return SelfTestStepResult.Pass;
|
||||
}
|
||||
|
||||
ImGui.Text("Select both options on any context menu.");
|
||||
if (ImGui.Button("Skip"))
|
||||
this.currentSubStep++;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
return SelfTestStepResult.Waiting;
|
||||
*/
|
||||
|
||||
return SelfTestStepResult.Pass;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void CleanUp()
|
||||
{
|
||||
/*
|
||||
var contextMenu = Service<ContextMenu>.Get();
|
||||
contextMenu.ContextMenuOpened -= this.ContextMenuOnContextMenuOpened;
|
||||
contextMenu.OnMenuOpened -= this.OnMenuOpened;
|
||||
|
||||
this.currentSubStep = SubStep.Start;
|
||||
this.clickedItemId = 0;
|
||||
this.clickedPlayerName = null;
|
||||
this.multipleTriggerOne = this.multipleTriggerTwo = false;
|
||||
*/
|
||||
this.targetInventorySubmenuOpened = null;
|
||||
this.targetCharacter = null;
|
||||
}
|
||||
|
||||
/*
|
||||
private void ContextMenuOnContextMenuOpened(ContextMenuOpenedArgs args)
|
||||
private void OnMenuOpened(MenuOpenedArgs args)
|
||||
{
|
||||
Log.Information("Got context menu with parent addon: {ParentAddonName}, title:{Title}, itemcnt:{ItemCount}", args.ParentAddonName, args.Title, args.Items.Count);
|
||||
if (args.GameObjectContext != null)
|
||||
{
|
||||
Log.Information(" => GameObject:{GameObjectName} world:{World} cid:{Cid} id:{Id}", args.GameObjectContext.Name, args.GameObjectContext.WorldId, args.GameObjectContext.ContentId, args.GameObjectContext.Id);
|
||||
}
|
||||
|
||||
if (args.InventoryItemContext != null)
|
||||
{
|
||||
Log.Information(" => Inventory:{ItemId} hq:{Hq} count:{Count}", args.InventoryItemContext.Id, args.InventoryItemContext.IsHighQuality, args.InventoryItemContext.Count);
|
||||
}
|
||||
LogMenuOpened(args);
|
||||
|
||||
switch (this.currentSubStep)
|
||||
{
|
||||
case SubStep.TestSubMenu:
|
||||
args.AddCustomSubMenu("Aging Submenu", openedArgs =>
|
||||
case SubStep.TestInventoryAndSubmenu:
|
||||
if (args.MenuType == ContextMenuType.Inventory)
|
||||
{
|
||||
openedArgs.AddCustomItem("Submenu Item 1", _ =>
|
||||
args.AddMenuItem(new()
|
||||
{
|
||||
this.multipleTriggerOne = true;
|
||||
});
|
||||
|
||||
openedArgs.AddCustomItem("Submenu Item 2", _ =>
|
||||
{
|
||||
this.multipleTriggerTwo = true;
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
case SubStep.TestMultiple:
|
||||
args.AddCustomItem("Aging Item 1", _ =>
|
||||
{
|
||||
this.multipleTriggerOne = true;
|
||||
});
|
||||
|
||||
args.AddCustomItem("Aging Item 2", _ =>
|
||||
{
|
||||
this.multipleTriggerTwo = true;
|
||||
});
|
||||
|
||||
return;
|
||||
case SubStep.Finish:
|
||||
return;
|
||||
|
||||
default:
|
||||
switch (args.ParentAddonName)
|
||||
{
|
||||
case "Inventory":
|
||||
if (this.currentSubStep != SubStep.TestItem)
|
||||
return;
|
||||
|
||||
args.AddCustomItem("Aging Item", _ =>
|
||||
Name = "Self Test",
|
||||
Prefix = SeIconChar.Hyadelyn,
|
||||
PrefixColor = 56,
|
||||
Priority = -1,
|
||||
IsSubmenu = true,
|
||||
OnClicked = (MenuItemClickedArgs a) =>
|
||||
{
|
||||
this.clickedItemId = args.InventoryItemContext!.Id;
|
||||
this.clickedItemHq = args.InventoryItemContext!.IsHighQuality;
|
||||
this.clickedItemCount = args.InventoryItemContext!.Count;
|
||||
Log.Warning("Clicked item: {Id} hq:{Hq} count:{Count}", this.clickedItemId, this.clickedItemHq, this.clickedItemCount);
|
||||
});
|
||||
break;
|
||||
SeString name;
|
||||
uint count;
|
||||
var targetItem = (a.Target as MenuTargetInventory).TargetItem;
|
||||
if (targetItem is { } item)
|
||||
{
|
||||
name = (this.itemSheet.GetRow(item.ItemId)?.Name.ToDalamudString() ?? $"Unknown ({item.ItemId})") + (item.IsHq ? $" {SeIconChar.HighQuality.ToIconString()}" : string.Empty);
|
||||
count = item.Quantity;
|
||||
}
|
||||
else
|
||||
{
|
||||
name = "None";
|
||||
count = 0;
|
||||
}
|
||||
|
||||
case null:
|
||||
case "_PartyList":
|
||||
case "ChatLog":
|
||||
case "ContactList":
|
||||
case "ContentMemberList":
|
||||
case "CrossWorldLinkshell":
|
||||
case "FreeCompany":
|
||||
case "FriendList":
|
||||
case "LookingForGroup":
|
||||
case "LinkShell":
|
||||
case "PartyMemberList":
|
||||
case "SocialList":
|
||||
if (this.currentSubStep != SubStep.TestGameObject || args.GameObjectContext == null || args.GameObjectContext.Name.IsNullOrEmpty())
|
||||
return;
|
||||
a.OpenSubmenu(new MenuItem[]
|
||||
{
|
||||
new()
|
||||
{
|
||||
Name = "Name: " + name,
|
||||
IsEnabled = false,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = $"Count: {count}",
|
||||
IsEnabled = false,
|
||||
},
|
||||
});
|
||||
|
||||
args.AddCustomItem("Aging Character", _ =>
|
||||
{
|
||||
this.clickedPlayerName = args.GameObjectContext.Name!;
|
||||
this.clickedPlayerWorld = args.GameObjectContext.WorldId;
|
||||
this.clickedPlayerCid = args.GameObjectContext.ContentId;
|
||||
this.clickedPlayerId = args.GameObjectContext.Id;
|
||||
|
||||
Log.Warning("Clicked player: {Name} world:{World} cid:{Cid} id:{Id}", this.clickedPlayerName, this.clickedPlayerWorld, this.clickedPlayerCid, this.clickedPlayerId);
|
||||
});
|
||||
|
||||
break;
|
||||
this.targetInventorySubmenuOpened = true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case SubStep.TestDefault:
|
||||
if (args.Target is MenuTargetDefault { TargetObject: PlayerCharacter { } character })
|
||||
this.targetCharacter = character;
|
||||
break;
|
||||
|
||||
case SubStep.Finish:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void LogMenuOpened(MenuOpenedArgs args)
|
||||
{
|
||||
Log.Verbose($"Got {args.MenuType} context menu with addon 0x{args.AddonPtr:X8} ({args.AddonName}) and agent 0x{args.AgentPtr:X8}");
|
||||
if (args.Target is MenuTargetDefault targetDefault)
|
||||
{
|
||||
{
|
||||
var b = new StringBuilder();
|
||||
b.AppendLine($"Target: {targetDefault.TargetName}");
|
||||
b.AppendLine($"Home World: {targetDefault.TargetHomeWorld.GameData?.Name.ToDalamudString() ?? "Unknown"} ({targetDefault.TargetHomeWorld.Id})");
|
||||
b.AppendLine($"Content Id: 0x{targetDefault.TargetContentId:X8}");
|
||||
b.AppendLine($"Object Id: 0x{targetDefault.TargetObjectId:X8}");
|
||||
Log.Verbose(b.ToString());
|
||||
}
|
||||
|
||||
if (targetDefault.TargetCharacter is { } character)
|
||||
{
|
||||
var b = new StringBuilder();
|
||||
b.AppendLine($"Character: {character.Name}");
|
||||
|
||||
b.AppendLine($"Name: {character.Name}");
|
||||
b.AppendLine($"Content Id: 0x{character.ContentId:X8}");
|
||||
b.AppendLine($"FC Tag: {character.FCTag}");
|
||||
|
||||
b.AppendLine($"Job: {character.ClassJob.GameData?.Abbreviation.ToDalamudString() ?? "Unknown"} ({character.ClassJob.Id})");
|
||||
b.AppendLine($"Statuses: {string.Join(", ", character.Statuses.Select(s => s.GameData?.Name.ToDalamudString() ?? s.Id.ToString()))}");
|
||||
b.AppendLine($"Home World: {character.HomeWorld.GameData?.Name.ToDalamudString() ?? "Unknown"} ({character.HomeWorld.Id})");
|
||||
b.AppendLine($"Current World: {character.CurrentWorld.GameData?.Name.ToDalamudString() ?? "Unknown"} ({character.CurrentWorld.Id})");
|
||||
b.AppendLine($"Is From Other Server: {character.IsFromOtherServer}");
|
||||
|
||||
b.Append("Location: ");
|
||||
if (character.Location.GameData is { } location)
|
||||
b.Append($"{location.PlaceNameRegion.Value?.Name.ToDalamudString() ?? "Unknown"}/{location.PlaceNameZone.Value?.Name.ToDalamudString() ?? "Unknown"}/{location.PlaceName.Value?.Name.ToDalamudString() ?? "Unknown"}");
|
||||
else
|
||||
b.Append("Unknown");
|
||||
b.AppendLine($" ({character.Location.Id})");
|
||||
|
||||
b.AppendLine($"Grand Company: {character.GrandCompany.GameData?.Name.ToDalamudString() ?? "Unknown"} ({character.GrandCompany.Id})");
|
||||
b.AppendLine($"Client Language: {character.ClientLanguage}");
|
||||
b.AppendLine($"Languages: {string.Join(", ", character.Languages)}");
|
||||
b.AppendLine($"Gender: {character.Gender}");
|
||||
b.AppendLine($"Display Group: {character.DisplayGroup}");
|
||||
b.AppendLine($"Sort: {character.Sort}");
|
||||
|
||||
Log.Verbose(b.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Verbose($"Character: null");
|
||||
}
|
||||
}
|
||||
else if (args.Target is MenuTargetInventory targetInventory)
|
||||
{
|
||||
if (targetInventory.TargetItem is { } item)
|
||||
{
|
||||
var b = new StringBuilder();
|
||||
b.AppendLine($"Item: {(item.IsEmpty ? "None" : this.itemSheet.GetRow(item.ItemId)?.Name.ToDalamudString())} ({item.ItemId})");
|
||||
b.AppendLine($"Container: {item.ContainerType}");
|
||||
b.AppendLine($"Slot: {item.InventorySlot}");
|
||||
b.AppendLine($"Quantity: {item.Quantity}");
|
||||
b.AppendLine($"{(item.IsCollectable ? "Collectability" : "Spiritbond")}: {item.Spiritbond}");
|
||||
b.AppendLine($"Condition: {item.Condition / 300f:0.00}% ({item.Condition})");
|
||||
b.AppendLine($"Is HQ: {item.IsHq}");
|
||||
b.AppendLine($"Is Company Crest Applied: {item.IsCompanyCrestApplied}");
|
||||
b.AppendLine($"Is Relic: {item.IsRelic}");
|
||||
b.AppendLine($"Is Collectable: {item.IsCollectable}");
|
||||
|
||||
b.Append("Materia: ");
|
||||
var materias = new List<string>();
|
||||
foreach (var (materiaId, materiaGrade) in item.Materia.ToArray().Zip(item.MateriaGrade.ToArray()).Where(m => m.First != 0))
|
||||
{
|
||||
Log.Verbose($"{materiaId} {materiaGrade}");
|
||||
if (this.materiaSheet.GetRow(materiaId) is { } materia &&
|
||||
materia.Item[materiaGrade].Value is { } materiaItem)
|
||||
materias.Add($"{materiaItem.Name.ToDalamudString()}");
|
||||
else
|
||||
materias.Add($"Unknown (Id: {materiaId}, Grade: {materiaGrade})");
|
||||
}
|
||||
|
||||
if (materias.Count == 0)
|
||||
b.AppendLine("None");
|
||||
else
|
||||
b.AppendLine(string.Join(", ", materias));
|
||||
|
||||
b.Append($"Dye/Stain: ");
|
||||
if (item.Stain != 0)
|
||||
b.AppendLine($"{this.stainSheet.GetRow(item.Stain)?.Name.ToDalamudString() ?? "Unknown"} ({item.Stain})");
|
||||
else
|
||||
b.AppendLine("None");
|
||||
|
||||
b.Append("Glamoured Item: ");
|
||||
if (item.GlamourId != 0)
|
||||
b.AppendLine($"{this.itemSheet.GetRow(item.GlamourId)?.Name.ToDalamudString() ?? "Unknown"} ({item.GlamourId})");
|
||||
else
|
||||
b.AppendLine("None");
|
||||
|
||||
Log.Verbose(b.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Verbose("Item: null");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Verbose($"Target: Unknown ({args.Target?.GetType().Name ?? "null"})");
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,11 +68,11 @@ internal class SettingsWindow : Window
|
|||
var interfaceManager = Service<InterfaceManager>.Get();
|
||||
var fontAtlasFactory = Service<FontAtlasFactory>.Get();
|
||||
|
||||
var rebuildFont = fontAtlasFactory.UseAxis != configuration.UseAxisFontsFromGame;
|
||||
var rebuildFont = !Equals(fontAtlasFactory.DefaultFontSpec, configuration.DefaultFontSpec);
|
||||
rebuildFont |= !Equals(ImGui.GetIO().FontGlobalScale, configuration.GlobalUiScale);
|
||||
|
||||
ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale;
|
||||
fontAtlasFactory.UseAxisOverride = null;
|
||||
fontAtlasFactory.DefaultFontSpecOverride = null;
|
||||
|
||||
if (rebuildFont)
|
||||
interfaceManager.RebuildFonts();
|
||||
|
|
|
|||
|
|
@ -5,9 +5,14 @@ using System.Text;
|
|||
|
||||
using CheapLoc;
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.ImGuiFontChooserDialog;
|
||||
using Dalamud.Interface.Internal.Windows.PluginInstaller;
|
||||
using Dalamud.Interface.Internal.Windows.Settings.Widgets;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Utility;
|
||||
|
|
@ -21,31 +26,19 @@ public class SettingsTabLook : SettingsTab
|
|||
{
|
||||
private static readonly (string, float)[] GlobalUiScalePresets =
|
||||
{
|
||||
("9.6pt##DalamudSettingsGlobalUiScaleReset96", 9.6f / InterfaceManager.DefaultFontSizePt),
|
||||
("12pt##DalamudSettingsGlobalUiScaleReset12", 12f / InterfaceManager.DefaultFontSizePt),
|
||||
("14pt##DalamudSettingsGlobalUiScaleReset14", 14f / InterfaceManager.DefaultFontSizePt),
|
||||
("18pt##DalamudSettingsGlobalUiScaleReset18", 18f / InterfaceManager.DefaultFontSizePt),
|
||||
("24pt##DalamudSettingsGlobalUiScaleReset24", 24f / InterfaceManager.DefaultFontSizePt),
|
||||
("36pt##DalamudSettingsGlobalUiScaleReset36", 36f / InterfaceManager.DefaultFontSizePt),
|
||||
("80%##DalamudSettingsGlobalUiScaleReset96", 0.8f),
|
||||
("100%##DalamudSettingsGlobalUiScaleReset12", 1f),
|
||||
("117%##DalamudSettingsGlobalUiScaleReset14", 14 / 12f),
|
||||
("150%##DalamudSettingsGlobalUiScaleReset18", 1.5f),
|
||||
("200%##DalamudSettingsGlobalUiScaleReset24", 2f),
|
||||
("300%##DalamudSettingsGlobalUiScaleReset36", 3f),
|
||||
};
|
||||
|
||||
private float globalUiScale;
|
||||
private IFontSpec defaultFontSpec = null!;
|
||||
|
||||
public override SettingsEntry[] Entries { get; } =
|
||||
{
|
||||
new GapSettingsEntry(5),
|
||||
|
||||
new SettingsEntry<bool>(
|
||||
Loc.Localize("DalamudSettingToggleAxisFonts", "Use AXIS fonts as default Dalamud font"),
|
||||
Loc.Localize("DalamudSettingToggleUiAxisFontsHint", "Use AXIS fonts (the game's main UI fonts) as default Dalamud font."),
|
||||
c => c.UseAxisFontsFromGame,
|
||||
(v, c) => c.UseAxisFontsFromGame = v,
|
||||
v =>
|
||||
{
|
||||
Service<FontAtlasFactory>.Get().UseAxisOverride = v;
|
||||
Service<InterfaceManager>.Get().RebuildFonts();
|
||||
}),
|
||||
|
||||
new GapSettingsEntry(5, true),
|
||||
|
||||
new ButtonSettingsEntry(
|
||||
|
|
@ -178,10 +171,10 @@ public class SettingsTabLook : SettingsTab
|
|||
}
|
||||
}
|
||||
|
||||
var globalUiScaleInPt = 12f * this.globalUiScale;
|
||||
if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref globalUiScaleInPt, 0.1f, 9.6f, 36f, "%.1fpt", ImGuiSliderFlags.AlwaysClamp))
|
||||
var globalUiScaleInPct = 100f * this.globalUiScale;
|
||||
if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref globalUiScaleInPct, 1f, 80f, 300f, "%.0f%%", ImGuiSliderFlags.AlwaysClamp))
|
||||
{
|
||||
this.globalUiScale = globalUiScaleInPt / 12f;
|
||||
this.globalUiScale = globalUiScaleInPct / 100f;
|
||||
ImGui.GetIO().FontGlobalScale = this.globalUiScale;
|
||||
interfaceManager.RebuildFonts();
|
||||
}
|
||||
|
|
@ -201,12 +194,53 @@ public class SettingsTabLook : SettingsTab
|
|||
}
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
|
||||
if (ImGui.Button(Loc.Localize("DalamudSettingChooseDefaultFont", "Choose Default Font")))
|
||||
{
|
||||
var faf = Service<FontAtlasFactory>.Get();
|
||||
var fcd = new SingleFontChooserDialog(
|
||||
faf.CreateFontAtlas($"{nameof(SettingsTabLook)}:Default", FontAtlasAutoRebuildMode.Async));
|
||||
fcd.SelectedFont = (SingleFontSpec)this.defaultFontSpec;
|
||||
fcd.FontFamilyExcludeFilter = x => x is DalamudDefaultFontAndFamilyId;
|
||||
interfaceManager.Draw += fcd.Draw;
|
||||
fcd.ResultTask.ContinueWith(
|
||||
r => Service<Framework>.Get().RunOnFrameworkThread(
|
||||
() =>
|
||||
{
|
||||
interfaceManager.Draw -= fcd.Draw;
|
||||
fcd.Dispose();
|
||||
|
||||
_ = r.Exception;
|
||||
if (!r.IsCompletedSuccessfully)
|
||||
return;
|
||||
|
||||
faf.DefaultFontSpecOverride = this.defaultFontSpec = r.Result;
|
||||
interfaceManager.RebuildFonts();
|
||||
}));
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
using (interfaceManager.MonoFontHandle?.Push())
|
||||
{
|
||||
if (ImGui.Button(Loc.Localize("DalamudSettingResetDefaultFont", "Reset Default Font")))
|
||||
{
|
||||
var faf = Service<FontAtlasFactory>.Get();
|
||||
faf.DefaultFontSpecOverride =
|
||||
this.defaultFontSpec =
|
||||
new SingleFontSpec { FontId = new GameFontAndFamilyId(GameFontFamily.Axis) };
|
||||
interfaceManager.RebuildFonts();
|
||||
}
|
||||
}
|
||||
|
||||
base.Draw();
|
||||
}
|
||||
|
||||
public override void Load()
|
||||
{
|
||||
this.globalUiScale = Service<DalamudConfiguration>.Get().GlobalUiScale;
|
||||
this.defaultFontSpec = Service<FontAtlasFactory>.Get().DefaultFontSpec;
|
||||
|
||||
base.Load();
|
||||
}
|
||||
|
|
@ -214,6 +248,7 @@ public class SettingsTabLook : SettingsTab
|
|||
public override void Save()
|
||||
{
|
||||
Service<DalamudConfiguration>.Get().GlobalUiScale = this.globalUiScale;
|
||||
Service<DalamudConfiguration>.Get().DefaultFontSpec = this.defaultFontSpec;
|
||||
|
||||
base.Save();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ public class StyleEditorWindow : Window
|
|||
this.SizeConstraints = new WindowSizeConstraints
|
||||
{
|
||||
MinimumSize = new Vector2(890, 560),
|
||||
MaximumSize = new Vector2(10000, 10000),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
33
Dalamud/Interface/ManagedFontAtlas/FontScaleMode.cs
Normal file
33
Dalamud/Interface/ManagedFontAtlas/FontScaleMode.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
using Dalamud.Interface.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies how should global font scale affect a font.
|
||||
/// </summary>
|
||||
public enum FontScaleMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Do the default handling. Dalamud will load the sufficienty large font that will accomodate the global scale,
|
||||
/// and stretch the loaded glyphs so that they look pixel-perfect after applying global scale on drawing.
|
||||
/// Note that bitmap fonts and game fonts will always look blurry if they're not in their original sizes.
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// Do nothing with the font. Dalamud will load the font with the size that is exactly as specified.
|
||||
/// On drawing, the font will look blurry due to stretching.
|
||||
/// Intended for use with custom scale handling.
|
||||
/// </summary>
|
||||
SkipHandling,
|
||||
|
||||
/// <summary>
|
||||
/// Stretch the glyphs of the loaded font by the inverse of the global scale.
|
||||
/// On drawing, the font will always render exactly as the requested size without blurring, as long as
|
||||
/// <see cref="ImGuiHelpers.GlobalScale"/> and <see cref="ImGui.SetWindowFontScale"/> do not affect the scale any
|
||||
/// further. Note that bitmap fonts and game fonts will always look blurry if they're not in their original sizes.
|
||||
/// </summary>
|
||||
UndoGlobalScale,
|
||||
}
|
||||
|
|
@ -8,7 +8,8 @@ using ImGuiNET;
|
|||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper for <see cref="ImFontAtlasPtr"/>.
|
||||
/// Wrapper for <see cref="ImFontAtlasPtr"/>.<br />
|
||||
/// Not intended for plugins to implement.
|
||||
/// </summary>
|
||||
public interface IFontAtlas : IDisposable
|
||||
{
|
||||
|
|
@ -93,11 +94,15 @@ public interface IFontAtlas : IDisposable
|
|||
/// </summary>
|
||||
/// <param name="buildStepDelegate">Callback for <see cref="IFontAtlas.BuildStepChange"/>.</param>
|
||||
/// <returns>Handle to a font that may or may not be ready yet.</returns>
|
||||
/// <remarks>
|
||||
/// Consider calling <see cref="IFontAtlasBuildToolkitPreBuild.AttachExtraGlyphsForDalamudLanguage"/> to support
|
||||
/// glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language users.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <b>On initialization</b>:
|
||||
/// <code>
|
||||
/// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => {
|
||||
/// var config = new SafeFontConfig { SizePx = 16 };
|
||||
/// var config = new SafeFontConfig { SizePx = UiBuilder.DefaultFontSizePx };
|
||||
/// config.MergeFont = tk.AddFontFromFile(@"C:\Windows\Fonts\comic.ttf", config);
|
||||
/// tk.AddGameSymbol(config);
|
||||
/// tk.AddExtraGlyphsForDalamudLanguage(config);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ using ImGuiNET;
|
|||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <summary>
|
||||
/// Common stuff for <see cref="IFontAtlasBuildToolkitPreBuild"/> and <see cref="IFontAtlasBuildToolkitPostBuild"/>.
|
||||
/// Common stuff for <see cref="IFontAtlasBuildToolkitPreBuild"/> and <see cref="IFontAtlasBuildToolkitPostBuild"/>.<br />
|
||||
/// Not intended for plugins to implement.
|
||||
/// </summary>
|
||||
public interface IFontAtlasBuildToolkit
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,20 +1,23 @@
|
|||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <summary>
|
||||
/// Toolkit for use when the build state is <see cref="FontAtlasBuildStep.PostBuild"/>.
|
||||
/// Toolkit for use when the build state is <see cref="FontAtlasBuildStep.PostBuild"/>.<br />
|
||||
/// Not intended for plugins to implement.
|
||||
/// </summary>
|
||||
public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets whether global scaling is ignored for the given font.
|
||||
/// </summary>
|
||||
/// <param name="fontPtr">The font.</param>
|
||||
/// <returns>True if ignored.</returns>
|
||||
bool IsGlobalScaleIgnored(ImFontPtr fontPtr);
|
||||
/// <inheritdoc cref="IFontAtlasBuildToolkitPreBuild.IsGlobalScaleIgnored"/>
|
||||
[Obsolete($"Use {nameof(this.GetFontScaleMode)}")]
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => this.GetFontScaleMode(fontPtr) == FontScaleMode.UndoGlobalScale;
|
||||
|
||||
/// <inheritdoc cref="IFontAtlasBuildToolkitPreBuild.GetFontScaleMode"/>
|
||||
FontScaleMode GetFontScaleMode(ImFontPtr fontPtr);
|
||||
|
||||
/// <summary>
|
||||
/// Stores a texture to be managed with the atlas.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
|
|
@ -10,6 +12,7 @@ namespace Dalamud.Interface.ManagedFontAtlas;
|
|||
|
||||
/// <summary>
|
||||
/// Toolkit for use when the build state is <see cref="FontAtlasBuildStep.PreBuild"/>.<br />
|
||||
/// Not intended for plugins to implement.<br />
|
||||
/// <br />
|
||||
/// After <see cref="FontAtlasBuildStepDelegate"/> returns,
|
||||
/// either <see cref="IFontAtlasBuildToolkit.Font"/> must be set,
|
||||
|
|
@ -43,14 +46,43 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit
|
|||
/// </summary>
|
||||
/// <param name="fontPtr">The font.</param>
|
||||
/// <returns>Same <see cref="ImFontPtr"/> with <paramref name="fontPtr"/>.</returns>
|
||||
ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr);
|
||||
[Obsolete(
|
||||
$"Use {nameof(this.SetFontScaleMode)} with {nameof(FontScaleMode)}.{nameof(FontScaleMode.UndoGlobalScale)}")]
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr) => this.SetFontScaleMode(fontPtr, FontScaleMode.UndoGlobalScale);
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether global scaling is ignored for the given font.
|
||||
/// </summary>
|
||||
/// <param name="fontPtr">The font.</param>
|
||||
/// <returns>True if ignored.</returns>
|
||||
bool IsGlobalScaleIgnored(ImFontPtr fontPtr);
|
||||
[Obsolete($"Use {nameof(this.GetFontScaleMode)}")]
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => this.GetFontScaleMode(fontPtr) == FontScaleMode.UndoGlobalScale;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the scaling mode for the given font.
|
||||
/// </summary>
|
||||
/// <param name="fontPtr">The font, returned from <see cref="AddFontFromFile"/> and alike.
|
||||
/// Note that <see cref="IFontAtlasBuildToolkit.Font"/> property is not guaranteed to be automatically updated upon
|
||||
/// calling font adding functions. Pass the return value from font adding functions, not
|
||||
/// <see cref="IFontAtlasBuildToolkit.Font"/> property.</param>
|
||||
/// <param name="mode">The scaling mode.</param>
|
||||
/// <returns><paramref name="fontPtr"/>.</returns>
|
||||
ImFontPtr SetFontScaleMode(ImFontPtr fontPtr, FontScaleMode mode);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the scaling mode for the given font.
|
||||
/// </summary>
|
||||
/// <param name="fontPtr">The font.</param>
|
||||
/// <returns>The scaling mode.</returns>
|
||||
FontScaleMode GetFontScaleMode(ImFontPtr fontPtr);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a function to be run after build.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to run.</param>
|
||||
void RegisterPostBuild(Action action);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a font from memory region allocated using <see cref="ImGuiHelpers.AllocateMemory"/>.<br />
|
||||
|
|
@ -134,7 +166,12 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit
|
|||
/// As this involves adding multiple fonts, calling this function will set <see cref="IFontAtlasBuildToolkit.Font"/>
|
||||
/// as the return value of this function, if it was empty before.
|
||||
/// </summary>
|
||||
/// <param name="sizePx">Font size in pixels.</param>
|
||||
/// <param name="sizePx">
|
||||
/// Font size in pixels.
|
||||
/// If a negative value is supplied,
|
||||
/// (<see cref="UiBuilder.DefaultFontSpec"/>.<see cref="IFontSpec.SizePx"/> * <paramref name="sizePx"/>) will be
|
||||
/// used as the font size. Specify -1 to use the default font size.
|
||||
/// </param>
|
||||
/// <param name="glyphRanges">The glyph ranges. Use <see cref="FontAtlasBuildToolkitUtilities"/>.ToGlyphRange to build.</param>
|
||||
/// <returns>A font returned from <see cref="ImFontAtlasPtr.AddFont"/>.</returns>
|
||||
ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges = null);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ using ImGuiNET;
|
|||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a reference counting handle for fonts.
|
||||
/// Represents a reference counting handle for fonts.<br />
|
||||
/// Not intended for plugins to implement.
|
||||
/// </summary>
|
||||
public interface IFontHandle : IDisposable
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ namespace Dalamud.Interface.ManagedFontAtlas;
|
|||
|
||||
/// <summary>
|
||||
/// The wrapper for <see cref="ImFontPtr"/>, guaranteeing that the associated data will be available as long as
|
||||
/// this struct is not disposed.
|
||||
/// this struct is not disposed.<br />
|
||||
/// Not intended for plugins to implement.
|
||||
/// </summary>
|
||||
public interface ILockedImFont : IDisposable
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Runtime.InteropServices;
|
|||
using System.Text.Unicode;
|
||||
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Utility;
|
||||
|
|
@ -42,6 +43,7 @@ internal sealed partial class FontAtlasFactory
|
|||
private readonly GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance;
|
||||
private readonly FontAtlasFactory factory;
|
||||
private readonly FontAtlasBuiltData data;
|
||||
private readonly List<Action> registeredPostBuildActions = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BuildToolkit"/> class.
|
||||
|
|
@ -81,9 +83,9 @@ internal sealed partial class FontAtlasFactory
|
|||
public ImVectorWrapper<ImFontPtr> Fonts => this.data.Fonts;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of fonts to ignore global scale.
|
||||
/// Gets the font scale modes.
|
||||
/// </summary>
|
||||
public List<ImFontPtr> GlobalScaleExclusions { get; } = new();
|
||||
private Dictionary<ImFontPtr, FontScaleMode> FontScaleModes { get; } = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose() => this.disposeAfterBuild.Dispose();
|
||||
|
|
@ -149,19 +151,22 @@ internal sealed partial class FontAtlasFactory
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr)
|
||||
public ImFontPtr SetFontScaleMode(ImFontPtr fontPtr, FontScaleMode scaleMode)
|
||||
{
|
||||
this.GlobalScaleExclusions.Add(fontPtr);
|
||||
this.FontScaleModes[fontPtr] = scaleMode;
|
||||
return fontPtr;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IFontAtlasBuildToolkitPreBuild.IsGlobalScaleIgnored"/>
|
||||
public bool IsGlobalScaleIgnored(ImFontPtr fontPtr) =>
|
||||
this.GlobalScaleExclusions.Contains(fontPtr);
|
||||
/// <inheritdoc cref="IFontAtlasBuildToolkitPreBuild.GetFontScaleMode"/>
|
||||
public FontScaleMode GetFontScaleMode(ImFontPtr fontPtr) =>
|
||||
this.FontScaleModes.GetValueOrDefault(fontPtr, FontScaleMode.Default);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError) =>
|
||||
this.data.AddNewTexture(textureWrap, disposeOnError);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RegisterPostBuild(Action action) => this.registeredPostBuildActions.Add(action);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory(
|
||||
|
|
@ -180,6 +185,7 @@ internal sealed partial class FontAtlasFactory
|
|||
dataSize,
|
||||
debugTag);
|
||||
|
||||
var font = default(ImFontPtr);
|
||||
try
|
||||
{
|
||||
fontConfig.ThrowOnInvalidValues();
|
||||
|
|
@ -187,6 +193,7 @@ internal sealed partial class FontAtlasFactory
|
|||
var raw = fontConfig.Raw with
|
||||
{
|
||||
FontData = dataPointer,
|
||||
FontDataOwnedByAtlas = 1,
|
||||
FontDataSize = dataSize,
|
||||
};
|
||||
|
||||
|
|
@ -198,7 +205,7 @@ internal sealed partial class FontAtlasFactory
|
|||
|
||||
TrueTypeUtils.CheckImGuiCompatibleOrThrow(raw);
|
||||
|
||||
var font = this.NewImAtlas.AddFont(&raw);
|
||||
font = this.NewImAtlas.AddFont(&raw);
|
||||
|
||||
var dataHash = default(HashCode);
|
||||
dataHash.AddBytes(new(dataPointer, dataSize));
|
||||
|
|
@ -235,8 +242,23 @@ internal sealed partial class FontAtlasFactory
|
|||
}
|
||||
catch
|
||||
{
|
||||
if (!font.IsNull())
|
||||
{
|
||||
// Note that for both RemoveAt calls, corresponding destructors will be called.
|
||||
|
||||
var configIndex = this.data.ConfigData.FindIndex(x => x.DstFont == font.NativePtr);
|
||||
if (configIndex >= 0)
|
||||
this.data.ConfigData.RemoveAt(configIndex);
|
||||
|
||||
var index = this.Fonts.IndexOf(font);
|
||||
if (index >= 0)
|
||||
this.Fonts.RemoveAt(index);
|
||||
}
|
||||
|
||||
// ImFontConfig has no destructor, and does not free the data.
|
||||
if (freeOnException)
|
||||
ImGuiNative.igMemFree(dataPointer);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
|
@ -314,18 +336,32 @@ internal sealed partial class FontAtlasFactory
|
|||
/// <inheritdoc/>
|
||||
public ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges)
|
||||
{
|
||||
ImFontPtr font;
|
||||
ImFontPtr font = default;
|
||||
glyphRanges ??= this.factory.DefaultGlyphRanges;
|
||||
if (this.factory.UseAxis)
|
||||
|
||||
var dfid = this.factory.DefaultFontSpec;
|
||||
if (sizePx < 0f)
|
||||
sizePx *= -dfid.SizePx;
|
||||
|
||||
if (dfid is SingleFontSpec sfs)
|
||||
{
|
||||
font = this.AddGameGlyphs(new(GameFontFamily.Axis, sizePx), glyphRanges, default);
|
||||
if (sfs.FontId is DalamudDefaultFontAndFamilyId)
|
||||
{
|
||||
// invalid; calling sfs.AddToBuildToolkit calls this function, causing infinite recursion
|
||||
}
|
||||
else
|
||||
{
|
||||
sfs = sfs with { SizePx = sizePx };
|
||||
font = sfs.AddToBuildToolkit(this);
|
||||
if (sfs.FontId is not GameFontAndFamilyId { GameFontFamily: GameFontFamily.Axis })
|
||||
this.AddGameSymbol(new() { SizePx = sizePx, MergeFont = font });
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
if (font.IsNull())
|
||||
{
|
||||
font = this.AddDalamudAssetFont(
|
||||
DalamudAsset.NotoSansJpMedium,
|
||||
new() { SizePx = sizePx, GlyphRanges = glyphRanges });
|
||||
this.AddGameSymbol(new() { SizePx = sizePx, MergeFont = font });
|
||||
// fall back to AXIS fonts
|
||||
font = this.AddGameGlyphs(new(GameFontFamily.Axis, sizePx), glyphRanges, default);
|
||||
}
|
||||
|
||||
this.AttachExtraGlyphsForDalamudLanguage(new() { SizePx = sizePx, MergeFont = font });
|
||||
|
|
@ -460,17 +496,17 @@ internal sealed partial class FontAtlasFactory
|
|||
var configData = this.data.ConfigData;
|
||||
foreach (ref var config in configData.DataSpan)
|
||||
{
|
||||
if (this.GlobalScaleExclusions.Contains(new(config.DstFont)))
|
||||
if (this.GetFontScaleMode(config.DstFont) != FontScaleMode.Default)
|
||||
continue;
|
||||
|
||||
config.SizePixels *= this.Scale;
|
||||
|
||||
config.GlyphMaxAdvanceX *= this.Scale;
|
||||
if (float.IsInfinity(config.GlyphMaxAdvanceX))
|
||||
if (float.IsInfinity(config.GlyphMaxAdvanceX) || float.IsNaN(config.GlyphMaxAdvanceX))
|
||||
config.GlyphMaxAdvanceX = config.GlyphMaxAdvanceX > 0 ? float.MaxValue : -float.MaxValue;
|
||||
|
||||
config.GlyphMinAdvanceX *= this.Scale;
|
||||
if (float.IsInfinity(config.GlyphMinAdvanceX))
|
||||
if (float.IsInfinity(config.GlyphMinAdvanceX) || float.IsNaN(config.GlyphMinAdvanceX))
|
||||
config.GlyphMinAdvanceX = config.GlyphMinAdvanceX > 0 ? float.MaxValue : -float.MaxValue;
|
||||
|
||||
config.GlyphOffset *= this.Scale;
|
||||
|
|
@ -500,7 +536,7 @@ internal sealed partial class FontAtlasFactory
|
|||
var scale = this.Scale;
|
||||
foreach (ref var font in this.Fonts.DataSpan)
|
||||
{
|
||||
if (!this.GlobalScaleExclusions.Contains(font))
|
||||
if (this.GetFontScaleMode(font) != FontScaleMode.SkipHandling)
|
||||
font.AdjustGlyphMetrics(1 / scale, 1 / scale);
|
||||
|
||||
foreach (var c in FallbackCodepoints)
|
||||
|
|
@ -531,6 +567,13 @@ internal sealed partial class FontAtlasFactory
|
|||
substance.OnPostBuild(this);
|
||||
}
|
||||
|
||||
public void PostBuildCallbacks()
|
||||
{
|
||||
foreach (var ac in this.registeredPostBuildActions)
|
||||
ac.InvokeSafely();
|
||||
this.registeredPostBuildActions.Clear();
|
||||
}
|
||||
|
||||
public unsafe void UploadTextures()
|
||||
{
|
||||
var buf = Array.Empty<byte>();
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ internal sealed partial class FontAtlasFactory
|
|||
|
||||
private class FontAtlasBuiltData : IRefCountable
|
||||
{
|
||||
// Field for debugging.
|
||||
private static int numActiveInstances;
|
||||
|
||||
private readonly List<IDalamudTextureWrap> wraps;
|
||||
private readonly List<IFontHandleSubstance> substances;
|
||||
|
||||
|
|
@ -73,6 +76,9 @@ internal sealed partial class FontAtlasFactory
|
|||
|
||||
this.Garbage.Add(() => ImGuiNative.ImFontAtlas_destroy(atlasPtr));
|
||||
this.IsBuildInProgress = true;
|
||||
|
||||
Interlocked.Increment(ref numActiveInstances);
|
||||
this.Garbage.Add(() => Interlocked.Decrement(ref numActiveInstances));
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -658,7 +664,7 @@ internal sealed partial class FontAtlasFactory
|
|||
toolkit = res.CreateToolkit(this.factory, isAsync);
|
||||
|
||||
// PreBuildSubstances deals with toolkit.Add... function family. Do this first.
|
||||
var defaultFont = toolkit.AddDalamudDefaultFont(InterfaceManager.DefaultFontSizePx, null);
|
||||
var defaultFont = toolkit.AddDalamudDefaultFont(-1, null);
|
||||
|
||||
this.BuildStepChange?.Invoke(toolkit);
|
||||
toolkit.PreBuildSubstances();
|
||||
|
|
@ -679,6 +685,7 @@ internal sealed partial class FontAtlasFactory
|
|||
|
||||
toolkit.PostBuild();
|
||||
toolkit.PostBuildSubstances();
|
||||
toolkit.PostBuildCallbacks();
|
||||
this.BuildStepChange?.Invoke(toolkit);
|
||||
|
||||
foreach (var font in toolkit.Fonts)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
|||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Storage.Assets;
|
||||
|
|
@ -108,14 +109,29 @@ internal sealed partial class FontAtlasFactory
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to override configuration for UseAxis.
|
||||
/// Gets or sets a value indicating whether to override configuration for <see cref="DefaultFontSpec"/>.
|
||||
/// </summary>
|
||||
public bool? UseAxisOverride { get; set; } = null;
|
||||
public IFontSpec? DefaultFontSpecOverride { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether to use AXIS fonts.
|
||||
/// Gets the default font ID.
|
||||
/// </summary>
|
||||
public bool UseAxis => this.UseAxisOverride ?? Service<DalamudConfiguration>.Get().UseAxisFontsFromGame;
|
||||
public IFontSpec DefaultFontSpec =>
|
||||
this.DefaultFontSpecOverride
|
||||
?? Service<DalamudConfiguration>.Get().DefaultFontSpec
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
?? (Service<DalamudConfiguration>.Get().UseAxisFontsFromGame
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
? new()
|
||||
{
|
||||
FontId = new GameFontAndFamilyId(GameFontFamily.Axis),
|
||||
SizePx = InterfaceManager.DefaultFontSizePx,
|
||||
}
|
||||
: new SingleFontSpec
|
||||
{
|
||||
FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansJpMedium),
|
||||
SizePx = InterfaceManager.DefaultFontSizePx + 1,
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Gets the service instance of <see cref="Framework"/>.
|
||||
|
|
@ -229,6 +245,25 @@ internal sealed partial class FontAtlasFactory
|
|||
|
||||
private static T ExtractResult<T>(Task<T> t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult();
|
||||
|
||||
/// <summary>
|
||||
/// Clones a texture wrap, by getting a new reference to the underlying <see cref="ShaderResourceView"/> and the
|
||||
/// texture behind.
|
||||
/// </summary>
|
||||
/// <param name="wrap">The <see cref="IDalamudTextureWrap"/> to clone from.</param>
|
||||
/// <returns>The cloned <see cref="IDalamudTextureWrap"/>.</returns>
|
||||
private static IDalamudTextureWrap CloneTextureWrap(IDalamudTextureWrap wrap)
|
||||
{
|
||||
var srv = CppObject.FromPointer<ShaderResourceView>(wrap.ImGuiHandle);
|
||||
using var res = srv.Resource;
|
||||
using var tex2D = res.QueryInterface<Texture2D>();
|
||||
var description = tex2D.Description;
|
||||
return new DalamudTextureWrap(
|
||||
new D3DTextureWrap(
|
||||
srv.QueryInterface<ShaderResourceView>(),
|
||||
description.Width,
|
||||
description.Height));
|
||||
}
|
||||
|
||||
private static unsafe void ExtractChannelFromB8G8R8A8(
|
||||
Span<byte> target,
|
||||
ReadOnlySpan<byte> source,
|
||||
|
|
@ -266,25 +301,6 @@ internal sealed partial class FontAtlasFactory
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clones a texture wrap, by getting a new reference to the underlying <see cref="ShaderResourceView"/> and the
|
||||
/// texture behind.
|
||||
/// </summary>
|
||||
/// <param name="wrap">The <see cref="IDalamudTextureWrap"/> to clone from.</param>
|
||||
/// <returns>The cloned <see cref="IDalamudTextureWrap"/>.</returns>
|
||||
private static IDalamudTextureWrap CloneTextureWrap(IDalamudTextureWrap wrap)
|
||||
{
|
||||
var srv = CppObject.FromPointer<ShaderResourceView>(wrap.ImGuiHandle);
|
||||
using var res = srv.Resource;
|
||||
using var tex2D = res.QueryInterface<Texture2D>();
|
||||
var description = tex2D.Description;
|
||||
return new DalamudTextureWrap(
|
||||
new D3DTextureWrap(
|
||||
srv.QueryInterface<ShaderResourceView>(),
|
||||
description.Width,
|
||||
description.Height));
|
||||
}
|
||||
|
||||
private static unsafe void ExtractChannelFromB4G4R4A4(
|
||||
Span<byte> target,
|
||||
ReadOnlySpan<byte> source,
|
||||
|
|
@ -317,7 +333,7 @@ internal sealed partial class FontAtlasFactory
|
|||
v |= v << 4;
|
||||
*wptr = (uint)((v << 24) | 0x00FFFFFF);
|
||||
wptr++;
|
||||
rptr += 4;
|
||||
rptr += 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -345,17 +345,36 @@ internal class GamePrebakedFontHandle : FontHandle
|
|||
{
|
||||
foreach (var (font, style, ranges) in this.attachments)
|
||||
{
|
||||
var effectiveStyle =
|
||||
toolkitPreBuild.IsGlobalScaleIgnored(font)
|
||||
? style.Scale(1 / toolkitPreBuild.Scale)
|
||||
: style;
|
||||
if (!this.fonts.TryGetValue(style, out var plan))
|
||||
{
|
||||
plan = new(
|
||||
effectiveStyle,
|
||||
toolkitPreBuild.Scale,
|
||||
this.handleManager.GameFontTextureProvider,
|
||||
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
|
||||
switch (toolkitPreBuild.GetFontScaleMode(font))
|
||||
{
|
||||
case FontScaleMode.Default:
|
||||
default:
|
||||
plan = new(
|
||||
style,
|
||||
toolkitPreBuild.Scale,
|
||||
this.handleManager.GameFontTextureProvider,
|
||||
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
|
||||
break;
|
||||
|
||||
case FontScaleMode.SkipHandling:
|
||||
plan = new(
|
||||
style,
|
||||
1f,
|
||||
this.handleManager.GameFontTextureProvider,
|
||||
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
|
||||
break;
|
||||
|
||||
case FontScaleMode.UndoGlobalScale:
|
||||
plan = new(
|
||||
style.Scale(1 / toolkitPreBuild.Scale),
|
||||
toolkitPreBuild.Scale,
|
||||
this.handleManager.GameFontTextureProvider,
|
||||
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
|
||||
break;
|
||||
}
|
||||
|
||||
this.fonts[style] = plan;
|
||||
}
|
||||
|
||||
|
|
@ -620,15 +639,14 @@ internal class GamePrebakedFontHandle : FontHandle
|
|||
public unsafe void CopyGlyphsToRanges(IFontAtlasBuildToolkitPostBuild toolkitPostBuild)
|
||||
{
|
||||
var scale = this.Style.SizePt / this.Fdt.FontHeader.Size;
|
||||
var atlasScale = toolkitPostBuild.Scale;
|
||||
var round = 1 / atlasScale;
|
||||
|
||||
foreach (var (font, rangeBits) in this.Ranges)
|
||||
{
|
||||
if (font.NativePtr == this.FullRangeFont.NativePtr)
|
||||
continue;
|
||||
|
||||
var noGlobalScale = toolkitPostBuild.IsGlobalScaleIgnored(font);
|
||||
var fontScaleMode = toolkitPostBuild.GetFontScaleMode(font);
|
||||
var round = fontScaleMode == FontScaleMode.SkipHandling ? 1 : 1 / toolkitPostBuild.Scale;
|
||||
|
||||
var lookup = font.IndexLookupWrapped();
|
||||
var glyphs = font.GlyphsWrapped();
|
||||
|
|
@ -649,7 +667,7 @@ internal class GamePrebakedFontHandle : FontHandle
|
|||
|
||||
ref var g = ref glyphs[glyphIndex];
|
||||
g = sourceGlyph;
|
||||
if (noGlobalScale)
|
||||
if (fontScaleMode == FontScaleMode.SkipHandling)
|
||||
{
|
||||
g.XY *= scale;
|
||||
g.AdvanceX *= scale;
|
||||
|
|
@ -673,7 +691,7 @@ internal class GamePrebakedFontHandle : FontHandle
|
|||
continue;
|
||||
if (!rangeBits[leftInt] || !rangeBits[rightInt])
|
||||
continue;
|
||||
if (noGlobalScale)
|
||||
if (fontScaleMode == FontScaleMode.SkipHandling)
|
||||
{
|
||||
font.AddKerningPair((ushort)leftInt, (ushort)rightInt, k.RightOffset * scale);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public struct SafeFontConfig
|
|||
this.PixelSnapH = true;
|
||||
this.GlyphMaxAdvanceX = float.MaxValue;
|
||||
this.RasterizerMultiply = 1f;
|
||||
this.RasterizerGamma = 1.4f;
|
||||
this.RasterizerGamma = 1.7f;
|
||||
this.EllipsisChar = unchecked((char)-1);
|
||||
this.Raw.FontDataOwnedByAtlas = 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using Dalamud.Game;
|
|||
using Dalamud.Game.ClientState;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Internal.ManagedAsserts;
|
||||
|
|
@ -173,12 +174,12 @@ public sealed class UiBuilder : IDisposable
|
|||
/// <summary>
|
||||
/// Gets the default Dalamud font size in points.
|
||||
/// </summary>
|
||||
public static float DefaultFontSizePt => InterfaceManager.DefaultFontSizePt;
|
||||
public static float DefaultFontSizePt => Service<FontAtlasFactory>.Get().DefaultFontSpec.SizePt;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default Dalamud font size in pixels.
|
||||
/// </summary>
|
||||
public static float DefaultFontSizePx => InterfaceManager.DefaultFontSizePx;
|
||||
public static float DefaultFontSizePx => Service<FontAtlasFactory>.Get().DefaultFontSpec.SizePx;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default Dalamud font - supporting all game languages and icons.<br />
|
||||
|
|
@ -198,6 +199,11 @@ public sealed class UiBuilder : IDisposable
|
|||
/// </summary>
|
||||
public static ImFontPtr MonoFont => InterfaceManager.MonoFont;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default font specifications.
|
||||
/// </summary>
|
||||
public IFontSpec DefaultFontSpec => Service<FontAtlasFactory>.Get().DefaultFontSpec;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the handle to the default Dalamud font - supporting all game languages and icons.
|
||||
/// </summary>
|
||||
|
|
@ -206,7 +212,7 @@ public sealed class UiBuilder : IDisposable
|
|||
/// <code>
|
||||
/// fontAtlas.NewDelegateFontHandle(
|
||||
/// e => e.OnPreBuild(
|
||||
/// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePt)));
|
||||
/// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePx)));
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public IFontHandle DefaultFontHandle =>
|
||||
|
|
@ -225,6 +231,8 @@ public sealed class UiBuilder : IDisposable
|
|||
/// fontAtlas.NewDelegateFontHandle(
|
||||
/// e => e.OnPreBuild(
|
||||
/// tk => tk.AddFontAwesomeIconFont(new() { SizePt = UiBuilder.DefaultFontSizePt })));
|
||||
/// // or use
|
||||
/// tk => tk.AddFontAwesomeIconFont(new() { SizePx = UiBuilder.DefaultFontSizePx })));
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public IFontHandle IconFontHandle =>
|
||||
|
|
@ -245,6 +253,8 @@ public sealed class UiBuilder : IDisposable
|
|||
/// tk => tk.AddDalamudAssetFont(
|
||||
/// DalamudAsset.InconsolataRegular,
|
||||
/// new() { SizePt = UiBuilder.DefaultFontSizePt })));
|
||||
/// // or use
|
||||
/// new() { SizePx = UiBuilder.DefaultFontSizePx })));
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public IFontHandle MonoFontHandle =>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Linq;
|
|||
using System.Numerics;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Unicode;
|
||||
|
||||
using Dalamud.Configuration.Internal;
|
||||
|
|
@ -543,6 +544,24 @@ public static class ImGuiHelpers
|
|||
var pageIndex = unchecked((ushort)(codepoint / 4096));
|
||||
font.NativePtr->Used4kPagesMap[pageIndex >> 3] |= unchecked((byte)(1 << (pageIndex & 7)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the text for a text input, during the callback.
|
||||
/// </summary>
|
||||
/// <param name="data">The callback data.</param>
|
||||
/// <param name="s">The new text.</param>
|
||||
internal static unsafe void SetTextFromCallback(ImGuiInputTextCallbackData* data, string s)
|
||||
{
|
||||
if (data->BufTextLen != 0)
|
||||
ImGuiNative.ImGuiInputTextCallbackData_DeleteChars(data, 0, data->BufTextLen);
|
||||
|
||||
var len = Encoding.UTF8.GetByteCount(s);
|
||||
var buf = len < 1024 ? stackalloc byte[len] : new byte[len];
|
||||
Encoding.UTF8.GetBytes(s, buf);
|
||||
fixed (byte* pBuf = buf)
|
||||
ImGuiNative.ImGuiInputTextCallbackData_InsertChars(data, 0, pBuf, pBuf + len);
|
||||
ImGuiNative.ImGuiInputTextCallbackData_SelectAll(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the corresponding ImGui viewport ID for the given window handle.
|
||||
|
|
|
|||
|
|
@ -623,15 +623,38 @@ public abstract class Window
|
|||
/// </summary>
|
||||
public struct WindowSizeConstraints
|
||||
{
|
||||
private Vector2 internalMaxSize = new(float.MaxValue);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WindowSizeConstraints"/> struct.
|
||||
/// </summary>
|
||||
public WindowSizeConstraints()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum size of the window.
|
||||
/// </summary>
|
||||
public Vector2 MinimumSize { get; set; }
|
||||
|
||||
public Vector2 MinimumSize { get; set; } = new(0);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum size of the window.
|
||||
/// </summary>
|
||||
public Vector2 MaximumSize { get; set; }
|
||||
public Vector2 MaximumSize
|
||||
{
|
||||
get => this.GetSafeMaxSize();
|
||||
set => this.internalMaxSize = value;
|
||||
}
|
||||
|
||||
private Vector2 GetSafeMaxSize()
|
||||
{
|
||||
var currentMin = this.MinimumSize;
|
||||
|
||||
if (this.internalMaxSize.X < currentMin.X || this.internalMaxSize.Y < currentMin.Y)
|
||||
return new Vector2(float.MaxValue);
|
||||
|
||||
return this.internalMaxSize;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,21 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Memory.Exceptions;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Memory;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
|
||||
using static Dalamud.NativeFunctions;
|
||||
|
||||
using LPayloadType = Lumina.Text.Payloads.PayloadType;
|
||||
using LSeString = Lumina.Text.SeString;
|
||||
|
||||
// Heavily inspired from Reloaded (https://github.com/Reloaded-Project/Reloaded.Memory)
|
||||
|
||||
namespace Dalamud.Memory;
|
||||
|
|
@ -19,6 +25,47 @@ namespace Dalamud.Memory;
|
|||
/// </summary>
|
||||
public static unsafe class MemoryHelper
|
||||
{
|
||||
private static readonly ObjectPool<StringBuilder> StringBuilderPool =
|
||||
ObjectPool.Create(new StringBuilderPooledObjectPolicy());
|
||||
|
||||
#region Cast
|
||||
|
||||
/// <summary>Casts the given memory address as the reference to the live object.</summary>
|
||||
/// <param name="memoryAddress">The memory address.</param>
|
||||
/// <typeparam name="T">The unmanaged type.</typeparam>
|
||||
/// <returns>The reference to the live object.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ref T Cast<T>(nint memoryAddress) where T : unmanaged => ref *(T*)memoryAddress;
|
||||
|
||||
/// <summary>Casts the given memory address as the span of the live object(s).</summary>
|
||||
/// <param name="memoryAddress">The memory address.</param>
|
||||
/// <param name="length">The number of items.</param>
|
||||
/// <typeparam name="T">The unmanaged type.</typeparam>
|
||||
/// <returns>The span containing reference to the live object(s).</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Span<T> Cast<T>(nint memoryAddress, int length) where T : unmanaged =>
|
||||
new((void*)memoryAddress, length);
|
||||
|
||||
/// <summary>Casts the given memory address as the span of the live object(s), until it encounters a zero.</summary>
|
||||
/// <param name="memoryAddress">The memory address.</param>
|
||||
/// <param name="maxLength">The maximum number of items.</param>
|
||||
/// <typeparam name="T">The unmanaged type.</typeparam>
|
||||
/// <returns>The span containing reference to the live object(s).</returns>
|
||||
/// <remarks>If <typeparamref name="T"/> is <c>byte</c> or <c>char</c> and <paramref name="maxLength"/> is not
|
||||
/// specified, consider using <see cref="MemoryMarshal.CreateReadOnlySpanFromNullTerminated(byte*)"/> or
|
||||
/// <see cref="MemoryMarshal.CreateReadOnlySpanFromNullTerminated(char*)"/>.</remarks>
|
||||
public static Span<T> CastNullTerminated<T>(nint memoryAddress, int maxLength = int.MaxValue)
|
||||
where T : unmanaged, IEquatable<T>
|
||||
{
|
||||
var typedPointer = (T*)memoryAddress;
|
||||
var length = 0;
|
||||
while (length < maxLength && !default(T).Equals(*typedPointer++))
|
||||
length++;
|
||||
return new((void*)memoryAddress, length);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Read
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -27,7 +74,9 @@ public static unsafe class MemoryHelper
|
|||
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <returns>The read in struct.</returns>
|
||||
public static T Read<T>(IntPtr memoryAddress) where T : unmanaged
|
||||
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint)"/> instead.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T Read<T>(nint memoryAddress) where T : unmanaged
|
||||
=> Read<T>(memoryAddress, false);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -37,12 +86,13 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
|
||||
/// <returns>The read in struct.</returns>
|
||||
public static T Read<T>(IntPtr memoryAddress, bool marshal)
|
||||
{
|
||||
return marshal
|
||||
? Marshal.PtrToStructure<T>(memoryAddress)
|
||||
: Unsafe.Read<T>((void*)memoryAddress);
|
||||
}
|
||||
/// <remarks>If you do not need to make a copy and <paramref name="marshal"/> is <c>false</c>,
|
||||
/// use <see cref="Cast{T}(nint)"/> instead.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T Read<T>(nint memoryAddress, bool marshal) =>
|
||||
marshal
|
||||
? Marshal.PtrToStructure<T>(memoryAddress)
|
||||
: Unsafe.Read<T>((void*)memoryAddress);
|
||||
|
||||
/// <summary>
|
||||
/// Reads a byte array from a specified memory address.
|
||||
|
|
@ -50,12 +100,9 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="length">The amount of bytes to read starting from the memoryAddress.</param>
|
||||
/// <returns>The read in byte array.</returns>
|
||||
public static byte[] ReadRaw(IntPtr memoryAddress, int length)
|
||||
{
|
||||
var value = new byte[length];
|
||||
Marshal.Copy(memoryAddress, value, 0, value.Length);
|
||||
return value;
|
||||
}
|
||||
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte[] ReadRaw(nint memoryAddress, int length) => Cast<byte>(memoryAddress, length).ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Reads a generic type array from a specified memory address.
|
||||
|
|
@ -64,8 +111,10 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="arrayLength">The amount of array items to read.</param>
|
||||
/// <returns>The read in struct array.</returns>
|
||||
public static T[] Read<T>(IntPtr memoryAddress, int arrayLength) where T : unmanaged
|
||||
=> Read<T>(memoryAddress, arrayLength, false);
|
||||
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T[] Read<T>(nint memoryAddress, int arrayLength) where T : unmanaged
|
||||
=> Cast<T>(memoryAddress, arrayLength).ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Reads a generic type array from a specified memory address.
|
||||
|
|
@ -75,16 +124,18 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="arrayLength">The amount of array items to read.</param>
|
||||
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
|
||||
/// <returns>The read in struct array.</returns>
|
||||
public static T[] Read<T>(IntPtr memoryAddress, int arrayLength, bool marshal)
|
||||
/// <remarks>If you do not need to make a copy and <paramref name="marshal"/> is <c>false</c>,
|
||||
/// use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
|
||||
public static T[] Read<T>(nint memoryAddress, int arrayLength, bool marshal)
|
||||
{
|
||||
var structSize = SizeOf<T>(marshal);
|
||||
var value = new T[arrayLength];
|
||||
|
||||
for (var i = 0; i < arrayLength; i++)
|
||||
{
|
||||
var address = memoryAddress + (structSize * i);
|
||||
Read(address, out T result, marshal);
|
||||
Read(memoryAddress, out T result, marshal);
|
||||
value[i] = result;
|
||||
memoryAddress += structSize;
|
||||
}
|
||||
|
||||
return value;
|
||||
|
|
@ -95,16 +146,10 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <returns>The read in byte array.</returns>
|
||||
public static unsafe byte[] ReadRawNullTerminated(IntPtr memoryAddress)
|
||||
{
|
||||
var byteCount = 0;
|
||||
while (*(byte*)(memoryAddress + byteCount) != 0x00)
|
||||
{
|
||||
byteCount++;
|
||||
}
|
||||
|
||||
return ReadRaw(memoryAddress, byteCount);
|
||||
}
|
||||
/// <remarks>If you do not need to make a copy, use <see cref="CastNullTerminated{T}(nint,int)"/> instead.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte[] ReadRawNullTerminated(nint memoryAddress) =>
|
||||
MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)memoryAddress).ToArray();
|
||||
|
||||
#endregion
|
||||
|
||||
|
|
@ -116,7 +161,9 @@ public static unsafe class MemoryHelper
|
|||
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="value">Local variable to receive the read in struct.</param>
|
||||
public static void Read<T>(IntPtr memoryAddress, out T value) where T : unmanaged
|
||||
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint)"/> instead.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Read<T>(nint memoryAddress, out T value) where T : unmanaged
|
||||
=> value = Read<T>(memoryAddress);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -126,7 +173,10 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="value">Local variable to receive the read in struct.</param>
|
||||
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
|
||||
public static void Read<T>(IntPtr memoryAddress, out T value, bool marshal)
|
||||
/// <remarks>If you do not need to make a copy and <paramref name="marshal"/> is <c>false</c>,
|
||||
/// use <see cref="Cast{T}(nint)"/> instead.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Read<T>(nint memoryAddress, out T value, bool marshal)
|
||||
=> value = Read<T>(memoryAddress, marshal);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -135,7 +185,9 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="length">The amount of bytes to read starting from the memoryAddress.</param>
|
||||
/// <param name="value">Local variable to receive the read in bytes.</param>
|
||||
public static void ReadRaw(IntPtr memoryAddress, int length, out byte[] value)
|
||||
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ReadRaw(nint memoryAddress, int length, out byte[] value)
|
||||
=> value = ReadRaw(memoryAddress, length);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -145,7 +197,9 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="arrayLength">The amount of array items to read.</param>
|
||||
/// <param name="value">The read in struct array.</param>
|
||||
public static void Read<T>(IntPtr memoryAddress, int arrayLength, out T[] value) where T : unmanaged
|
||||
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Read<T>(nint memoryAddress, int arrayLength, out T[] value) where T : unmanaged
|
||||
=> value = Read<T>(memoryAddress, arrayLength);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -156,7 +210,10 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="arrayLength">The amount of array items to read.</param>
|
||||
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
|
||||
/// <param name="value">The read in struct array.</param>
|
||||
public static void Read<T>(IntPtr memoryAddress, int arrayLength, bool marshal, out T[] value)
|
||||
/// <remarks>If you do not need to make a copy and <paramref name="marshal"/> is <c>false</c>,
|
||||
/// use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Read<T>(nint memoryAddress, int arrayLength, bool marshal, out T[] value)
|
||||
=> value = Read<T>(memoryAddress, arrayLength, marshal);
|
||||
|
||||
#endregion
|
||||
|
|
@ -184,15 +241,27 @@ public static unsafe class MemoryHelper
|
|||
var length = 0;
|
||||
while (length < maxLength && pmem[length] != 0)
|
||||
length++;
|
||||
|
||||
|
||||
var mem = new Span<byte>(pmem, length);
|
||||
var memCharCount = encoding.GetCharCount(mem);
|
||||
if (memCharCount != charSpan.Length)
|
||||
return false;
|
||||
|
||||
Span<char> chars = stackalloc char[memCharCount];
|
||||
encoding.GetChars(mem, chars);
|
||||
return charSpan.SequenceEqual(chars);
|
||||
if (memCharCount < 1024)
|
||||
{
|
||||
Span<char> chars = stackalloc char[memCharCount];
|
||||
encoding.GetChars(mem, chars);
|
||||
return charSpan.SequenceEqual(chars);
|
||||
}
|
||||
else
|
||||
{
|
||||
var rented = ArrayPool<char>.Shared.Rent(memCharCount);
|
||||
var chars = rented.AsSpan(0, memCharCount);
|
||||
encoding.GetChars(mem, chars);
|
||||
var equals = charSpan.SequenceEqual(chars);
|
||||
ArrayPool<char>.Shared.Return(rented);
|
||||
return equals;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -203,8 +272,9 @@ public static unsafe class MemoryHelper
|
|||
/// </remarks>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <returns>The read in string.</returns>
|
||||
public static string ReadStringNullTerminated(IntPtr memoryAddress)
|
||||
=> ReadStringNullTerminated(memoryAddress, Encoding.UTF8);
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static string ReadStringNullTerminated(nint memoryAddress)
|
||||
=> Encoding.UTF8.GetString(MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)memoryAddress));
|
||||
|
||||
/// <summary>
|
||||
/// Read a string with the given encoding from a specified memory address.
|
||||
|
|
@ -215,10 +285,25 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="encoding">The encoding to use to decode the string.</param>
|
||||
/// <returns>The read in string.</returns>
|
||||
public static string ReadStringNullTerminated(IntPtr memoryAddress, Encoding encoding)
|
||||
public static string ReadStringNullTerminated(nint memoryAddress, Encoding encoding)
|
||||
{
|
||||
var buffer = ReadRawNullTerminated(memoryAddress);
|
||||
return encoding.GetString(buffer);
|
||||
switch (encoding)
|
||||
{
|
||||
case UTF8Encoding:
|
||||
case var _ when encoding.IsSingleByte:
|
||||
return encoding.GetString(MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)memoryAddress));
|
||||
case UnicodeEncoding:
|
||||
// Note that it may be in little or big endian, so using `new string(...)` is not always correct.
|
||||
return encoding.GetString(
|
||||
MemoryMarshal.Cast<char, byte>(
|
||||
MemoryMarshal.CreateReadOnlySpanFromNullTerminated((char*)memoryAddress)));
|
||||
case UTF32Encoding:
|
||||
return encoding.GetString(MemoryMarshal.Cast<int, byte>(CastNullTerminated<int>(memoryAddress)));
|
||||
default:
|
||||
// For correctness' sake; if there does not exist an encoding which will contain a (byte)0 for a
|
||||
// non-null character, then this branch can be merged with UTF8Encoding one.
|
||||
return encoding.GetString(ReadRawNullTerminated(memoryAddress));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -228,10 +313,12 @@ public static unsafe class MemoryHelper
|
|||
/// Attention! If this is an <see cref="SeString"/>, use the applicable helper methods to decode.
|
||||
/// </remarks>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="maxLength">The maximum length of the string.</param>
|
||||
/// <param name="maxLength">The maximum number of bytes to read.
|
||||
/// Note that this is NOT the maximum length of the returned string.</param>
|
||||
/// <returns>The read in string.</returns>
|
||||
public static string ReadString(IntPtr memoryAddress, int maxLength)
|
||||
=> ReadString(memoryAddress, Encoding.UTF8, maxLength);
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static string ReadString(nint memoryAddress, int maxLength)
|
||||
=> Encoding.UTF8.GetString(CastNullTerminated<byte>(memoryAddress, maxLength));
|
||||
|
||||
/// <summary>
|
||||
/// Read a string with the given encoding from a specified memory address.
|
||||
|
|
@ -241,18 +328,32 @@ public static unsafe class MemoryHelper
|
|||
/// </remarks>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="encoding">The encoding to use to decode the string.</param>
|
||||
/// <param name="maxLength">The maximum length of the string.</param>
|
||||
/// <param name="maxLength">The maximum number of bytes to read.
|
||||
/// Note that this is NOT the maximum length of the returned string.</param>
|
||||
/// <returns>The read in string.</returns>
|
||||
public static string ReadString(IntPtr memoryAddress, Encoding encoding, int maxLength)
|
||||
public static string ReadString(nint memoryAddress, Encoding encoding, int maxLength)
|
||||
{
|
||||
if (maxLength <= 0)
|
||||
return string.Empty;
|
||||
|
||||
ReadRaw(memoryAddress, maxLength, out var buffer);
|
||||
|
||||
var data = encoding.GetString(buffer);
|
||||
var eosPos = data.IndexOf('\0');
|
||||
return eosPos >= 0 ? data.Substring(0, eosPos) : data;
|
||||
switch (encoding)
|
||||
{
|
||||
case UTF8Encoding:
|
||||
case var _ when encoding.IsSingleByte:
|
||||
return encoding.GetString(CastNullTerminated<byte>(memoryAddress, maxLength));
|
||||
case UnicodeEncoding:
|
||||
return encoding.GetString(
|
||||
MemoryMarshal.Cast<char, byte>(CastNullTerminated<char>(memoryAddress, maxLength / 2)));
|
||||
case UTF32Encoding:
|
||||
return encoding.GetString(
|
||||
MemoryMarshal.Cast<int, byte>(CastNullTerminated<int>(memoryAddress, maxLength / 4)));
|
||||
default:
|
||||
// For correctness' sake; if there does not exist an encoding which will contain a (byte)0 for a
|
||||
// non-null character, then this branch can be merged with UTF8Encoding one.
|
||||
var data = encoding.GetString(Cast<byte>(memoryAddress, maxLength));
|
||||
var eosPos = data.IndexOf('\0');
|
||||
return eosPos >= 0 ? data[..eosPos] : data;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -260,11 +361,9 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <returns>The read in string.</returns>
|
||||
public static SeString ReadSeStringNullTerminated(IntPtr memoryAddress)
|
||||
{
|
||||
var buffer = ReadRawNullTerminated(memoryAddress);
|
||||
return SeString.Parse(buffer);
|
||||
}
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SeString ReadSeStringNullTerminated(nint memoryAddress) =>
|
||||
SeString.Parse(MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)memoryAddress));
|
||||
|
||||
/// <summary>
|
||||
/// Read an SeString from a specified memory address.
|
||||
|
|
@ -272,40 +371,165 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="maxLength">The maximum length of the string.</param>
|
||||
/// <returns>The read in string.</returns>
|
||||
public static SeString ReadSeString(IntPtr memoryAddress, int maxLength)
|
||||
{
|
||||
ReadRaw(memoryAddress, maxLength, out var buffer);
|
||||
|
||||
var eos = Array.IndexOf(buffer, (byte)0);
|
||||
if (eos < 0)
|
||||
{
|
||||
return SeString.Parse(buffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
var newBuffer = new byte[eos];
|
||||
Buffer.BlockCopy(buffer, 0, newBuffer, 0, eos);
|
||||
return SeString.Parse(newBuffer);
|
||||
}
|
||||
}
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SeString ReadSeString(nint memoryAddress, int maxLength) =>
|
||||
// Note that a valid SeString never contains a null character, other than for the sequence terminator purpose.
|
||||
SeString.Parse(CastNullTerminated<byte>(memoryAddress, maxLength));
|
||||
|
||||
/// <summary>
|
||||
/// Read an SeString from a specified Utf8String structure.
|
||||
/// </summary>
|
||||
/// <param name="utf8String">The memory address to read from.</param>
|
||||
/// <returns>The read in string.</returns>
|
||||
public static unsafe SeString ReadSeString(Utf8String* utf8String)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SeString ReadSeString(Utf8String* utf8String) =>
|
||||
utf8String == null ? string.Empty : SeString.Parse(utf8String->AsSpan());
|
||||
|
||||
/// <summary>
|
||||
/// Reads an SeString from a specified memory address, and extracts the outermost string.<br />
|
||||
/// If the SeString is malformed, behavior is undefined.
|
||||
/// </summary>
|
||||
/// <param name="containsNonRepresentedPayload">Whether the SeString contained a non-represented payload.</param>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="maxLength">The maximum length of the string.</param>
|
||||
/// <param name="stopOnFirstNonRepresentedPayload">Stop reading on encountering the first non-represented payload.
|
||||
/// What payloads are represented via this function may change.</param>
|
||||
/// <param name="nonRepresentedPayloadReplacement">Replacement for non-represented payloads.</param>
|
||||
/// <returns>The read in string.</returns>
|
||||
public static string ReadSeStringAsString(
|
||||
out bool containsNonRepresentedPayload,
|
||||
nint memoryAddress,
|
||||
int maxLength = int.MaxValue,
|
||||
bool stopOnFirstNonRepresentedPayload = false,
|
||||
string nonRepresentedPayloadReplacement = "*")
|
||||
{
|
||||
if (utf8String == null)
|
||||
return string.Empty;
|
||||
var sb = StringBuilderPool.Get();
|
||||
sb.EnsureCapacity(maxLength = CastNullTerminated<byte>(memoryAddress, maxLength).Length);
|
||||
|
||||
var ptr = utf8String->StringPtr;
|
||||
if (ptr == null)
|
||||
return string.Empty;
|
||||
// 1 utf-8 codepoint can spill up to 2 characters.
|
||||
Span<char> tmp = stackalloc char[2];
|
||||
|
||||
var len = Math.Max(utf8String->BufUsed, utf8String->StringLength);
|
||||
var pin = (byte*)memoryAddress;
|
||||
containsNonRepresentedPayload = false;
|
||||
while (*pin != 0 && maxLength > 0)
|
||||
{
|
||||
if (*pin != LSeString.StartByte)
|
||||
{
|
||||
var len = *pin switch
|
||||
{
|
||||
< 0x80 => 1,
|
||||
>= 0b11000000 and <= 0b11011111 => 2,
|
||||
>= 0b11100000 and <= 0b11101111 => 3,
|
||||
>= 0b11110000 and <= 0b11110111 => 4,
|
||||
_ => 0,
|
||||
};
|
||||
if (len == 0 || len > maxLength)
|
||||
break;
|
||||
|
||||
return ReadSeString((IntPtr)ptr, (int)len);
|
||||
var numChars = Encoding.UTF8.GetChars(new(pin, len), tmp);
|
||||
sb.Append(tmp[..numChars]);
|
||||
pin += len;
|
||||
maxLength -= len;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Start byte
|
||||
++pin;
|
||||
--maxLength;
|
||||
|
||||
// Payload type
|
||||
var payloadType = (LPayloadType)(*pin++);
|
||||
|
||||
// Payload length
|
||||
if (!ReadIntExpression(ref pin, ref maxLength, out var expressionLength))
|
||||
break;
|
||||
if (expressionLength > maxLength)
|
||||
break;
|
||||
pin += expressionLength;
|
||||
maxLength -= unchecked((int)expressionLength);
|
||||
|
||||
// End byte
|
||||
if (*pin++ != LSeString.EndByte)
|
||||
break;
|
||||
--maxLength;
|
||||
|
||||
switch (payloadType)
|
||||
{
|
||||
case LPayloadType.NewLine:
|
||||
sb.AppendLine();
|
||||
break;
|
||||
case LPayloadType.Hyphen:
|
||||
sb.Append('–');
|
||||
break;
|
||||
case LPayloadType.SoftHyphen:
|
||||
sb.Append('\u00AD');
|
||||
break;
|
||||
default:
|
||||
sb.Append(nonRepresentedPayloadReplacement);
|
||||
containsNonRepresentedPayload = true;
|
||||
if (stopOnFirstNonRepresentedPayload)
|
||||
maxLength = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var res = sb.ToString();
|
||||
StringBuilderPool.Return(sb);
|
||||
return res;
|
||||
|
||||
static bool ReadIntExpression(ref byte* p, ref int maxLength, out uint value)
|
||||
{
|
||||
if (maxLength <= 0)
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
var typeByte = *p++;
|
||||
--maxLength;
|
||||
|
||||
switch (typeByte)
|
||||
{
|
||||
case > 0 and < 0xD0:
|
||||
value = (uint)typeByte - 1;
|
||||
return true;
|
||||
case >= 0xF0 and <= 0xFE:
|
||||
++typeByte;
|
||||
value = 0u;
|
||||
if ((typeByte & 8) != 0)
|
||||
{
|
||||
if (maxLength <= 0 || *p == 0)
|
||||
return false;
|
||||
value |= (uint)*p++ << 24;
|
||||
}
|
||||
|
||||
if ((typeByte & 4) != 0)
|
||||
{
|
||||
if (maxLength <= 0 || *p == 0)
|
||||
return false;
|
||||
value |= (uint)*p++ << 16;
|
||||
}
|
||||
|
||||
if ((typeByte & 2) != 0)
|
||||
{
|
||||
if (maxLength <= 0 || *p == 0)
|
||||
return false;
|
||||
value |= (uint)*p++ << 8;
|
||||
}
|
||||
|
||||
if ((typeByte & 1) != 0)
|
||||
{
|
||||
if (maxLength <= 0 || *p == 0)
|
||||
return false;
|
||||
value |= *p++;
|
||||
}
|
||||
|
||||
return true;
|
||||
default:
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -320,7 +544,8 @@ public static unsafe class MemoryHelper
|
|||
/// </remarks>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="value">The read in string.</param>
|
||||
public static void ReadStringNullTerminated(IntPtr memoryAddress, out string value)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ReadStringNullTerminated(nint memoryAddress, out string value)
|
||||
=> value = ReadStringNullTerminated(memoryAddress);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -332,7 +557,8 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="encoding">The encoding to use to decode the string.</param>
|
||||
/// <param name="value">The read in string.</param>
|
||||
public static void ReadStringNullTerminated(IntPtr memoryAddress, Encoding encoding, out string value)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ReadStringNullTerminated(nint memoryAddress, Encoding encoding, out string value)
|
||||
=> value = ReadStringNullTerminated(memoryAddress, encoding);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -344,7 +570,8 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="value">The read in string.</param>
|
||||
/// <param name="maxLength">The maximum length of the string.</param>
|
||||
public static void ReadString(IntPtr memoryAddress, out string value, int maxLength)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ReadString(nint memoryAddress, out string value, int maxLength)
|
||||
=> value = ReadString(memoryAddress, maxLength);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -357,7 +584,8 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="encoding">The encoding to use to decode the string.</param>
|
||||
/// <param name="maxLength">The maximum length of the string.</param>
|
||||
/// <param name="value">The read in string.</param>
|
||||
public static void ReadString(IntPtr memoryAddress, Encoding encoding, int maxLength, out string value)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ReadString(nint memoryAddress, Encoding encoding, int maxLength, out string value)
|
||||
=> value = ReadString(memoryAddress, encoding, maxLength);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -365,7 +593,8 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="value">The read in SeString.</param>
|
||||
public static void ReadSeStringNullTerminated(IntPtr memoryAddress, out SeString value)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ReadSeStringNullTerminated(nint memoryAddress, out SeString value)
|
||||
=> value = ReadSeStringNullTerminated(memoryAddress);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -374,7 +603,8 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="maxLength">The maximum length of the string.</param>
|
||||
/// <param name="value">The read in SeString.</param>
|
||||
public static void ReadSeString(IntPtr memoryAddress, int maxLength, out SeString value)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ReadSeString(nint memoryAddress, int maxLength, out SeString value)
|
||||
=> value = ReadSeString(memoryAddress, maxLength);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -382,6 +612,7 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <param name="utf8String">The memory address to read from.</param>
|
||||
/// <param name="value">The read in string.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static unsafe void ReadSeString(Utf8String* utf8String, out SeString value)
|
||||
=> value = ReadSeString(utf8String);
|
||||
|
||||
|
|
@ -395,7 +626,8 @@ public static unsafe class MemoryHelper
|
|||
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="item">The item to write to the address.</param>
|
||||
public static void Write<T>(IntPtr memoryAddress, T item) where T : unmanaged
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Write<T>(nint memoryAddress, T item) where T : unmanaged
|
||||
=> Write(memoryAddress, item, false);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -405,7 +637,7 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="item">The item to write to the address.</param>
|
||||
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
|
||||
public static void Write<T>(IntPtr memoryAddress, T item, bool marshal)
|
||||
public static void Write<T>(nint memoryAddress, T item, bool marshal)
|
||||
{
|
||||
if (marshal)
|
||||
Marshal.StructureToPtr(item, memoryAddress, false);
|
||||
|
|
@ -418,10 +650,8 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="data">The bytes to write to memoryAddress.</param>
|
||||
public static void WriteRaw(IntPtr memoryAddress, byte[] data)
|
||||
{
|
||||
Marshal.Copy(data, 0, memoryAddress, data.Length);
|
||||
}
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void WriteRaw(nint memoryAddress, byte[] data) => Marshal.Copy(data, 0, memoryAddress, data.Length);
|
||||
|
||||
/// <summary>
|
||||
/// Writes a generic type array to a specified memory address.
|
||||
|
|
@ -429,7 +659,8 @@ public static unsafe class MemoryHelper
|
|||
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
|
||||
/// <param name="memoryAddress">The memory address to write to.</param>
|
||||
/// <param name="items">The array of items to write to the address.</param>
|
||||
public static void Write<T>(IntPtr memoryAddress, T[] items) where T : unmanaged
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Write<T>(nint memoryAddress, T[] items) where T : unmanaged
|
||||
=> Write(memoryAddress, items, false);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -439,7 +670,8 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to write to.</param>
|
||||
/// <param name="items">The array of items to write to the address.</param>
|
||||
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
|
||||
public static void Write<T>(IntPtr memoryAddress, T[] items, bool marshal)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Write<T>(nint memoryAddress, T[] items, bool marshal)
|
||||
{
|
||||
var structSize = SizeOf<T>(marshal);
|
||||
|
||||
|
|
@ -462,7 +694,8 @@ public static unsafe class MemoryHelper
|
|||
/// </remarks>
|
||||
/// <param name="memoryAddress">The memory address to write to.</param>
|
||||
/// <param name="value">The string to write.</param>
|
||||
public static void WriteString(IntPtr memoryAddress, string value)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void WriteString(nint memoryAddress, string? value)
|
||||
=> WriteString(memoryAddress, value, Encoding.UTF8);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -474,14 +707,12 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to write to.</param>
|
||||
/// <param name="value">The string to write.</param>
|
||||
/// <param name="encoding">The encoding to use.</param>
|
||||
public static void WriteString(IntPtr memoryAddress, string value, Encoding encoding)
|
||||
public static void WriteString(nint memoryAddress, string? value, Encoding encoding)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return;
|
||||
|
||||
var bytes = encoding.GetBytes(value + '\0');
|
||||
|
||||
WriteRaw(memoryAddress, bytes);
|
||||
var ptr = 0;
|
||||
if (value is not null)
|
||||
ptr = encoding.GetBytes(value, Cast<byte>(memoryAddress, encoding.GetMaxByteCount(value.Length)));
|
||||
encoding.GetBytes("\0", Cast<byte>(memoryAddress + ptr, 4));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -489,7 +720,8 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <param name="memoryAddress">The memory address to write to.</param>
|
||||
/// <param name="value">The SeString to write.</param>
|
||||
public static void WriteSeString(IntPtr memoryAddress, SeString value)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void WriteSeString(nint memoryAddress, SeString? value)
|
||||
{
|
||||
if (value is null)
|
||||
return;
|
||||
|
|
@ -507,15 +739,16 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <param name="length">Amount of bytes to be allocated.</param>
|
||||
/// <returns>Address to the newly allocated memory.</returns>
|
||||
public static IntPtr Allocate(int length)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static nint Allocate(int length)
|
||||
{
|
||||
var address = VirtualAlloc(
|
||||
IntPtr.Zero,
|
||||
(UIntPtr)length,
|
||||
nint.Zero,
|
||||
(nuint)length,
|
||||
AllocationType.Commit | AllocationType.Reserve,
|
||||
MemoryProtection.ExecuteReadWrite);
|
||||
|
||||
if (address == IntPtr.Zero)
|
||||
if (address == nint.Zero)
|
||||
throw new MemoryAllocationException($"Unable to allocate {length} bytes.");
|
||||
|
||||
return address;
|
||||
|
|
@ -527,7 +760,8 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <param name="length">Amount of bytes to be allocated.</param>
|
||||
/// <param name="memoryAddress">Address to the newly allocated memory.</param>
|
||||
public static void Allocate(int length, out IntPtr memoryAddress)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Allocate(int length, out nint memoryAddress)
|
||||
=> memoryAddress = Allocate(length);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -535,9 +769,10 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <param name="memoryAddress">The address of the memory to free.</param>
|
||||
/// <returns>True if the operation is successful.</returns>
|
||||
public static bool Free(IntPtr memoryAddress)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool Free(nint memoryAddress)
|
||||
{
|
||||
return VirtualFree(memoryAddress, UIntPtr.Zero, AllocationType.Release);
|
||||
return VirtualFree(memoryAddress, nuint.Zero, AllocationType.Release);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -547,9 +782,9 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="length">The region size for which to change permissions for.</param>
|
||||
/// <param name="newPermissions">The new permissions to set.</param>
|
||||
/// <returns>The old page permissions.</returns>
|
||||
public static MemoryProtection ChangePermission(IntPtr memoryAddress, int length, MemoryProtection newPermissions)
|
||||
public static MemoryProtection ChangePermission(nint memoryAddress, int length, MemoryProtection newPermissions)
|
||||
{
|
||||
var result = VirtualProtect(memoryAddress, (UIntPtr)length, newPermissions, out var oldPermissions);
|
||||
var result = VirtualProtect(memoryAddress, (nuint)length, newPermissions, out var oldPermissions);
|
||||
|
||||
if (!result)
|
||||
throw new MemoryPermissionException($"Unable to change permissions at 0x{memoryAddress.ToInt64():X} of length {length} and permission {newPermissions} (result={result})");
|
||||
|
|
@ -568,7 +803,9 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="length">The region size for which to change permissions for.</param>
|
||||
/// <param name="newPermissions">The new permissions to set.</param>
|
||||
/// <param name="oldPermissions">The old page permissions.</param>
|
||||
public static void ChangePermission(IntPtr memoryAddress, int length, MemoryProtection newPermissions, out MemoryProtection oldPermissions)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ChangePermission(
|
||||
nint memoryAddress, int length, MemoryProtection newPermissions, out MemoryProtection oldPermissions)
|
||||
=> oldPermissions = ChangePermission(memoryAddress, length, newPermissions);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -580,7 +817,9 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="newPermissions">The new permissions to set.</param>
|
||||
/// <param name="marshal">Set to true to calculate the size of the struct after marshalling instead of before.</param>
|
||||
/// <returns>The old page permissions.</returns>
|
||||
public static MemoryProtection ChangePermission<T>(IntPtr memoryAddress, ref T baseElement, MemoryProtection newPermissions, bool marshal)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static MemoryProtection ChangePermission<T>(
|
||||
nint memoryAddress, ref T baseElement, MemoryProtection newPermissions, bool marshal)
|
||||
=> ChangePermission(memoryAddress, SizeOf<T>(marshal), newPermissions);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -590,7 +829,8 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="length">The amount of bytes to read starting from the memoryAddress.</param>
|
||||
/// <returns>The read in bytes.</returns>
|
||||
public static byte[] ReadProcessMemory(IntPtr memoryAddress, int length)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte[] ReadProcessMemory(nint memoryAddress, int length)
|
||||
{
|
||||
var value = new byte[length];
|
||||
ReadProcessMemory(memoryAddress, ref value);
|
||||
|
|
@ -604,7 +844,8 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="length">The amount of bytes to read starting from the memoryAddress.</param>
|
||||
/// <param name="value">The read in bytes.</param>
|
||||
public static void ReadProcessMemory(IntPtr memoryAddress, int length, out byte[] value)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ReadProcessMemory(nint memoryAddress, int length, out byte[] value)
|
||||
=> value = ReadProcessMemory(memoryAddress, length);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -613,12 +854,12 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="value">The read in bytes.</param>
|
||||
public static void ReadProcessMemory(IntPtr memoryAddress, ref byte[] value)
|
||||
public static void ReadProcessMemory(nint memoryAddress, ref byte[] value)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var length = value.Length;
|
||||
var result = NativeFunctions.ReadProcessMemory((IntPtr)0xFFFFFFFF, memoryAddress, value, length, out _);
|
||||
var result = NativeFunctions.ReadProcessMemory((nint)0xFFFFFFFF, memoryAddress, value, length, out _);
|
||||
|
||||
if (!result)
|
||||
throw new MemoryReadException($"Unable to read memory at 0x{memoryAddress.ToInt64():X} of length {length} (result={result})");
|
||||
|
|
@ -635,12 +876,12 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <param name="memoryAddress">The memory address to write to.</param>
|
||||
/// <param name="data">The bytes to write to memoryAddress.</param>
|
||||
public static void WriteProcessMemory(IntPtr memoryAddress, byte[] data)
|
||||
public static void WriteProcessMemory(nint memoryAddress, byte[] data)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var length = data.Length;
|
||||
var result = NativeFunctions.WriteProcessMemory((IntPtr)0xFFFFFFFF, memoryAddress, data, length, out _);
|
||||
var result = NativeFunctions.WriteProcessMemory((nint)0xFFFFFFFF, memoryAddress, data, length, out _);
|
||||
|
||||
if (!result)
|
||||
throw new MemoryWriteException($"Unable to write memory at 0x{memoryAddress.ToInt64():X} of length {length} (result={result})");
|
||||
|
|
@ -660,6 +901,7 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
|
||||
/// <returns>The size of the primitive or struct.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int SizeOf<T>()
|
||||
=> SizeOf<T>(false);
|
||||
|
||||
|
|
@ -669,6 +911,7 @@ public static unsafe class MemoryHelper
|
|||
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
|
||||
/// <param name="marshal">If set to true; will return the size of an element after marshalling.</param>
|
||||
/// <returns>The size of the primitive or struct.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int SizeOf<T>(bool marshal)
|
||||
=> marshal ? Marshal.SizeOf<T>() : Unsafe.SizeOf<T>();
|
||||
|
||||
|
|
@ -678,6 +921,7 @@ public static unsafe class MemoryHelper
|
|||
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
|
||||
/// <param name="elementCount">The number of array elements present.</param>
|
||||
/// <returns>The size of the primitive or struct array.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int SizeOf<T>(int elementCount) where T : unmanaged
|
||||
=> SizeOf<T>() * elementCount;
|
||||
|
||||
|
|
@ -688,6 +932,7 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="elementCount">The number of array elements present.</param>
|
||||
/// <param name="marshal">If set to true; will return the size of an element after marshalling.</param>
|
||||
/// <returns>The size of the primitive or struct array.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int SizeOf<T>(int elementCount, bool marshal)
|
||||
=> SizeOf<T>(marshal) * elementCount;
|
||||
|
||||
|
|
@ -701,9 +946,10 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="size">Amount of bytes to allocate.</param>
|
||||
/// <param name="alignment">The alignment of the allocation.</param>
|
||||
/// <returns>Pointer to the allocated region.</returns>
|
||||
public static IntPtr GameAllocateUi(ulong size, ulong alignment = 0)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static nint GameAllocateUi(ulong size, ulong alignment = 0)
|
||||
{
|
||||
return new IntPtr(IMemorySpace.GetUISpace()->Malloc(size, alignment));
|
||||
return new nint(IMemorySpace.GetUISpace()->Malloc(size, alignment));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -712,9 +958,10 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="size">Amount of bytes to allocate.</param>
|
||||
/// <param name="alignment">The alignment of the allocation.</param>
|
||||
/// <returns>Pointer to the allocated region.</returns>
|
||||
public static IntPtr GameAllocateDefault(ulong size, ulong alignment = 0)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static nint GameAllocateDefault(ulong size, ulong alignment = 0)
|
||||
{
|
||||
return new IntPtr(IMemorySpace.GetDefaultSpace()->Malloc(size, alignment));
|
||||
return new nint(IMemorySpace.GetDefaultSpace()->Malloc(size, alignment));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -723,9 +970,10 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="size">Amount of bytes to allocate.</param>
|
||||
/// <param name="alignment">The alignment of the allocation.</param>
|
||||
/// <returns>Pointer to the allocated region.</returns>
|
||||
public static IntPtr GameAllocateAnimation(ulong size, ulong alignment = 0)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static nint GameAllocateAnimation(ulong size, ulong alignment = 0)
|
||||
{
|
||||
return new IntPtr(IMemorySpace.GetAnimationSpace()->Malloc(size, alignment));
|
||||
return new nint(IMemorySpace.GetAnimationSpace()->Malloc(size, alignment));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -734,9 +982,10 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="size">Amount of bytes to allocate.</param>
|
||||
/// <param name="alignment">The alignment of the allocation.</param>
|
||||
/// <returns>Pointer to the allocated region.</returns>
|
||||
public static IntPtr GameAllocateApricot(ulong size, ulong alignment = 0)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static nint GameAllocateApricot(ulong size, ulong alignment = 0)
|
||||
{
|
||||
return new IntPtr(IMemorySpace.GetApricotSpace()->Malloc(size, alignment));
|
||||
return new nint(IMemorySpace.GetApricotSpace()->Malloc(size, alignment));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -745,9 +994,10 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="size">Amount of bytes to allocate.</param>
|
||||
/// <param name="alignment">The alignment of the allocation.</param>
|
||||
/// <returns>Pointer to the allocated region.</returns>
|
||||
public static IntPtr GameAllocateSound(ulong size, ulong alignment = 0)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static nint GameAllocateSound(ulong size, ulong alignment = 0)
|
||||
{
|
||||
return new IntPtr(IMemorySpace.GetSoundSpace()->Malloc(size, alignment));
|
||||
return new nint(IMemorySpace.GetSoundSpace()->Malloc(size, alignment));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -756,15 +1006,15 @@ public static unsafe class MemoryHelper
|
|||
/// <remarks>The memory you are freeing must be allocated with game allocators.</remarks>
|
||||
/// <param name="ptr">Position at which the memory to be freed is located.</param>
|
||||
/// <param name="size">Amount of bytes to free.</param>
|
||||
public static void GameFree(ref IntPtr ptr, ulong size)
|
||||
public static void GameFree(ref nint ptr, ulong size)
|
||||
{
|
||||
if (ptr == IntPtr.Zero)
|
||||
if (ptr == nint.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IMemorySpace.Free((void*)ptr, size);
|
||||
ptr = IntPtr.Zero;
|
||||
ptr = nint.Zero;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
|
|||
37
Dalamud/Plugin/Services/IContextMenu.cs
Normal file
37
Dalamud/Plugin/Services/IContextMenu.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using Dalamud.Game.Gui.ContextMenu;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
|
||||
namespace Dalamud.Plugin.Services;
|
||||
|
||||
/// <summary>
|
||||
/// This class provides methods for interacting with the game's context menu.
|
||||
/// </summary>
|
||||
public interface IContextMenu
|
||||
{
|
||||
/// <summary>
|
||||
/// A delegate type used for the <see cref="OnMenuOpened"/> event.
|
||||
/// </summary>
|
||||
/// <param name="args">Information about the currently opening menu.</param>
|
||||
public delegate void OnMenuOpenedDelegate(MenuOpenedArgs args);
|
||||
|
||||
/// <summary>
|
||||
/// Event that gets fired every time the game framework updates.
|
||||
/// </summary>
|
||||
event OnMenuOpenedDelegate OnMenuOpened;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a menu item to a context menu.
|
||||
/// </summary>
|
||||
/// <param name="menuType">The type of context menu to add the item to.</param>
|
||||
/// <param name="item">The item to add.</param>
|
||||
void AddMenuItem(ContextMenuType menuType, MenuItem item);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a menu item from a context menu.
|
||||
/// </summary>
|
||||
/// <param name="menuType">The type of context menu to remove the item from.</param>
|
||||
/// <param name="item">The item to add.</param>
|
||||
/// <returns><see langword="true"/> if the item was removed, <see langword="false"/> if it was not found.</returns>
|
||||
bool RemoveMenuItem(ContextMenuType menuType, MenuItem item);
|
||||
}
|
||||
|
|
@ -1,14 +1,23 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Game.Config;
|
||||
using FFXIVClientStructs.FFXIV.Common.Configuration;
|
||||
using Dalamud.Plugin.Internal.Types;
|
||||
|
||||
namespace Dalamud.Plugin.Services;
|
||||
|
||||
/// <summary>
|
||||
/// This class represents the game's configuration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Accessing <see cref="GameConfigSection"/>-typed properties such as <see cref="System"/>, directly or indirectly
|
||||
/// via <see cref="TryGet(Game.Config.SystemConfigOption,out bool)"/>,
|
||||
/// <see cref="Set(Game.Config.SystemConfigOption,bool)"/>, or alike will block, if the game is not done loading.<br />
|
||||
/// Therefore, avoid accessing configuration from your plugin constructor, especially if your plugin sets
|
||||
/// <see cref="PluginManifest.LoadRequiredState"/> to <c>2</c> and <see cref="PluginManifest.LoadSync"/> to <c>true</c>.
|
||||
/// If property access from the plugin constructor is desired, do the value retrieval asynchronously via
|
||||
/// <see cref="IFramework.RunOnFrameworkThread{T}(Func{T})"/>; do not wait for the result right away.
|
||||
/// </remarks>
|
||||
public interface IGameConfig
|
||||
{
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -194,12 +194,14 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
|
|||
|
||||
try
|
||||
{
|
||||
await using var tempPathStream = File.Open(tempPath, FileMode.Create, FileAccess.Write);
|
||||
await url.DownloadAsync(
|
||||
this.httpClient.SharedHttpClient,
|
||||
tempPathStream,
|
||||
this.cancellationTokenSource.Token);
|
||||
tempPathStream.Dispose();
|
||||
await using (var tempPathStream = File.Open(tempPath, FileMode.Create, FileAccess.Write))
|
||||
{
|
||||
await url.DownloadAsync(
|
||||
this.httpClient.SharedHttpClient,
|
||||
tempPathStream,
|
||||
this.cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
for (var j = RenameAttemptCount; ; j--)
|
||||
{
|
||||
try
|
||||
|
|
@ -265,7 +267,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
|
|||
/// <inheritdoc/>
|
||||
[Pure]
|
||||
public IDalamudTextureWrap GetDalamudTextureWrap(DalamudAsset asset) =>
|
||||
ExtractResult(this.GetDalamudTextureWrapAsync(asset));
|
||||
this.GetDalamudTextureWrapAsync(asset).Result;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Pure]
|
||||
|
|
@ -332,8 +334,6 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
|
|||
}
|
||||
}
|
||||
|
||||
private static T ExtractResult<T>(Task<T> t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult();
|
||||
|
||||
private Task<TOut> TransformImmediate<TIn, TOut>(Task<TIn> task, Func<TIn, TOut> transformer)
|
||||
{
|
||||
if (task.IsCompletedSuccessfully)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ namespace Dalamud.Storage.Assets;
|
|||
/// Think of C++ [[nodiscard]]. Also, like the intended meaning of the attribute, such methods will not have
|
||||
/// externally visible state changes.
|
||||
/// </summary>
|
||||
internal interface IDalamudAssetManager
|
||||
public interface IDalamudAssetManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the shared texture wrap for <see cref="DalamudAsset.Empty4X4"/>.
|
||||
|
|
|
|||
|
|
@ -97,4 +97,76 @@ internal static class ArrayExtensions
|
|||
/// <returns><paramref name="array"/> casted as a <see cref="IReadOnlyCollection{T}"/> if it is one; otherwise the result of <see cref="Enumerable.ToArray{TSource}"/>.</returns>
|
||||
public static IReadOnlyCollection<T> AsReadOnlyCollection<T>(this IEnumerable<T> array) =>
|
||||
array as IReadOnlyCollection<T> ?? array.ToArray();
|
||||
|
||||
/// <inheritdoc cref="List{T}.FindIndex(System.Predicate{T})"/>
|
||||
public static int FindIndex<T>(this IReadOnlyList<T> list, Predicate<T> match)
|
||||
=> list.FindIndex(0, list.Count, match);
|
||||
|
||||
/// <inheritdoc cref="List{T}.FindIndex(int,System.Predicate{T})"/>
|
||||
public static int FindIndex<T>(this IReadOnlyList<T> list, int startIndex, Predicate<T> match)
|
||||
=> list.FindIndex(startIndex, list.Count - startIndex, match);
|
||||
|
||||
/// <inheritdoc cref="List{T}.FindIndex(int,int,System.Predicate{T})"/>
|
||||
public static int FindIndex<T>(this IReadOnlyList<T> list, int startIndex, int count, Predicate<T> match)
|
||||
{
|
||||
if ((uint)startIndex > (uint)list.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(startIndex), startIndex, null);
|
||||
|
||||
if (count < 0 || startIndex > list.Count - count)
|
||||
throw new ArgumentOutOfRangeException(nameof(count), count, null);
|
||||
|
||||
if (match == null)
|
||||
throw new ArgumentNullException(nameof(match));
|
||||
|
||||
var endIndex = startIndex + count;
|
||||
for (var i = startIndex; i < endIndex; i++)
|
||||
{
|
||||
if (match(list[i])) return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="List{T}.FindLastIndex(System.Predicate{T})"/>
|
||||
public static int FindLastIndex<T>(this IReadOnlyList<T> list, Predicate<T> match)
|
||||
=> list.FindLastIndex(list.Count - 1, list.Count, match);
|
||||
|
||||
/// <inheritdoc cref="List{T}.FindLastIndex(int,System.Predicate{T})"/>
|
||||
public static int FindLastIndex<T>(this IReadOnlyList<T> list, int startIndex, Predicate<T> match)
|
||||
=> list.FindLastIndex(startIndex, startIndex + 1, match);
|
||||
|
||||
/// <inheritdoc cref="List{T}.FindLastIndex(int,int,System.Predicate{T})"/>
|
||||
public static int FindLastIndex<T>(this IReadOnlyList<T> list, int startIndex, int count, Predicate<T> match)
|
||||
{
|
||||
if (match == null)
|
||||
throw new ArgumentNullException(nameof(match));
|
||||
|
||||
if (list.Count == 0)
|
||||
{
|
||||
// Special case for 0 length List
|
||||
if (startIndex != -1)
|
||||
throw new ArgumentOutOfRangeException(nameof(startIndex), startIndex, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Make sure we're not out of range
|
||||
if ((uint)startIndex >= (uint)list.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(startIndex), startIndex, null);
|
||||
}
|
||||
|
||||
// 2nd have of this also catches when startIndex == MAXINT, so MAXINT - 0 + 1 == -1, which is < 0.
|
||||
if (count < 0 || startIndex - count + 1 < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(count), count, null);
|
||||
|
||||
var endIndex = startIndex - count;
|
||||
for (var i = startIndex; i > endIndex; i--)
|
||||
{
|
||||
if (match(list[i]))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,21 +39,23 @@ public static class DisposeSafety
|
|||
public static IDisposable ToDisposableIgnoreExceptions<T>(this Task<T> task)
|
||||
where T : IDisposable
|
||||
{
|
||||
return Disposable.Create(() => task.ContinueWith(r =>
|
||||
{
|
||||
_ = r.Exception;
|
||||
if (r.IsCompleted)
|
||||
{
|
||||
try
|
||||
return Disposable.Create(
|
||||
() => task.ContinueWith(
|
||||
r =>
|
||||
{
|
||||
r.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}));
|
||||
_ = r.Exception;
|
||||
if (r.IsCompleted)
|
||||
{
|
||||
try
|
||||
{
|
||||
r.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -102,25 +104,26 @@ public static class DisposeSafety
|
|||
if (disposables is not T[] array)
|
||||
array = disposables?.ToArray() ?? Array.Empty<T>();
|
||||
|
||||
return Disposable.Create(() =>
|
||||
{
|
||||
List<Exception?> exceptions = null;
|
||||
foreach (var d in array)
|
||||
return Disposable.Create(
|
||||
() =>
|
||||
{
|
||||
try
|
||||
List<Exception?> exceptions = null;
|
||||
foreach (var d in array)
|
||||
{
|
||||
d?.Dispose();
|
||||
try
|
||||
{
|
||||
d?.Dispose();
|
||||
}
|
||||
catch (Exception de)
|
||||
{
|
||||
exceptions ??= new();
|
||||
exceptions.Add(de);
|
||||
}
|
||||
}
|
||||
catch (Exception de)
|
||||
{
|
||||
exceptions ??= new();
|
||||
exceptions.Add(de);
|
||||
}
|
||||
}
|
||||
|
||||
if (exceptions is not null)
|
||||
throw new AggregateException(exceptions);
|
||||
});
|
||||
if (exceptions is not null)
|
||||
throw new AggregateException(exceptions);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -137,7 +140,11 @@ public static class DisposeSafety
|
|||
public event Action<IDisposeCallback, Exception?>? AfterDispose;
|
||||
|
||||
/// <inheritdoc cref="Stack{T}.EnsureCapacity"/>
|
||||
public void EnsureCapacity(int capacity) => this.objects.EnsureCapacity(capacity);
|
||||
public void EnsureCapacity(int capacity)
|
||||
{
|
||||
lock (this.objects)
|
||||
this.objects.EnsureCapacity(capacity);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Stack{T}.Push"/>
|
||||
/// <returns>The parameter.</returns>
|
||||
|
|
@ -145,7 +152,10 @@ public static class DisposeSafety
|
|||
public T? Add<T>(T? d) where T : IDisposable
|
||||
{
|
||||
if (d is not null)
|
||||
this.objects.Add(this.CheckAdd(d));
|
||||
{
|
||||
lock (this.objects)
|
||||
this.objects.Add(this.CheckAdd(d));
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
|
@ -155,7 +165,10 @@ public static class DisposeSafety
|
|||
public Action? Add(Action? d)
|
||||
{
|
||||
if (d is not null)
|
||||
this.objects.Add(this.CheckAdd(d));
|
||||
{
|
||||
lock (this.objects)
|
||||
this.objects.Add(this.CheckAdd(d));
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
|
@ -165,7 +178,10 @@ public static class DisposeSafety
|
|||
public Func<Task>? Add(Func<Task>? d)
|
||||
{
|
||||
if (d is not null)
|
||||
this.objects.Add(this.CheckAdd(d));
|
||||
{
|
||||
lock (this.objects)
|
||||
this.objects.Add(this.CheckAdd(d));
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
|
@ -174,7 +190,10 @@ public static class DisposeSafety
|
|||
public GCHandle Add(GCHandle d)
|
||||
{
|
||||
if (d != default)
|
||||
this.objects.Add(this.CheckAdd(d));
|
||||
{
|
||||
lock (this.objects)
|
||||
this.objects.Add(this.CheckAdd(d));
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
|
@ -183,29 +202,41 @@ public static class DisposeSafety
|
|||
/// Queue all the given <see cref="IDisposable"/> to be disposed later.
|
||||
/// </summary>
|
||||
/// <param name="ds">Disposables.</param>
|
||||
public void AddRange(IEnumerable<IDisposable?> ds) =>
|
||||
this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d)));
|
||||
public void AddRange(IEnumerable<IDisposable?> ds)
|
||||
{
|
||||
lock (this.objects)
|
||||
this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue all the given <see cref="IDisposable"/> to be run later.
|
||||
/// </summary>
|
||||
/// <param name="ds">Actions.</param>
|
||||
public void AddRange(IEnumerable<Action?> ds) =>
|
||||
this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d)));
|
||||
public void AddRange(IEnumerable<Action?> ds)
|
||||
{
|
||||
lock (this.objects)
|
||||
this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue all the given <see cref="Func{T}"/> returning <see cref="Task"/> to be run later.
|
||||
/// </summary>
|
||||
/// <param name="ds">Func{Task}s.</param>
|
||||
public void AddRange(IEnumerable<Func<Task>?> ds) =>
|
||||
this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d)));
|
||||
public void AddRange(IEnumerable<Func<Task>?> ds)
|
||||
{
|
||||
lock (this.objects)
|
||||
this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue all the given <see cref="GCHandle"/> to be disposed later.
|
||||
/// </summary>
|
||||
/// <param name="ds">GCHandles.</param>
|
||||
public void AddRange(IEnumerable<GCHandle> ds) =>
|
||||
this.objects.AddRange(ds.Select(d => (object)this.CheckAdd(d)));
|
||||
public void AddRange(IEnumerable<GCHandle> ds)
|
||||
{
|
||||
lock (this.objects)
|
||||
this.objects.AddRange(ds.Select(d => (object)this.CheckAdd(d)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancel all pending disposals.
|
||||
|
|
@ -213,9 +244,12 @@ public static class DisposeSafety
|
|||
/// <remarks>Use this after successful initialization of multiple disposables.</remarks>
|
||||
public void Cancel()
|
||||
{
|
||||
foreach (var o in this.objects)
|
||||
this.CheckRemove(o);
|
||||
this.objects.Clear();
|
||||
lock (this.objects)
|
||||
{
|
||||
foreach (var o in this.objects)
|
||||
this.CheckRemove(o);
|
||||
this.objects.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Stack{T}.EnsureCapacity"/>
|
||||
|
|
@ -264,11 +298,17 @@ public static class DisposeSafety
|
|||
this.BeforeDispose?.InvokeSafely(this);
|
||||
|
||||
List<Exception>? exceptions = null;
|
||||
while (this.objects.Any())
|
||||
while (true)
|
||||
{
|
||||
var obj = this.objects[^1];
|
||||
this.objects.RemoveAt(this.objects.Count - 1);
|
||||
|
||||
object obj;
|
||||
lock (this.objects)
|
||||
{
|
||||
if (this.objects.Count == 0)
|
||||
break;
|
||||
obj = this.objects[^1];
|
||||
this.objects.RemoveAt(this.objects.Count - 1);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
switch (obj)
|
||||
|
|
@ -294,7 +334,8 @@ public static class DisposeSafety
|
|||
}
|
||||
}
|
||||
|
||||
this.objects.TrimExcess();
|
||||
lock (this.objects)
|
||||
this.objects.TrimExcess();
|
||||
|
||||
if (exceptions is not null)
|
||||
{
|
||||
|
|
@ -318,10 +359,16 @@ public static class DisposeSafety
|
|||
this.BeforeDispose?.InvokeSafely(this);
|
||||
|
||||
List<Exception>? exceptions = null;
|
||||
while (this.objects.Any())
|
||||
while (true)
|
||||
{
|
||||
var obj = this.objects[^1];
|
||||
this.objects.RemoveAt(this.objects.Count - 1);
|
||||
object obj;
|
||||
lock (this.objects)
|
||||
{
|
||||
if (this.objects.Count == 0)
|
||||
break;
|
||||
obj = this.objects[^1];
|
||||
this.objects.RemoveAt(this.objects.Count - 1);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -351,7 +398,8 @@ public static class DisposeSafety
|
|||
}
|
||||
}
|
||||
|
||||
this.objects.TrimExcess();
|
||||
lock (this.objects)
|
||||
this.objects.TrimExcess();
|
||||
|
||||
if (exceptions is not null)
|
||||
{
|
||||
|
|
@ -386,7 +434,8 @@ public static class DisposeSafety
|
|||
private void OnItemDisposed(IDisposeCallback obj)
|
||||
{
|
||||
obj.BeforeDispose -= this.OnItemDisposed;
|
||||
this.objects.Remove(obj);
|
||||
lock (this.objects)
|
||||
this.objects.Remove(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Linq;
|
||||
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.Gui.ContextMenu;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Serilog;
|
||||
|
||||
|
|
@ -99,6 +100,23 @@ internal static class EventHandlerExtensions
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replacement for Invoke() on OnMenuOpenedDelegate to catch exceptions that stop event propagation in case
|
||||
/// of a thrown Exception inside of an invocation.
|
||||
/// </summary>
|
||||
/// <param name="openedDelegate">The OnMenuOpenedDelegate in question.</param>
|
||||
/// <param name="argument">Templated argument for Action.</param>
|
||||
public static void InvokeSafely(this IContextMenu.OnMenuOpenedDelegate? openedDelegate, MenuOpenedArgs argument)
|
||||
{
|
||||
if (openedDelegate == null)
|
||||
return;
|
||||
|
||||
foreach (var action in openedDelegate.GetInvocationList().Cast<IContextMenu.OnMenuOpenedDelegate>())
|
||||
{
|
||||
HandleInvoke(() => action(argument));
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleInvoke(Action act)
|
||||
{
|
||||
try
|
||||
|
|
|
|||
12
Dalamud/Utility/IDeferredDisposable.cs
Normal file
12
Dalamud/Utility/IDeferredDisposable.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
namespace Dalamud.Utility;
|
||||
|
||||
/// <summary>
|
||||
/// An extension of <see cref="IDisposable"/> which makes <see cref="IDisposable.Dispose"/> queue
|
||||
/// <see cref="RealDispose"/> to be called at a later time.
|
||||
/// </summary>
|
||||
internal interface IDeferredDisposable : IDisposable
|
||||
{
|
||||
/// <summary>Actually dispose the object.</summary>
|
||||
/// <remarks>Not to be called from the code that uses the end object.</remarks>
|
||||
void RealDispose();
|
||||
}
|
||||
234
Dalamud/Utility/RollingList.cs
Normal file
234
Dalamud/Utility/RollingList.cs
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Dalamud.Utility
|
||||
{
|
||||
/// <summary>
|
||||
/// A list with limited capacity holding items of type <typeparamref name="T"/>.
|
||||
/// Adding further items will result in the list rolling over.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Item type.</typeparam>
|
||||
/// <remarks>
|
||||
/// Implemented as a circular list using a <see cref="List{T}"/> internally.
|
||||
/// Insertions and Removals are not supported.
|
||||
/// Not thread-safe.
|
||||
/// </remarks>
|
||||
internal class RollingList<T> : IList<T>
|
||||
{
|
||||
private List<T> items;
|
||||
private int size;
|
||||
private int firstIndex;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="RollingList{T}"/> class.</summary>
|
||||
/// <param name="size"><see cref="RollingList{T}"/> size.</param>
|
||||
/// <param name="capacity">Internal <see cref="List{T}"/> initial capacity.</param>
|
||||
public RollingList(int size, int capacity)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(size), size, 0);
|
||||
capacity = Math.Min(capacity, size);
|
||||
this.size = size;
|
||||
this.items = new List<T>(capacity);
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="RollingList{T}"/> class.</summary>
|
||||
/// <param name="size"><see cref="RollingList{T}"/> size.</param>
|
||||
public RollingList(int size)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(size), size, 0);
|
||||
this.size = size;
|
||||
this.items = new();
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="RollingList{T}"/> class.</summary>
|
||||
/// <param name="items">Collection where elements are copied from.</param>
|
||||
/// <param name="size"><see cref="RollingList{T}"/> size.</param>
|
||||
public RollingList(IEnumerable<T> items, int size)
|
||||
{
|
||||
if (!items.TryGetNonEnumeratedCount(out var capacity)) capacity = 4;
|
||||
capacity = Math.Min(capacity, size);
|
||||
this.size = size;
|
||||
this.items = new List<T>(capacity);
|
||||
this.AddRange(items);
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="RollingList{T}"/> class.</summary>
|
||||
/// <param name="items">Collection where elements are copied from.</param>
|
||||
/// <param name="size"><see cref="RollingList{T}"/> size.</param>
|
||||
/// <param name="capacity">Internal <see cref="List{T}"/> initial capacity.</param>
|
||||
public RollingList(IEnumerable<T> items, int size, int capacity)
|
||||
{
|
||||
if (items.TryGetNonEnumeratedCount(out var count) && count > capacity) capacity = count;
|
||||
capacity = Math.Min(capacity, size);
|
||||
this.size = size;
|
||||
this.items = new List<T>(capacity);
|
||||
this.AddRange(items);
|
||||
}
|
||||
|
||||
/// <summary>Gets item count.</summary>
|
||||
public int Count => this.items.Count;
|
||||
|
||||
/// <summary>Gets or sets the internal list capacity.</summary>
|
||||
public int Capacity
|
||||
{
|
||||
get => this.items.Capacity;
|
||||
set => this.items.Capacity = Math.Min(value, this.size);
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets rolling list size.</summary>
|
||||
public int Size
|
||||
{
|
||||
get => this.size;
|
||||
set
|
||||
{
|
||||
if (value == this.size) return;
|
||||
if (value > this.size)
|
||||
{
|
||||
if (this.firstIndex > 0)
|
||||
{
|
||||
this.items = new List<T>(this);
|
||||
this.firstIndex = 0;
|
||||
}
|
||||
}
|
||||
else // value < this._size
|
||||
{
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(value), value, 0);
|
||||
if (value < this.Count)
|
||||
{
|
||||
this.items = new List<T>(this.TakeLast(value));
|
||||
this.firstIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
this.size = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets a value indicating whether the item is read only.</summary>
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
/// <summary>Gets or sets an item by index.</summary>
|
||||
/// <param name="index">Item index.</param>
|
||||
/// <returns>Item at specified index.</returns>
|
||||
public T this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfGreaterThanOrEqual(nameof(index), index, this.Count);
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(index), index, 0);
|
||||
return this.items[this.GetRealIndex(index)];
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfGreaterThanOrEqual(nameof(index), index, this.Count);
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(index), index, 0);
|
||||
this.items[this.GetRealIndex(index)] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Adds an item to this <see cref="RollingList{T}"/>.</summary>
|
||||
/// <param name="item">Item to add.</param>
|
||||
public void Add(T item)
|
||||
{
|
||||
if (this.size == 0) return;
|
||||
if (this.items.Count >= this.size)
|
||||
{
|
||||
this.items[this.firstIndex] = item;
|
||||
this.firstIndex = (this.firstIndex + 1) % this.size;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this.items.Count == this.items.Capacity)
|
||||
{
|
||||
// Manual list capacity resize
|
||||
var newCapacity = Math.Max(Math.Min(this.size, this.items.Capacity * 2), this.items.Capacity);
|
||||
this.items.Capacity = newCapacity;
|
||||
}
|
||||
|
||||
this.items.Add(item);
|
||||
}
|
||||
|
||||
Debug.Assert(this.items.Count <= this.size, "Item count should be less than Size");
|
||||
}
|
||||
|
||||
/// <summary>Add items to this <see cref="RollingList{T}"/>.</summary>
|
||||
/// <param name="items">Items to add.</param>
|
||||
public void AddRange(IEnumerable<T> items)
|
||||
{
|
||||
if (this.size == 0) return;
|
||||
foreach (var item in items) this.Add(item);
|
||||
}
|
||||
|
||||
/// <summary>Removes all elements from the <see cref="RollingList{T}"/></summary>
|
||||
public void Clear()
|
||||
{
|
||||
this.items.Clear();
|
||||
this.firstIndex = 0;
|
||||
}
|
||||
|
||||
/// <summary>Find the index of a specific item.</summary>
|
||||
/// <param name="item">item to find.</param>
|
||||
/// <returns>Index where <paramref name="item"/> is found. -1 if not found.</returns>
|
||||
public int IndexOf(T item)
|
||||
{
|
||||
var index = this.items.IndexOf(item);
|
||||
if (index == -1) return -1;
|
||||
return this.GetVirtualIndex(index);
|
||||
}
|
||||
|
||||
/// <summary>Not supported.</summary>
|
||||
[SuppressMessage("Documentation Rules", "SA1611", Justification = "Not supported")]
|
||||
void IList<T>.Insert(int index, T item) => throw new NotSupportedException();
|
||||
|
||||
/// <summary>Not supported.</summary>
|
||||
[SuppressMessage("Documentation Rules", "SA1611", Justification = "Not supported")]
|
||||
void IList<T>.RemoveAt(int index) => throw new NotSupportedException();
|
||||
|
||||
/// <summary>Find wether an item exists.</summary>
|
||||
/// <param name="item">item to find.</param>
|
||||
/// <returns>Wether <paramref name="item"/> is found.</returns>
|
||||
public bool Contains(T item) => this.items.Contains(item);
|
||||
|
||||
/// <summary>Copies the content of this list into an array.</summary>
|
||||
/// <param name="array">Array to copy into.</param>
|
||||
/// <param name="arrayIndex"><paramref name="array"/> index to start coping into.</param>
|
||||
public void CopyTo(T[] array, int arrayIndex)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(arrayIndex), arrayIndex, 0);
|
||||
if (array.Length - arrayIndex < this.Count) ThrowHelper.ThrowArgumentException("Not enough space");
|
||||
for (var index = 0; index < this.Count; index++)
|
||||
{
|
||||
array[arrayIndex++] = this[index];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Not supported.</summary>
|
||||
[SuppressMessage("Documentation Rules", "SA1611", Justification = "Not supported")]
|
||||
[SuppressMessage("Documentation Rules", "SA1615", Justification = "Not supported")]
|
||||
bool ICollection<T>.Remove(T item) => throw new NotSupportedException();
|
||||
|
||||
/// <summary>Gets an enumerator for this <see cref="RollingList{T}"/>.</summary>
|
||||
/// <returns><see cref="RollingList{T}"/> enumerator.</returns>
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
for (var index = 0; index < this.items.Count; index++)
|
||||
{
|
||||
yield return this.items[this.GetRealIndex(index)];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets an enumerator for this <see cref="RollingList{T}"/>.</summary>
|
||||
/// <returns><see cref="RollingList{T}"/> enumerator.</returns>
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private int GetRealIndex(int index) => this.size > 0 ? (index + this.firstIndex) % this.size : 0;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private int GetVirtualIndex(int index) => this.size > 0 ? (this.size + index - this.firstIndex) % this.size : 0;
|
||||
}
|
||||
}
|
||||
52
Dalamud/Utility/ThrowHelper.cs
Normal file
52
Dalamud/Utility/ThrowHelper.cs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Dalamud.Utility
|
||||
{
|
||||
/// <summary>Helper methods for throwing exceptions.</summary>
|
||||
internal static class ThrowHelper
|
||||
{
|
||||
/// <summary>Throws a <see cref="ArgumentException"/> with a specified <paramref name="message"/>.</summary>
|
||||
/// <param name="message">Message for the exception.</param>
|
||||
/// <exception cref="ArgumentException">Thrown by this method.</exception>
|
||||
[DoesNotReturn]
|
||||
public static void ThrowArgumentException(string message) => throw new ArgumentException(message);
|
||||
|
||||
/// <summary>Throws a <see cref="ArgumentOutOfRangeException"/> with a specified <paramref name="message"/> for a specified <paramref name="paramName"/>.</summary>
|
||||
/// <param name="paramName">Parameter name.</param>
|
||||
/// <param name="message">Message for the exception.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown by this method.</exception>
|
||||
[DoesNotReturn]
|
||||
public static void ThrowArgumentOutOfRangeException(string paramName, string message) => throw new ArgumentOutOfRangeException(paramName, message);
|
||||
|
||||
/// <summary>Throws a <see cref="ArgumentOutOfRangeException"/> if the specified <paramref name="value"/> is less than <paramref name="comparand"/>.</summary>
|
||||
/// <typeparam name="T"><see cref="IComparable{T}"/> value type.</typeparam>
|
||||
/// <param name="paramName">Parameter name.</param>
|
||||
/// <param name="value">Value to compare from.</param>
|
||||
/// <param name="comparand">Value to compare with.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown by this method if <paramref name="value"/> is less than <paramref name="comparand"/>.</exception>
|
||||
public static void ThrowArgumentOutOfRangeExceptionIfLessThan<T>(string paramName, T value, T comparand) where T : IComparable<T>
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(value, comparand);
|
||||
#else
|
||||
if (Comparer<T>.Default.Compare(value, comparand) <= -1) ThrowArgumentOutOfRangeException(paramName, $"{paramName} must be greater than or equal {comparand}");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Throws a <see cref="ArgumentOutOfRangeException"/> if the specified <paramref name="value"/> is greater than or equal to <paramref name="comparand"/>.</summary>
|
||||
/// <typeparam name="T"><see cref="IComparable{T}"/> value type.</typeparam>
|
||||
/// <param name="paramName">Parameter name.</param>
|
||||
/// <param name="value">Value to compare from.</param>
|
||||
/// <param name="comparand">Value to compare with.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown by this method if <paramref name="value"/> is greater than or equal to<paramref name="comparand"/>.</exception>
|
||||
public static void ThrowArgumentOutOfRangeExceptionIfGreaterThanOrEqual<T>(string paramName, T value, T comparand) where T : IComparable<T>
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(value, comparand);
|
||||
#else
|
||||
if (Comparer<T>.Default.Compare(value, comparand) >= 0) ThrowArgumentOutOfRangeException(paramName, $"{paramName} must be less than {comparand}");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ using System.Reflection.Emit;
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Data;
|
||||
|
|
@ -22,6 +23,9 @@ using Dalamud.Logging.Internal;
|
|||
using ImGuiNET;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using Serilog;
|
||||
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
using Windows.Win32.Storage.FileSystem;
|
||||
|
||||
namespace Dalamud.Utility;
|
||||
|
|
@ -684,6 +688,55 @@ public static class Util
|
|||
return names.ElementAt(rng.Next(0, names.Count() - 1)).Singular.RawString;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws a corresponding exception if <see cref="HRESULT.FAILED"/> is true.
|
||||
/// </summary>
|
||||
/// <param name="hr">The result value.</param>
|
||||
internal static void ThrowOnError(this HRESULT hr)
|
||||
{
|
||||
if (hr.FAILED)
|
||||
Marshal.ThrowExceptionForHR(hr.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="TaskCompletionSource.SetException(System.Exception)"/> if the task is incomplete.
|
||||
/// </summary>
|
||||
/// <param name="t">The task.</param>
|
||||
/// <param name="ex">The exception to set.</param>
|
||||
internal static void SetExceptionIfIncomplete(this TaskCompletionSource t, Exception ex)
|
||||
{
|
||||
if (t.Task.IsCompleted)
|
||||
return;
|
||||
try
|
||||
{
|
||||
t.SetException(ex);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="TaskCompletionSource.SetException(System.Exception)"/> if the task is incomplete.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the result.</typeparam>
|
||||
/// <param name="t">The task.</param>
|
||||
/// <param name="ex">The exception to set.</param>
|
||||
internal static void SetExceptionIfIncomplete<T>(this TaskCompletionSource<T> t, Exception ex)
|
||||
{
|
||||
if (t.Task.IsCompleted)
|
||||
return;
|
||||
try
|
||||
{
|
||||
t.SetException(ex);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Print formatted GameObject Information to ImGui.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
#include <shellapi.h>
|
||||
#include <ShlGuid.h>
|
||||
#include <ShObjIdl.h>
|
||||
#include <winhttp.h>
|
||||
#include <shlobj_core.h>
|
||||
|
||||
#pragma comment(lib, "comctl32.lib")
|
||||
#pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
|
||||
|
|
@ -152,7 +152,7 @@ std::wstring describe_module(const std::filesystem::path& path) {
|
|||
WORD wLanguage;
|
||||
WORD wCodePage;
|
||||
};
|
||||
const auto langs = std::span(reinterpret_cast<const LANGANDCODEPAGE*>(lpBuffer), size / sizeof(LANGANDCODEPAGE));
|
||||
const auto langs = std::span(static_cast<const LANGANDCODEPAGE*>(lpBuffer), size / sizeof(LANGANDCODEPAGE));
|
||||
for (const auto& lang : langs) {
|
||||
if (!VerQueryValueW(block.data(), std::format(L"\\StringFileInfo\\{:04x}{:04x}\\FileDescription", lang.wLanguage, lang.wCodePage).c_str(), &lpBuffer, &size))
|
||||
continue;
|
||||
|
|
@ -441,6 +441,26 @@ std::wstring escape_shell_arg(const std::wstring& arg) {
|
|||
return res;
|
||||
}
|
||||
|
||||
void open_folder_and_select_items(HWND hwndOpener, const std::wstring& path) {
|
||||
const auto piid = ILCreateFromPathW(path.c_str());
|
||||
if (!piid
|
||||
|| FAILED(SHOpenFolderAndSelectItems(piid, 0, nullptr, 0))) {
|
||||
const auto args = std::format(L"/select,{}", escape_shell_arg(path));
|
||||
SHELLEXECUTEINFOW seiw{
|
||||
.cbSize = sizeof seiw,
|
||||
.hwnd = hwndOpener,
|
||||
.lpFile = L"explorer.exe",
|
||||
.lpParameters = args.c_str(),
|
||||
.nShow = SW_SHOW,
|
||||
};
|
||||
if (!ShellExecuteExW(&seiw))
|
||||
throw_last_error("ShellExecuteExW");
|
||||
}
|
||||
|
||||
if (piid)
|
||||
ILFree(piid);
|
||||
}
|
||||
|
||||
void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const std::string& crashLog, const std::string& troubleshootingPackData) {
|
||||
static const char* SourceLogFiles[] = {
|
||||
"output.log",
|
||||
|
|
@ -457,7 +477,6 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s
|
|||
}};
|
||||
|
||||
std::optional<std::wstring> filePath;
|
||||
std::fstream fileStream;
|
||||
try {
|
||||
IShellItemPtr pItem;
|
||||
SYSTEMTIME st;
|
||||
|
|
@ -482,7 +501,7 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s
|
|||
pItem.Release();
|
||||
filePath.emplace(pFilePath);
|
||||
|
||||
fileStream.open(*filePath, std::ios::binary | std::ios::in | std::ios::out | std::ios::trunc);
|
||||
std::fstream fileStream(*filePath, std::ios::binary | std::ios::in | std::ios::out | std::ios::trunc);
|
||||
|
||||
mz_zip_archive zipa{};
|
||||
zipa.m_pIO_opaque = &fileStream;
|
||||
|
|
@ -518,13 +537,14 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s
|
|||
mz_throw_if_failed(mz_zip_writer_init_v2(&zipa, 0, 0), "mz_zip_writer_init_v2");
|
||||
mz_throw_if_failed(mz_zip_writer_add_mem(&zipa, "trouble.json", troubleshootingPackData.data(), troubleshootingPackData.size(), MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE | MZ_BEST_COMPRESSION), "mz_zip_writer_add_mem: trouble.json");
|
||||
mz_throw_if_failed(mz_zip_writer_add_mem(&zipa, "crash.log", crashLog.data(), crashLog.size(), MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE | MZ_BEST_COMPRESSION), "mz_zip_writer_add_mem: crash.log");
|
||||
std::string logExportLog;
|
||||
|
||||
struct HandleAndBaseOffset {
|
||||
HANDLE h;
|
||||
int64_t off;
|
||||
};
|
||||
const auto fnHandleReader = [](void* pOpaque, mz_uint64 file_ofs, void* pBuf, size_t n) -> size_t {
|
||||
const auto& info = *reinterpret_cast<const HandleAndBaseOffset*>(pOpaque);
|
||||
const auto& info = *static_cast<const HandleAndBaseOffset*>(pOpaque);
|
||||
if (!SetFilePointerEx(info.h, { .QuadPart = static_cast<int64_t>(info.off + file_ofs) }, nullptr, SEEK_SET))
|
||||
throw_last_error("fnHandleReader: SetFilePointerEx");
|
||||
if (DWORD read; !ReadFile(info.h, pBuf, static_cast<DWORD>(n), &read, nullptr))
|
||||
|
|
@ -534,8 +554,12 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s
|
|||
};
|
||||
for (const auto& pcszLogFileName : SourceLogFiles) {
|
||||
const auto logFilePath = logDir / pcszLogFileName;
|
||||
if (!exists(logFilePath))
|
||||
if (!exists(logFilePath)) {
|
||||
logExportLog += std::format("File does not exist: {}\n", ws_to_u8(logFilePath.wstring()));
|
||||
continue;
|
||||
} else {
|
||||
logExportLog += std::format("Including: {}\n", ws_to_u8(logFilePath.wstring()));
|
||||
}
|
||||
|
||||
const auto hLogFile = CreateFileW(logFilePath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
|
||||
if (hLogFile == INVALID_HANDLE_VALUE)
|
||||
|
|
@ -574,12 +598,12 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s
|
|||
), std::format("mz_zip_writer_add_read_buf_callback({})", ws_to_u8(logFilePath.wstring())));
|
||||
}
|
||||
|
||||
mz_throw_if_failed(mz_zip_writer_add_mem(&zipa, "logexport.log", logExportLog.data(), logExportLog.size(), MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE | MZ_BEST_COMPRESSION), "mz_zip_writer_add_mem: logexport.log");
|
||||
mz_throw_if_failed(mz_zip_writer_finalize_archive(&zipa), "mz_zip_writer_finalize_archive");
|
||||
mz_throw_if_failed(mz_zip_writer_end(&zipa), "mz_zip_writer_end");
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
MessageBoxW(hWndParent, std::format(L"Failed to save file: {}", u8_to_ws(e.what())).c_str(), get_window_string(hWndParent).c_str(), MB_OK | MB_ICONERROR);
|
||||
fileStream.close();
|
||||
if (filePath) {
|
||||
try {
|
||||
std::filesystem::remove(*filePath);
|
||||
|
|
@ -590,9 +614,10 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s
|
|||
return;
|
||||
}
|
||||
|
||||
fileStream.close();
|
||||
if (filePath) {
|
||||
ShellExecuteW(hWndParent, nullptr, L"explorer.exe", escape_shell_arg(std::format(L"/select,{}", *filePath)).c_str(), nullptr, SW_SHOW);
|
||||
// Not sure why, but without the wait, the selected file momentarily disappears and reappears
|
||||
Sleep(1000);
|
||||
open_folder_and_select_items(hWndParent, *filePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -603,6 +628,7 @@ enum {
|
|||
IdRadioRestartWithoutDalamud,
|
||||
|
||||
IdButtonRestart = 201,
|
||||
IdButtonSaveTsPack = 202,
|
||||
IdButtonHelp = IDHELP,
|
||||
IdButtonExit = IDCANCEL,
|
||||
};
|
||||
|
|
@ -664,6 +690,9 @@ int main() {
|
|||
std::filesystem::path assetDir, logDir;
|
||||
std::optional<std::vector<std::wstring>> launcherArgs;
|
||||
auto fullDump = false;
|
||||
|
||||
// IFileSaveDialog only works on STA
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
|
||||
std::vector<std::wstring> args;
|
||||
if (int argc = 0; const auto argv = CommandLineToArgvW(GetCommandLineW(), &argc)) {
|
||||
|
|
@ -710,6 +739,42 @@ int main() {
|
|||
return InvalidParameter;
|
||||
}
|
||||
|
||||
if (logDir.filename().wstring().ends_with(L".log")) {
|
||||
std::wcout << L"logDir seems to be pointing to a file; stripping the last path component.\n" << std::endl;
|
||||
std::wcout << L"Previous: " << logDir.wstring() << std::endl;
|
||||
logDir = logDir.parent_path();
|
||||
std::wcout << L"Stripped: " << logDir.wstring() << std::endl;
|
||||
}
|
||||
|
||||
// Only keep the last 3 minidumps
|
||||
if (!logDir.empty())
|
||||
{
|
||||
std::vector<std::pair<std::filesystem::path, std::filesystem::file_time_type>> minidumps;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(logDir)) {
|
||||
if (entry.path().filename().wstring().ends_with(L".dmp")) {
|
||||
minidumps.emplace_back(entry.path(), std::filesystem::last_write_time(entry));
|
||||
}
|
||||
}
|
||||
|
||||
if (minidumps.size() > 3)
|
||||
{
|
||||
std::sort(minidumps.begin(), minidumps.end(), [](const auto& a, const auto& b) { return a.second < b.second; });
|
||||
for (size_t i = 0; i < minidumps.size() - 3; i++) {
|
||||
if (std::filesystem::exists(minidumps[i].first))
|
||||
{
|
||||
std::wcout << std::format(L"Removing old minidump: {}", minidumps[i].first.wstring()) << std::endl;
|
||||
std::filesystem::remove(minidumps[i].first);
|
||||
}
|
||||
|
||||
// Also remove corresponding .log, if it exists
|
||||
if (const auto logPath = minidumps[i].first.replace_extension(L".log"); std::filesystem::exists(logPath)) {
|
||||
std::wcout << std::format(L"Removing corresponding log: {}", logPath.wstring()) << std::endl;
|
||||
std::filesystem::remove(logPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
std::cout << "Waiting for crash...\n";
|
||||
|
||||
|
|
@ -740,6 +805,36 @@ int main() {
|
|||
|
||||
std::cout << "Crash triggered" << std::endl;
|
||||
|
||||
std::cout << "Creating progress window" << std::endl;
|
||||
IProgressDialog* pProgressDialog = NULL;
|
||||
if (SUCCEEDED(CoCreateInstance(CLSID_ProgressDialog, NULL, CLSCTX_ALL, IID_IProgressDialog, (void**)&pProgressDialog)) && pProgressDialog) {
|
||||
pProgressDialog->SetTitle(L"Dalamud Crash Handler");
|
||||
pProgressDialog->SetLine(1, L"The game has crashed!", FALSE, NULL);
|
||||
pProgressDialog->SetLine(2, L"Dalamud is collecting further information...", FALSE, NULL);
|
||||
pProgressDialog->SetLine(3, L"Refreshing Game Module List", FALSE, NULL);
|
||||
pProgressDialog->StartProgressDialog(NULL, NULL, PROGDLG_MARQUEEPROGRESS | PROGDLG_NOCANCEL | PROGDLG_NOMINIMIZE, NULL);
|
||||
IOleWindow* pOleWindow;
|
||||
HRESULT hr = pProgressDialog->QueryInterface(IID_IOleWindow, (LPVOID*)&pOleWindow);
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
HWND hwndProgressDialog = NULL;
|
||||
hr = pOleWindow->GetWindow(&hwndProgressDialog);
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
SetWindowPos(hwndProgressDialog, HWND_TOPMOST, 0, 0, 0, 0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
|
||||
SetForegroundWindow(hwndProgressDialog);
|
||||
}
|
||||
|
||||
pOleWindow->Release();
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
std::cerr << "Failed to create progress window" << std::endl;
|
||||
pProgressDialog = NULL;
|
||||
}
|
||||
|
||||
auto shutup_mutex = CreateMutex(NULL, false, L"DALAMUD_CRASHES_NO_MORE");
|
||||
bool shutup = false;
|
||||
if (shutup_mutex == NULL && GetLastError() == ERROR_ALREADY_EXISTS)
|
||||
|
|
@ -778,6 +873,9 @@ int main() {
|
|||
std::wcerr << std::format(L"SymInitialize error: 0x{:x}", GetLastError()) << std::endl;
|
||||
}
|
||||
|
||||
if (pProgressDialog)
|
||||
pProgressDialog->SetLine(3, L"Reading troubleshooting data", FALSE, NULL);
|
||||
|
||||
std::wstring stackTrace(exinfo.dwStackTraceLength, L'\0');
|
||||
if (exinfo.dwStackTraceLength) {
|
||||
if (DWORD read; !ReadFile(hPipeRead, &stackTrace[0], 2 * exinfo.dwStackTraceLength, &read, nullptr)) {
|
||||
|
|
@ -792,10 +890,12 @@ int main() {
|
|||
}
|
||||
}
|
||||
|
||||
if (pProgressDialog)
|
||||
pProgressDialog->SetLine(3, fullDump ? L"Creating full dump" : L"Creating minidump", FALSE, NULL);
|
||||
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
const auto dalamudLogPath = logDir.empty() ? std::filesystem::path() : logDir / L"Dalamud.log";
|
||||
const auto dalamudBootLogPath = logDir.empty() ? std::filesystem::path() : logDir / L"Dalamud.boot.log";
|
||||
const auto dumpPath = logDir.empty() ? std::filesystem::path() : logDir / std::format(L"dalamud_appcrash_{:04}{:02}{:02}_{:02}{:02}{:02}_{:03}_{}.dmp", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, dwProcessId);
|
||||
const auto logPath = logDir.empty() ? std::filesystem::path() : logDir / std::format(L"dalamud_appcrash_{:04}{:02}{:02}_{:02}{:02}{:02}_{:03}_{}.log", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, dwProcessId);
|
||||
std::wstring dumpError;
|
||||
|
|
@ -844,70 +944,33 @@ int main() {
|
|||
log << L"System Time: " << std::chrono::system_clock::now() << std::endl;
|
||||
log << L"\n" << stackTrace << std::endl;
|
||||
|
||||
if (pProgressDialog)
|
||||
pProgressDialog->SetLine(3, L"Refreshing Module List", FALSE, NULL);
|
||||
|
||||
SymRefreshModuleList(GetCurrentProcess());
|
||||
print_exception_info(exinfo.hThreadHandle, exinfo.ExceptionPointers, exinfo.ContextRecord, log);
|
||||
const auto window_log_str = log.str();
|
||||
print_exception_info_extended(exinfo.ExceptionPointers, exinfo.ContextRecord, log);
|
||||
std::wofstream(logPath) << log.str();
|
||||
|
||||
std::thread submitThread;
|
||||
if (!getenv("DALAMUD_NO_METRIC")) {
|
||||
auto url = std::format(L"/Dalamud/Metric/ReportCrash?lt={}&code={:x}", exinfo.nLifetime, exinfo.ExceptionRecord.ExceptionCode);
|
||||
|
||||
submitThread = std::thread([url = std::move(url)] {
|
||||
const auto hInternet = WinHttpOpen(L"Dalamud Crash Handler/1.0",
|
||||
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
|
||||
WINHTTP_NO_PROXY_NAME,
|
||||
WINHTTP_NO_PROXY_BYPASS, 0);
|
||||
const auto hConnect = !hInternet ? nullptr : WinHttpConnect(hInternet, L"kamori.goats.dev", INTERNET_DEFAULT_HTTP_PORT, 0);
|
||||
const auto hRequest = !hConnect ? nullptr : WinHttpOpenRequest(hConnect, L"GET", url.c_str(), NULL, WINHTTP_NO_REFERER,
|
||||
WINHTTP_DEFAULT_ACCEPT_TYPES,
|
||||
0);
|
||||
if (hRequest) WinHttpAddRequestHeaders(hRequest, L"Host: kamori.goats.dev", (ULONG)-1L, WINHTTP_ADDREQ_FLAG_ADD);
|
||||
const auto bSent = !hRequest ? false : WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS,
|
||||
0, WINHTTP_NO_REQUEST_DATA, 0,
|
||||
0, 0);
|
||||
|
||||
if (!bSent)
|
||||
std::cerr << std::format("Failed to send metric: 0x{:x}", GetLastError()) << std::endl;
|
||||
|
||||
if (WinHttpReceiveResponse(hRequest, nullptr))
|
||||
{
|
||||
DWORD dwStatusCode = 0;
|
||||
DWORD dwStatusCodeSize = sizeof(DWORD);
|
||||
|
||||
WinHttpQueryHeaders(hRequest,
|
||||
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
|
||||
WINHTTP_HEADER_NAME_BY_INDEX,
|
||||
&dwStatusCode, &dwStatusCodeSize, WINHTTP_NO_HEADER_INDEX);
|
||||
|
||||
if (dwStatusCode != 200)
|
||||
std::cerr << std::format("Failed to send metric: {}", dwStatusCode) << std::endl;
|
||||
}
|
||||
|
||||
if (hRequest) WinHttpCloseHandle(hRequest);
|
||||
if (hConnect) WinHttpCloseHandle(hConnect);
|
||||
if (hInternet) WinHttpCloseHandle(hInternet);
|
||||
});
|
||||
}
|
||||
|
||||
TASKDIALOGCONFIG config = { 0 };
|
||||
|
||||
const TASKDIALOG_BUTTON radios[]{
|
||||
{IdRadioRestartNormal, L"Restart"},
|
||||
{IdRadioRestartWithout3pPlugins, L"Restart without 3rd party plugins"},
|
||||
{IdRadioRestartNormal, L"Restart normally"},
|
||||
{IdRadioRestartWithout3pPlugins, L"Restart without custom repository plugins"},
|
||||
{IdRadioRestartWithoutPlugins, L"Restart without any plugins"},
|
||||
{IdRadioRestartWithoutDalamud, L"Restart without Dalamud"},
|
||||
};
|
||||
|
||||
const TASKDIALOG_BUTTON buttons[]{
|
||||
{IdButtonRestart, L"Restart\nRestart the game, optionally without plugins or Dalamud."},
|
||||
{IdButtonRestart, L"Restart\nRestart the game with the above-selected option."},
|
||||
{IdButtonSaveTsPack, L"Save Troubleshooting Info\nSave a .tspack file containing information about this crash for analysis."},
|
||||
{IdButtonExit, L"Exit\nExit the game."},
|
||||
};
|
||||
|
||||
config.cbSize = sizeof(config);
|
||||
config.hInstance = GetModuleHandleW(nullptr);
|
||||
config.dwFlags = TDF_ENABLE_HYPERLINKS | TDF_CAN_BE_MINIMIZED | TDF_ALLOW_DIALOG_CANCELLATION | TDF_USE_COMMAND_LINKS;
|
||||
config.dwFlags = TDF_ENABLE_HYPERLINKS | TDF_CAN_BE_MINIMIZED | TDF_ALLOW_DIALOG_CANCELLATION | TDF_USE_COMMAND_LINKS | TDF_NO_DEFAULT_RADIO_BUTTON;
|
||||
config.pszMainIcon = MAKEINTRESOURCE(IDI_ICON1);
|
||||
config.pszMainInstruction = L"An error in the game occurred";
|
||||
config.pszContent = (L""
|
||||
|
|
@ -915,7 +978,7 @@ int main() {
|
|||
"\n"
|
||||
R"aa(Try running a game repair in XIVLauncher by right clicking the login button, and disabling plugins you don't need. Please also check your antivirus, see our <a href="help">help site</a> for more information.)aa" "\n"
|
||||
"\n"
|
||||
R"aa(Upload <a href="exporttspack">this file (click here)</a> if you want to ask for help in our <a href="discord">Discord server</a>.)aa" "\n"
|
||||
R"aa(For further assistance, please upload <a href="exporttspack">a troubleshooting pack</a> to our <a href="discord">Discord server</a>.)aa" "\n"
|
||||
|
||||
);
|
||||
config.pButtons = buttons;
|
||||
|
|
@ -924,10 +987,9 @@ int main() {
|
|||
config.pszExpandedControlText = L"Hide stack trace";
|
||||
config.pszCollapsedControlText = L"Stack trace for plugin developers";
|
||||
config.pszExpandedInformation = window_log_str.c_str();
|
||||
config.pszWindowTitle = L"Dalamud Error";
|
||||
config.pszWindowTitle = L"Dalamud Crash Handler";
|
||||
config.pRadioButtons = radios;
|
||||
config.cRadioButtons = ARRAYSIZE(radios);
|
||||
config.nDefaultRadioButton = IdRadioRestartNormal;
|
||||
config.cxWidth = 300;
|
||||
|
||||
#if _DEBUG
|
||||
|
|
@ -949,6 +1011,7 @@ int main() {
|
|||
case TDN_CREATED:
|
||||
{
|
||||
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
|
||||
SendMessage(hwnd, TDM_ENABLE_BUTTON, IdButtonRestart, 0);
|
||||
return S_OK;
|
||||
}
|
||||
case TDN_HYPERLINK_CLICKED:
|
||||
|
|
@ -957,7 +1020,7 @@ int main() {
|
|||
if (link == L"help") {
|
||||
ShellExecuteW(hwnd, nullptr, L"https://goatcorp.github.io/faq?utm_source=vectored", nullptr, nullptr, SW_SHOW);
|
||||
} else if (link == L"logdir") {
|
||||
ShellExecuteW(hwnd, nullptr, L"explorer.exe", escape_shell_arg(std::format(L"/select,{}", logPath.wstring())).c_str(), nullptr, SW_SHOW);
|
||||
open_folder_and_select_items(hwnd, logPath.wstring());
|
||||
} else if (link == L"logfile") {
|
||||
ShellExecuteW(hwnd, nullptr, logPath.c_str(), nullptr, nullptr, SW_SHOW);
|
||||
} else if (link == L"exporttspack") {
|
||||
|
|
@ -970,6 +1033,18 @@ int main() {
|
|||
}
|
||||
return S_OK;
|
||||
}
|
||||
case TDN_RADIO_BUTTON_CLICKED:
|
||||
SendMessage(hwnd, TDM_ENABLE_BUTTON, IdButtonRestart, 1);
|
||||
return S_OK;
|
||||
case TDN_BUTTON_CLICKED:
|
||||
const auto button = static_cast<int>(wParam);
|
||||
if (button == IdButtonSaveTsPack)
|
||||
{
|
||||
export_tspack(hwnd, logDir, ws_to_u8(log.str()), troubleshootingPackData);
|
||||
return S_FALSE; // keep the dialog open
|
||||
}
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return S_OK;
|
||||
|
|
@ -979,10 +1054,11 @@ int main() {
|
|||
return (*reinterpret_cast<decltype(callback)*>(dwRefData))(hwnd, uNotification, wParam, lParam);
|
||||
};
|
||||
config.lpCallbackData = reinterpret_cast<LONG_PTR>(&callback);
|
||||
|
||||
if (submitThread.joinable()) {
|
||||
submitThread.join();
|
||||
submitThread = {};
|
||||
|
||||
if (pProgressDialog) {
|
||||
pProgressDialog->StopProgressDialog();
|
||||
pProgressDialog->Release();
|
||||
pProgressDialog = NULL;
|
||||
}
|
||||
|
||||
if (shutup) {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ void ConsoleTeardown()
|
|||
|
||||
std::optional<CoreCLR> g_clr;
|
||||
|
||||
int InitializeClrAndGetEntryPoint(
|
||||
HRESULT InitializeClrAndGetEntryPoint(
|
||||
void* calling_module,
|
||||
bool enable_etw,
|
||||
std::wstring runtimeconfig_path,
|
||||
|
|
@ -76,7 +76,7 @@ int InitializeClrAndGetEntryPoint(
|
|||
if (result != 0)
|
||||
{
|
||||
logging::E("Unable to get RoamingAppData path (err={})", result);
|
||||
return result;
|
||||
return HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND);
|
||||
}
|
||||
|
||||
std::filesystem::path fs_app_data(_appdata);
|
||||
|
|
@ -92,7 +92,7 @@ int InitializeClrAndGetEntryPoint(
|
|||
if (!std::filesystem::exists(dotnet_path))
|
||||
{
|
||||
logging::E("Error: Unable to find .NET runtime path");
|
||||
return 1;
|
||||
return HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND);
|
||||
}
|
||||
|
||||
get_hostfxr_parameters init_parameters
|
||||
|
|
@ -137,12 +137,12 @@ int InitializeClrAndGetEntryPoint(
|
|||
entrypoint_delegate_type_name.c_str(),
|
||||
nullptr, entrypoint_fn)) != 0)
|
||||
{
|
||||
logging::E("Failed to load module (err={})", result);
|
||||
logging::E("Failed to load module (err=0x{:X})", static_cast<uint32_t>(result));
|
||||
return result;
|
||||
}
|
||||
logging::I("Done!");
|
||||
|
||||
// =========================================================================== //
|
||||
|
||||
return 0;
|
||||
return S_OK;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
void ConsoleSetup(const std::wstring console_name);
|
||||
void ConsoleTeardown();
|
||||
|
||||
int InitializeClrAndGetEntryPoint(
|
||||
HRESULT InitializeClrAndGetEntryPoint(
|
||||
void* calling_module,
|
||||
bool enable_etw,
|
||||
std::wstring runtimeconfig_path,
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit e3bd5910678683a718e68f0f940c88b08c24eba5
|
||||
Subproject commit 722a2c512238ac4b5324e3d343b316d8c8633a02
|
||||
Loading…
Add table
Add a link
Reference in a new issue