diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj b/Dalamud.Boot/Dalamud.Boot.vcxproj index ea263d7f9..298edbcbc 100644 --- a/Dalamud.Boot/Dalamud.Boot.vcxproj +++ b/Dalamud.Boot/Dalamud.Boot.vcxproj @@ -32,6 +32,9 @@ obj\$(Configuration)\ + + + true $(SolutionDir)bin\lib\$(Configuration)\libMinHook\;$(VC_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64) @@ -55,7 +58,7 @@ Windows true false - Version.lib;%(AdditionalDependencies) + Version.lib;Shlwapi.lib;%(AdditionalDependencies) ..\lib\CoreCLR;%(AdditionalLibraryDirectories) @@ -70,6 +73,7 @@ false false + module.def @@ -83,9 +87,13 @@ true true + module.def + + + nethost.dll @@ -129,6 +137,7 @@ NotUsing NotUsing + NotUsing NotUsing @@ -168,6 +177,7 @@ + @@ -181,8 +191,14 @@ + + + + + + - \ No newline at end of file + diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters index 8b4483684..87eaf6fcc 100644 --- a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters +++ b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters @@ -73,6 +73,9 @@ Dalamud.Boot DLL + + Dalamud.Boot DLL + @@ -140,6 +143,9 @@ + + Dalamud.Boot DLL + @@ -147,4 +153,14 @@ + + + Dalamud.Boot DLL + + + + + Dalamud.Boot DLL + + \ No newline at end of file diff --git a/Dalamud.Boot/DalamudStartInfo.cpp b/Dalamud.Boot/DalamudStartInfo.cpp index d20265bf8..f5632a2ea 100644 --- a/Dalamud.Boot/DalamudStartInfo.cpp +++ b/Dalamud.Boot/DalamudStartInfo.cpp @@ -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); diff --git a/Dalamud.Boot/DalamudStartInfo.h b/Dalamud.Boot/DalamudStartInfo.h index 5cee8f16b..e6cc54ab0 100644 --- a/Dalamud.Boot/DalamudStartInfo.h +++ b/Dalamud.Boot/DalamudStartInfo.h @@ -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; diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp index 2566016e8..e6aa9c4ac 100644 --- a/Dalamud.Boot/dllmain.cpp +++ b/Dalamud.Boot/dllmain.cpp @@ -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)); } diff --git a/Dalamud.Boot/hooks.cpp b/Dalamud.Boot/hooks.cpp index 7cf489195..1b1280cf0 100644 --- a/Dalamud.Boot/hooks.cpp +++ b/Dalamud.Boot/hooks.cpp @@ -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("LdrRegisterDllNotification"); -static const auto LdrUnregisterDllNotification = utils::loaded_module(GetModuleHandleW(L"ntdll.dll")).get_exported_function("LdrUnregisterDllNotification"); - hooks::getprocaddress_singleton_import_hook::getprocaddress_singleton_import_hook() : m_pfnGetProcAddress(GetProcAddress) , m_thunk("kernel32!GetProcAddress(Singleton Import Hook)", diff --git a/Dalamud.Boot/hooks.h b/Dalamud.Boot/hooks.h index ad3b2cc6c..f6ad370d1 100644 --- a/Dalamud.Boot/hooks.h +++ b/Dalamud.Boot/hooks.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include "utils.h" diff --git a/Dalamud.Boot/module.def b/Dalamud.Boot/module.def new file mode 100644 index 000000000..047d825e5 --- /dev/null +++ b/Dalamud.Boot/module.def @@ -0,0 +1,5 @@ +LIBRARY Dalamud.Boot +EXPORTS + Initialize @1 + RewriteRemoteEntryPointW @2 + RewrittenEntryPoint @3 diff --git a/Dalamud.Boot/ntdll.cpp b/Dalamud.Boot/ntdll.cpp new file mode 100644 index 000000000..9bda0e1c4 --- /dev/null +++ b/Dalamud.Boot/ntdll.cpp @@ -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("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("LdrUnregisterDllNotification"); + return pfn(Cookie); +} diff --git a/Dalamud.Boot/ntdll.h b/Dalamud.Boot/ntdll.h new file mode 100644 index 000000000..c631475d1 --- /dev/null +++ b/Dalamud.Boot/ntdll.h @@ -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); diff --git a/Dalamud.Boot/pch.h b/Dalamud.Boot/pch.h index 3302a44fb..c2194c157 100644 --- a/Dalamud.Boot/pch.h +++ b/Dalamud.Boot/pch.h @@ -15,18 +15,28 @@ #include // Windows Header Files (2) +#include #include #include +#include #include #include #include +#include #include #include +// Windows Header Files (3) +#include // Must be loaded after iphlpapi.h + // MSVC Compiler Intrinsic #include +// COM +#include + // C++ Standard Libraries +#include #include #include #include @@ -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; diff --git a/Dalamud.Boot/rewrite_entrypoint.cpp b/Dalamud.Boot/rewrite_entrypoint.cpp index 85a3a950b..3a1672af7 100644 --- a/Dalamud.Boot/rewrite_entrypoint.cpp +++ b/Dalamud.Boot/rewrite_entrypoint.cpp @@ -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(pfn); + if (*ptr == 0xe9) + return ptr + 5 + *reinterpret_cast(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(resolve_thunk_address(pfn)); *reinterpret_cast(ptr) != Terminator; ptr++) + length++; + return length; + } - struct DUMMYSTRUCTNAME { - struct { - const uint8_t op_mov_rcx_imm[2]{ 0x48, 0xb9 }; - void* val = nullptr; - } lpLibFileName; + template + void* fill_placeholders(void* pfn, const T& value) { + auto ptr = static_cast(pfn); - struct { - const uint8_t op_mov_rdi_imm[2]{ 0x48, 0xbf }; - decltype(&LoadLibraryW) ptr = nullptr; - } fn; + while (*reinterpret_cast(ptr) != Placeholder) + ptr++; - const uint8_t op_call_rdi[2]{ 0xff, 0xd7 }; - } CallLoadLibrary_nethost; + *reinterpret_cast(ptr) = 0; + *reinterpret_cast(ptr) = value; + return ptr + sizeof(value); + } - struct DUMMYSTRUCTNAME { - struct { - const uint8_t op_mov_rcx_imm[2]{ 0x48, 0xb9 }; - void* val = nullptr; - } lpLibFileName; + template + void* fill_placeholders(void* ptr, const T& value, TArgs&&...more_values) { + return fill_placeholders(fill_placeholders(ptr, value), std::forward(more_values)...); + } - struct { - const uint8_t op_mov_rdi_imm[2]{ 0x48, 0xbf }; - decltype(&LoadLibraryW) ptr = nullptr; - } fn; + std::vector create_entrypointreplacement() { + std::vector 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 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(dalamud_path_wview.data()), dalamud_path_wview.size() * 2 + 2); + const auto nethost_path_view = std::span(reinterpret_cast(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 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(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(&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(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(wstr.size()), nullptr, 0, nullptr, nullptr), 0); - WideCharToMultiByte(codePage, 0, &wstr[0], static_cast(wstr.size()), &str[0], static_cast(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(str.size()), nullptr, 0), 0); - MultiByteToWideChar(codePage, errorOnInvalidChars ? MB_ERR_INVALID_CHARS : 0, &str[0], static_cast(str.size()), &wstr[0], static_cast(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(get_mapped_image_base_address(hProcess, pcwzPath)); + last_operation = L"get_mapped_image_base_address"; + const auto base_address = static_cast(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(&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(&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(pcwzLoadInfo)"; + auto load_info = unicode::convert(pcwzLoadInfo); load_info.resize(load_info.size() + 1); //ensure null termination - // Allocate full buffer in advance to keep reference to trampoline valid. - std::vector buffer(sizeof TrampolineTemplate + load_info.size() + nethost_path_bytes.size() + path_bytes.size()); - auto& trampoline = *reinterpret_cast(&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 buffer(bufferSize); // Allocate buffer in remote process, which will be used to fill addresses in the local buffer. - const auto remote_buffer = reinterpret_cast(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(VirtualAllocEx(hProcess, nullptr, buffer.size(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)); + + auto& params = *reinterpret_cast(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(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(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(L"Dalamud.Boot")))) + return hr; + if (FAILED(cei->SetDescription(const_cast(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(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(¶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(unicode::convert(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); } diff --git a/Dalamud.Boot/rewrite_entrypoint_thunks.asm b/Dalamud.Boot/rewrite_entrypoint_thunks.asm new file mode 100644 index 000000000..af7be8287 --- /dev/null +++ b/Dalamud.Boot/rewrite_entrypoint_thunks.asm @@ -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 diff --git a/Dalamud.Boot/utils.cpp b/Dalamud.Boot/utils.cpp index 62a9d7055..65018add4 100644 --- a/Dalamud.Boot/utils.cpp +++ b/Dalamud.Boot/utils.cpp @@ -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(const_cast(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(const_cast(pAddress)), length) { try { - for (auto pCoveredAddress = &m_data[0]; - pCoveredAddress < &m_data[0] + m_data.size(); - pCoveredAddress = reinterpret_cast(m_regions.back().BaseAddress) + m_regions.back().RegionSize) { + for (auto pCoveredAddress = m_data.data(); + pCoveredAddress < m_data.data() + m_data.size(); + pCoveredAddress = static_cast(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(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(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 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(&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); +} diff --git a/Dalamud.Boot/utils.h b/Dalamud.Boot/utils.h index ebf48a294..f10e277c0 100644 --- a/Dalamud.Boot/utils.h +++ b/Dalamud.Boot/utils.h @@ -111,10 +111,13 @@ namespace utils { }; class memory_tenderizer { + HANDLE m_process; std::span m_data; std::vector 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&& std::is_standard_layout_v>> @@ -264,8 +267,6 @@ namespace utils { return get_env_list(unicode::convert(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); } diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index eb27acce7..58234783a 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -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> g_HookSetUnhandledExceptionFilter; HANDLE g_crashhandler_process = nullptr; HANDLE g_crashhandler_event = nullptr; @@ -110,13 +112,16 @@ static void append_injector_launch_args(std::vector& 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(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(g_startInfo.WorkingDirectory) + L"\""); + args.emplace_back(L"--dalamud-configuration-path=\"" + unicode::convert(g_startInfo.ConfigurationPath) + L"\""); + args.emplace_back(L"--logpath=\"" + unicode::convert(g_startInfo.LogPath) + L"\""); + args.emplace_back(L"--logname=\"" + unicode::convert(g_startInfo.LogName) + L"\""); + args.emplace_back(L"--dalamud-plugin-directory=\"" + unicode::convert(g_startInfo.PluginDirectory) + L"\""); + args.emplace_back(L"--dalamud-asset-directory=\"" + unicode::convert(g_startInfo.AssetDirectory) + L"\""); + args.emplace_back(std::format(L"--dalamud-client-language={}", static_cast(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& 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( - time_now.time_since_epoch()).count() + time_now.time_since_epoch()).count() - std::chrono::duration_cast( 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(g_clr->get_function_pointer( + if (!g_clr) + { + stackTrace = L"(no CLR stack trace available)"; + } + else if (void* fn; const auto err = static_cast(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(fn)(); // Don't free it, as the program's going to be quit anyway } - + exinfo.dwStackTraceLength = static_cast(stackTrace.size()); exinfo.dwTroubleshootingPackDataLength = static_cast(g_startInfo.TroubleshootingPackData.size()); if (DWORD written; !WriteFile(g_crashhandler_pipe_write, &exinfo, static_cast(sizeof exinfo), &written, nullptr) || sizeof exinfo != written) return EXCEPTION_CONTINUE_SEARCH; - if (DWORD written; !WriteFile(g_crashhandler_pipe_write, &stackTrace[0], static_cast(std::span(stackTrace).size_bytes()), &written, nullptr) || std::span(stackTrace).size_bytes() != written) - return EXCEPTION_CONTINUE_SEARCH; + if (const auto nb = static_cast(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(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(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(lpTopLevelExceptionFilter), reinterpret_cast(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; diff --git a/Dalamud.Boot/xivfixes.cpp b/Dalamud.Boot/xivfixes.cpp index e16dd6e5a..f3b6aaa2c 100644 --- a/Dalamud.Boot/xivfixes.cpp +++ b/Dalamud.Boot/xivfixes.cpp @@ -5,9 +5,8 @@ #include "DalamudStartInfo.h" #include "hooks.h" #include "logging.h" +#include "ntdll.h" #include "utils.h" -#include -#include template static std::span assume_nonempty_span(std::span 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> s_HookClrFatalError; - static std::optional> 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> 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(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(ddir.AddressOfRawData); + if (pdbref.Signature == DotNetPdbInfoSignatureValue) { + const auto pathSpan = std::string_view(pdbref.PdbPath, strlen(pdbref.PdbPath)); + const auto pathWide = unicode::convert(pathSpan); + std::wstring windowsDirectory(GetWindowsDirectoryW(nullptr, 0) + 1, L'\0'); + windowsDirectory.resize( + GetWindowsDirectoryW(windowsDirectory.data(), static_cast(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(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> { @@ -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 { diff --git a/Dalamud.Boot/xivfixes.h b/Dalamud.Boot/xivfixes.h index 701913c88..afe2edb45 100644 --- a/Dalamud.Boot/xivfixes.h +++ b/Dalamud.Boot/xivfixes.h @@ -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); } diff --git a/Dalamud.Common/DalamudStartInfo.cs b/Dalamud.Common/DalamudStartInfo.cs index edf21d174..a84d3b68f 100644 --- a/Dalamud.Common/DalamudStartInfo.cs +++ b/Dalamud.Common/DalamudStartInfo.cs @@ -5,6 +5,7 @@ namespace Dalamud.Common; /// /// Struct containing information needed to initialize Dalamud. +/// Modify DalamudStartInfo.h and DalamudStartInfo.cpp along with this record. /// [Serializable] public record DalamudStartInfo diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index a6aa96cdf..f9959a910 100644 --- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -27,7 +27,7 @@ - + diff --git a/Dalamud.Injector.Boot/main.cpp b/Dalamud.Injector.Boot/main.cpp index 741505d08..7fc44f5e1 100644 --- a/Dalamud.Injector.Boot/main.cpp +++ b/Dalamud.Injector.Boot/main.cpp @@ -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(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; } diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index f839d9656..9085eae04 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -31,89 +31,100 @@ namespace Dalamud.Injector /// /// Count of arguments. /// char** string arguments. - public delegate void MainDelegate(int argc, IntPtr argvPtr); + /// Return value (HRESULT). + public delegate int MainDelegate(int argc, IntPtr argvPtr); /// /// Start the Dalamud injector. /// /// Count of arguments. /// byte** string arguments. - public static void Main(int argc, IntPtr argvPtr) + /// Return value (HRESULT). + public static int Main(int argc, IntPtr argvPtr) { - List args = new(argc); - - unsafe + try { - var argv = (IntPtr*)argvPtr; - for (var i = 0; i < argc; i++) - args.Add(Marshal.PtrToStringUni(argv[i])); - } + List 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(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(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 { "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!"); } }, diff --git a/Dalamud.sln b/Dalamud.sln index 200238a83..93089b9a6 100644 --- a/Dalamud.sln +++ b/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 diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 66c2745c5..85a9507c9 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -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 /// /// Gets or sets a value indicating whether to use AXIS fonts from the game. /// - public bool UseAxisFontsFromGame { get; set; } = false; + [Obsolete($"See {nameof(DefaultFontSpec)}")] + public bool UseAxisFontsFromGame { get; set; } = true; + + /// + /// Gets or sets the default font spec. + /// + public IFontSpec? DefaultFontSpec { get; set; } /// /// Gets or sets the gamma value to apply for Dalamud fonts. Do not use. @@ -208,6 +215,11 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// public bool LogOpenAtStartup { get; set; } + /// + /// Gets or sets the number of lines to keep for the Dalamud Console window. + /// + public int LogLinesLimit { get; set; } = 10000; + /// /// Gets or sets a value indicating whether or not the dev bar should open at startup. /// diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 434e6f868..f6ac5b151 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.17 + 9.0.0.21 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) @@ -68,7 +68,7 @@ - + diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index b82d64f24..162df9417 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -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 tcsSystem = new(); + private readonly TaskCompletionSource tcsUiConfig = new(); + private readonly TaskCompletionSource tcsUiControl = new(); + private readonly GameConfigAddressResolver address = new(); private Hook? 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.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.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? UiControlChanged; #pragma warning restore 67 - /// - public GameConfigSection System { get; private set; } + /// + /// Gets a task representing the initialization state of this class. + /// + public Task InitializationTask => this.tcsInitialization.Task; /// - public GameConfigSection UiConfig { get; private set; } + public GameConfigSection System => this.tcsSystem.Task.Result; /// - public GameConfigSection UiControl { get; private set; } + public GameConfigSection UiConfig => this.tcsUiConfig.Task.Result; + + /// + public GameConfigSection UiControl => this.tcsUiControl.Task.Result; /// 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 /// 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.Get(); + private readonly Task initializationTask; + /// /// Initializes a new instance of the class. /// 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(); } /// @@ -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; diff --git a/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs b/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs new file mode 100644 index 000000000..65c9b2760 --- /dev/null +++ b/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs @@ -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; + +/// +/// This class handles interacting with the game's (right-click) context menu. +/// +[InterfaceVersion("1.0")] +[ServiceManager.EarlyLoadedService] +internal sealed unsafe class ContextMenu : IDisposable, IServiceType, IContextMenu +{ + private static readonly ModuleLog Log = new("ContextMenu"); + + private readonly Hook raptureAtkModuleOpenAddonByAgentHook; + private readonly Hook addonContextMenuOnMenuSelectedHook; + private readonly RaptureAtkModuleOpenAddonDelegate raptureAtkModuleOpenAddon; + + [ServiceManager.ServiceConstructor] + private ContextMenu() + { + this.raptureAtkModuleOpenAddonByAgentHook = Hook.FromAddress((nint)RaptureAtkModule.Addresses.OpenAddonByAgent.Value, this.RaptureAtkModuleOpenAddonByAgentDetour); + this.addonContextMenuOnMenuSelectedHook = Hook.FromAddress((nint)AddonContextMenu.StaticVTable.OnMenuSelected, this.AddonContextMenuOnMenuSelectedDetour); + this.raptureAtkModuleOpenAddon = Marshal.GetDelegateForFunctionPointer((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); + + /// + public event IContextMenu.OnMenuOpenedDelegate OnMenuOpened; + + private Dictionary> MenuItems { get; } = new(); + + private object MenuItemsLock { get; } = new(); + + private AgentInterface* SelectedAgent { get; set; } + + private ContextMenuType? SelectedMenuType { get; set; } + + private List? SelectedItems { get; set; } + + private HashSet SelectedEventInterfaces { get; } = new(); + + private AtkUnitBase* SelectedParentAddon { get; set; } + + // -1 -> -inf: native items + // 0 -> inf: selected items + private List MenuCallbackIds { get; } = new(); + + private IReadOnlyList? SubmenuItems { get; set; } + + /// + 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(); + } + + /// + 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); + } + } + + /// + 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 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.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 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(values, headerCount); + var nameData = new Span(values + headerCount, nativeMenuSize + items.Count); + var disabledData = hasAnyDisabled ? new Span(values + headerCount + nativeMenuSize + items.Count, nativeMenuSize + items.Count) : Span.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(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 disabledData, Span 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 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 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>(menu->EventHandlerArray, 32); + var ids = new Span(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 FixupMenuList(List 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 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); + } +} + +/// +/// Plugin-scoped version of a service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class ContextMenuPluginScoped : IDisposable, IServiceType, IContextMenu +{ + [ServiceManager.ServiceDependency] + private readonly ContextMenu parentService = Service.Get(); + + private ContextMenuPluginScoped() + { + this.parentService.OnMenuOpened += this.OnMenuOpenedForward; + } + + /// + public event IContextMenu.OnMenuOpenedDelegate OnMenuOpened; + + private Dictionary> MenuItems { get; } = new(); + + private object MenuItemsLock { get; } = new(); + + /// + 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); + } + } + } + + /// + 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); + } + + /// + 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); +} diff --git a/Dalamud/Game/Gui/ContextMenu/ContextMenuType.cs b/Dalamud/Game/Gui/ContextMenu/ContextMenuType.cs new file mode 100644 index 000000000..2cd52a4b7 --- /dev/null +++ b/Dalamud/Game/Gui/ContextMenu/ContextMenuType.cs @@ -0,0 +1,18 @@ +namespace Dalamud.Game.Gui.ContextMenu; + +/// +/// The type of context menu. +/// Each one has a different associated . +/// +public enum ContextMenuType +{ + /// + /// The default context menu. + /// + Default, + + /// + /// The inventory context menu. Used when right-clicked on an item. + /// + Inventory, +} diff --git a/Dalamud/Game/Gui/ContextMenu/MenuArgs.cs b/Dalamud/Game/Gui/ContextMenu/MenuArgs.cs new file mode 100644 index 000000000..d0d8ec0dc --- /dev/null +++ b/Dalamud/Game/Gui/ContextMenu/MenuArgs.cs @@ -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; + +/// +/// Base class for menu args. +/// +public abstract unsafe class MenuArgs +{ + private IReadOnlySet? eventInterfaces; + + /// + /// Initializes a new instance of the class. + /// + /// Addon associated with the context menu. + /// Agent associated with the context menu. + /// The type of context menu. + /// List of AtkEventInterfaces associated with the context menu. + protected internal MenuArgs(AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet? 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)), + }; + } + + /// + /// Gets the name of the addon that opened the context menu. + /// + public string? AddonName { get; } + + /// + /// Gets the memory pointer of the addon that opened the context menu. + /// + public nint AddonPtr { get; } + + /// + /// Gets the memory pointer of the agent that opened the context menu. + /// + public nint AgentPtr { get; } + + /// + /// Gets the type of the context menu. + /// + public ContextMenuType MenuType { get; } + + /// + /// Gets the target info of the context menu. The actual type depends on . + /// signifies a . + /// signifies a . + /// + public MenuTarget Target { get; } + + /// + /// Gets a list of AtkEventInterface pointers associated with the context menu. + /// Only available with . + /// Almost always an agent pointer. You can use this to find out what type of context menu it is. + /// + /// Thrown when the context menu is not a . + public IReadOnlySet EventInterfaces => + this.MenuType != ContextMenuType.Default ? + this.eventInterfaces : + throw new InvalidOperationException("Not a default context menu"); +} diff --git a/Dalamud/Game/Gui/ContextMenu/MenuItem.cs b/Dalamud/Game/Gui/ContextMenu/MenuItem.cs new file mode 100644 index 000000000..fdeb64d13 --- /dev/null +++ b/Dalamud/Game/Gui/ContextMenu/MenuItem.cs @@ -0,0 +1,91 @@ +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; + +using Lumina.Excel.GeneratedSheets; + +namespace Dalamud.Game.Gui.ContextMenu; + +/// +/// A menu item that can be added to a context menu. +/// +public sealed record MenuItem +{ + /// + /// Gets or sets the display name of the menu item. + /// + public SeString Name { get; set; } = SeString.Empty; + + /// + /// Gets or sets the prefix attached to the beginning of . + /// + public SeIconChar? Prefix { get; set; } + + /// + /// Sets the character to prefix the with. Will be converted into a fancy boxed letter icon. Must be an uppercase letter. + /// + /// must be an uppercase letter. + 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; + } + } + } + + /// + /// Gets or sets the color of the . Specifies a row id. + /// + public ushort PrefixColor { get; set; } + + /// + /// Gets or sets the callback to be invoked when the menu item is clicked. + /// + public Action? OnClicked { get; set; } + + /// + /// 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. + /// + public int Priority { get; set; } + + /// + /// Gets or sets a value indicating whether the menu item is enabled. + /// Disabled items will be faded and cannot be clicked on. + /// + public bool IsEnabled { get; set; } = true; + + /// + /// 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. + /// + public bool IsSubmenu { get; set; } + + /// + /// 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 and are true, the return arrow will take precedence. + /// + public bool IsReturn { get; set; } + + /// + /// Gets the name with the given prefix. + /// + internal SeString PrefixedName => + this.Prefix is { } prefix + ? new SeStringBuilder() + .AddUiForeground($"{prefix.ToIconString()} ", this.PrefixColor) + .Append(this.Name) + .Build() + : this.Name; +} diff --git a/Dalamud/Game/Gui/ContextMenu/MenuItemClickedArgs.cs b/Dalamud/Game/Gui/ContextMenu/MenuItemClickedArgs.cs new file mode 100644 index 000000000..bec16590d --- /dev/null +++ b/Dalamud/Game/Gui/ContextMenu/MenuItemClickedArgs.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; + +using Dalamud.Game.Text.SeStringHandling; + +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Gui.ContextMenu; + +/// +/// Callback args used when a menu item is clicked. +/// +public sealed unsafe class MenuItemClickedArgs : MenuArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// Callback for opening a submenu. + /// Addon associated with the context menu. + /// Agent associated with the context menu. + /// The type of context menu. + /// List of AtkEventInterfaces associated with the context menu. + internal MenuItemClickedArgs(Action> openSubmenu, AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet eventInterfaces) + : base(addon, agent, type, eventInterfaces) + { + this.OnOpenSubmenu = openSubmenu; + } + + private Action> OnOpenSubmenu { get; } + + /// + /// Opens a submenu with the given name and items. + /// + /// The name of the submenu, displayed at the top. + /// The items to display in the submenu. + public void OpenSubmenu(SeString name, IReadOnlyList items) => + this.OnOpenSubmenu(name, items); + + /// + /// Opens a submenu with the given items. + /// + /// The items to display in the submenu. + public void OpenSubmenu(IReadOnlyList items) => + this.OnOpenSubmenu(null, items); +} diff --git a/Dalamud/Game/Gui/ContextMenu/MenuOpenedArgs.cs b/Dalamud/Game/Gui/ContextMenu/MenuOpenedArgs.cs new file mode 100644 index 000000000..de3347f63 --- /dev/null +++ b/Dalamud/Game/Gui/ContextMenu/MenuOpenedArgs.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Gui.ContextMenu; + +/// +/// Callback args used when a menu item is opened. +/// +public sealed unsafe class MenuOpenedArgs : MenuArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// Callback for adding a custom menu item. + /// Addon associated with the context menu. + /// Agent associated with the context menu. + /// The type of context menu. + /// List of AtkEventInterfaces associated with the context menu. + internal MenuOpenedArgs(Action addMenuItem, AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet eventInterfaces) + : base(addon, agent, type, eventInterfaces) + { + this.OnAddMenuItem = addMenuItem; + } + + private Action OnAddMenuItem { get; } + + /// + /// Adds a custom menu item to the context menu. + /// + /// The menu item to add. + public void AddMenuItem(MenuItem item) => + this.OnAddMenuItem(item); +} diff --git a/Dalamud/Game/Gui/ContextMenu/MenuTarget.cs b/Dalamud/Game/Gui/ContextMenu/MenuTarget.cs new file mode 100644 index 000000000..c486a3b9b --- /dev/null +++ b/Dalamud/Game/Gui/ContextMenu/MenuTarget.cs @@ -0,0 +1,9 @@ +namespace Dalamud.Game.Gui.ContextMenu; + +/// +/// Base class for contexts. +/// Discriminated based on . +/// +public abstract class MenuTarget +{ +} diff --git a/Dalamud/Game/Gui/ContextMenu/MenuTargetDefault.cs b/Dalamud/Game/Gui/ContextMenu/MenuTargetDefault.cs new file mode 100644 index 000000000..d87bc36b6 --- /dev/null +++ b/Dalamud/Game/Gui/ContextMenu/MenuTargetDefault.cs @@ -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; + +/// +/// Target information on a default context menu. +/// +public sealed unsafe class MenuTargetDefault : MenuTarget +{ + /// + /// Initializes a new instance of the class. + /// + /// The agent associated with the context menu. + internal MenuTargetDefault(AgentContext* context) + { + this.Context = context; + } + + /// + /// Gets the name of the target. + /// + public string TargetName => this.Context->TargetName.ToString(); + + /// + /// Gets the object id of the target. + /// + public ulong TargetObjectId => this.Context->TargetObjectId; + + /// + /// Gets the target object. + /// + public GameObject? TargetObject => Service.Get().SearchById(this.TargetObjectId); + + /// + /// Gets the content id of the target. + /// + public ulong TargetContentId => this.Context->TargetContentId; + + /// + /// Gets the home world id of the target. + /// + public ExcelResolver TargetHomeWorld => new((uint)this.Context->TargetHomeWorldId); + + /// + /// Gets the currently targeted character. Only shows up for specific targets, like friends, party finder listings, or party members. + /// Just because this is doesn't mean the target isn't a character. + /// + public CharacterData? TargetCharacter + { + get + { + var target = this.Context->CurrentContextMenuTarget; + if (target != null) + return new(target); + return null; + } + } + + private AgentContext* Context { get; } +} diff --git a/Dalamud/Game/Gui/ContextMenu/MenuTargetInventory.cs b/Dalamud/Game/Gui/ContextMenu/MenuTargetInventory.cs new file mode 100644 index 000000000..dee550370 --- /dev/null +++ b/Dalamud/Game/Gui/ContextMenu/MenuTargetInventory.cs @@ -0,0 +1,36 @@ +using Dalamud.Game.Inventory; + +using FFXIVClientStructs.FFXIV.Client.UI.Agent; + +namespace Dalamud.Game.Gui.ContextMenu; + +/// +/// Target information on an inventory context menu. +/// +public sealed unsafe class MenuTargetInventory : MenuTarget +{ + /// + /// Initializes a new instance of the class. + /// + /// The agent associated with the context menu. + internal MenuTargetInventory(AgentInventoryContext* context) + { + this.Context = context; + } + + /// + /// Gets the target item. + /// + public GameInventoryItem? TargetItem + { + get + { + var target = this.Context->TargetInventorySlot; + if (target != null) + return new(*target); + return null; + } + } + + private AgentInventoryContext* Context { get; } +} diff --git a/Dalamud/Game/Internal/DalamudAtkTweaks.cs b/Dalamud/Game/Internal/DalamudAtkTweaks.cs index 4eb605a76..30fab6b1b 100644 --- a/Dalamud/Game/Internal/DalamudAtkTweaks.cs +++ b/Dalamud/Game/Internal/DalamudAtkTweaks.cs @@ -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 diff --git a/Dalamud/Game/Inventory/GameInventoryItem.cs b/Dalamud/Game/Inventory/GameInventoryItem.cs index 912b91f53..d37e1081f 100644 --- a/Dalamud/Game/Inventory/GameInventoryItem.cs +++ b/Dalamud/Game/Inventory/GameInventoryItem.cs @@ -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 /// /// Gets the array of materia grades. /// + // TODO: Replace with MateriaGradeBytes + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public ReadOnlySpan MateriaGrade => - new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.MateriaGrade[0])), 5); + this.MateriaGradeBytes.ToArray().Select(g => (ushort)g).ToArray().AsSpan(); /// /// Gets the address of native inventory item in the game.
@@ -146,6 +151,9 @@ public unsafe struct GameInventoryItem : IEquatable ///
internal ulong CrafterContentId => this.InternalItem.CrafterContentID; + private ReadOnlySpan 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); diff --git a/Dalamud/Game/Network/Structures/InfoProxy/CharacterData.cs b/Dalamud/Game/Network/Structures/InfoProxy/CharacterData.cs new file mode 100644 index 000000000..0ca35d672 --- /dev/null +++ b/Dalamud/Game/Network/Structures/InfoProxy/CharacterData.cs @@ -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; + +/// +/// Dalamud wrapper around a client structs . +/// +public unsafe class CharacterData +{ + /// + /// Initializes a new instance of the class. + /// + /// Character data to wrap. + internal CharacterData(InfoProxyCommonList.CharacterData* data) + { + this.Address = (nint)data; + } + + /// + /// Gets the address of the in memory. + /// + public nint Address { get; } + + /// + /// Gets the content id of the character. + /// + public ulong ContentId => this.Struct->ContentId; + + /// + /// Gets the status mask of the character. + /// + public ulong StatusMask => (ulong)this.Struct->State; + + /// + /// Gets the applicable statues of the character. + /// + public IReadOnlyList> Statuses + { + get + { + var statuses = new List>(); + for (var i = 0; i < 64; i++) + { + if ((this.StatusMask & (1UL << i)) != 0) + statuses.Add(new((uint)i)); + } + + return statuses; + } + } + + /// + /// Gets the display group of the character. + /// + public DisplayGroup DisplayGroup => (DisplayGroup)this.Struct->Group; + + /// + /// Gets a value indicating whether the character's home world is different from the current world. + /// + public bool IsFromOtherServer => this.Struct->IsOtherServer; + + /// + /// Gets the sort order of the character. + /// + public byte Sort => this.Struct->Sort; + + /// + /// Gets the current world of the character. + /// + public ExcelResolver CurrentWorld => new(this.Struct->CurrentWorld); + + /// + /// Gets the home world of the character. + /// + public ExcelResolver HomeWorld => new(this.Struct->HomeWorld); + + /// + /// Gets the location of the character. + /// + public ExcelResolver Location => new(this.Struct->Location); + + /// + /// Gets the grand company of the character. + /// + public ExcelResolver GrandCompany => new((uint)this.Struct->GrandCompany); + + /// + /// Gets the primary client language of the character. + /// + public ClientLanguage ClientLanguage => (ClientLanguage)this.Struct->ClientLanguage; + + /// + /// Gets the supported language mask of the character. + /// + public byte LanguageMask => (byte)this.Struct->Languages; + + /// + /// Gets the supported languages the character supports. + /// + public IReadOnlyList Languages + { + get + { + var languages = new List(); + for (var i = 0; i < 4; i++) + { + if ((this.LanguageMask & (1 << i)) != 0) + languages.Add((ClientLanguage)i); + } + + return languages; + } + } + + /// + /// Gets the gender of the character. + /// + public byte Gender => this.Struct->Sex; + + /// + /// Gets the job of the character. + /// + public ExcelResolver ClassJob => new(this.Struct->Job); + + /// + /// Gets the name of the character. + /// + public string Name => MemoryHelper.ReadString((nint)this.Struct->Name, 32); + + /// + /// Gets the free company tag of the character. + /// + public string FCTag => MemoryHelper.ReadString((nint)this.Struct->Name, 6); + + /// + /// Gets the underlying struct. + /// + internal InfoProxyCommonList.CharacterData* Struct => (InfoProxyCommonList.CharacterData*)this.Address; +} + +/// +/// Display group of a character. Used for friends. +/// +public enum DisplayGroup : sbyte +{ + /// + /// All display groups. + /// + All = -1, + + /// + /// No display group. + /// + None, + + /// + /// Star display group. + /// + Star, + + /// + /// Circle display group. + /// + Circle, + + /// + /// Triangle display group. + /// + Triangle, + + /// + /// Diamond display group. + /// + Diamond, + + /// + /// Heart display group. + /// + Heart, + + /// + /// Spade display group. + /// + Spade, + + /// + /// Club display group. + /// + Club, +} diff --git a/Dalamud/Interface/FontIdentifier/DalamudAssetFontAndFamilyId.cs b/Dalamud/Interface/FontIdentifier/DalamudAssetFontAndFamilyId.cs new file mode 100644 index 000000000..a6d40e4b7 --- /dev/null +++ b/Dalamud/Interface/FontIdentifier/DalamudAssetFontAndFamilyId.cs @@ -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; + +/// +/// Represents a font from Dalamud assets. +/// +public sealed class DalamudAssetFontAndFamilyId : IFontFamilyId, IFontId +{ + /// + /// Initializes a new instance of the class. + /// + /// The font asset. + 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; + } + + /// + /// Gets the font asset. + /// + [JsonProperty] + public DalamudAsset Asset { get; init; } + + /// + [JsonIgnore] + public string EnglishName => $"Dalamud: {this.Asset}"; + + /// + [JsonIgnore] + public IReadOnlyDictionary? LocaleNames => null; + + /// + [JsonIgnore] + public IReadOnlyList Fonts => new List { this }.AsReadOnly(); + + /// + [JsonIgnore] + public IFontFamilyId Family => this; + + /// + [JsonIgnore] + public int Weight => (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL; + + /// + [JsonIgnore] + public int Stretch => (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL; + + /// + [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); + + /// + public override bool Equals(object? obj) => obj is DalamudAssetFontAndFamilyId other && this.Equals(other); + + /// + public override int GetHashCode() => (int)this.Asset; + + /// + public override string ToString() => $"{nameof(DalamudAssetFontAndFamilyId)}:{this.Asset}"; + + /// + public int FindBestMatch(int weight, int stretch, int style) => 0; + + /// + public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config) => + tk.AddDalamudAssetFont(this.Asset, config); + + private bool Equals(DalamudAssetFontAndFamilyId other) => this.Asset == other.Asset; +} diff --git a/Dalamud/Interface/FontIdentifier/DalamudDefaultFontAndFamilyId.cs b/Dalamud/Interface/FontIdentifier/DalamudDefaultFontAndFamilyId.cs new file mode 100644 index 000000000..7c6a69622 --- /dev/null +++ b/Dalamud/Interface/FontIdentifier/DalamudDefaultFontAndFamilyId.cs @@ -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; + +/// +/// Represents the default Dalamud font. +/// +public sealed class DalamudDefaultFontAndFamilyId : IFontId, IFontFamilyId +{ + /// + /// The shared instance of . + /// + public static readonly DalamudDefaultFontAndFamilyId Instance = new(); + + private DalamudDefaultFontAndFamilyId() + { + } + + /// + [JsonIgnore] + public string EnglishName => "(Default)"; + + /// + [JsonIgnore] + public IReadOnlyDictionary? LocaleNames => null; + + /// + [JsonIgnore] + public IFontFamilyId Family => this; + + /// + [JsonIgnore] + public int Weight => (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL; + + /// + [JsonIgnore] + public int Stretch => (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL; + + /// + [JsonIgnore] + public int Style => (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL; + + /// + [JsonIgnore] + public IReadOnlyList Fonts => new List { 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; + + /// + public override bool Equals(object? obj) => obj is DalamudDefaultFontAndFamilyId; + + /// + public override int GetHashCode() => 12345678; + + /// + public override string ToString() => nameof(DalamudDefaultFontAndFamilyId); + + /// + public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config) + => tk.AddDalamudDefaultFont(config.SizePx, config.GlyphRanges); + // TODO: mergeFont + + /// + public int FindBestMatch(int weight, int stretch, int style) => 0; +} diff --git a/Dalamud/Interface/FontIdentifier/GameFontAndFamilyId.cs b/Dalamud/Interface/FontIdentifier/GameFontAndFamilyId.cs new file mode 100644 index 000000000..dd4ba0d66 --- /dev/null +++ b/Dalamud/Interface/FontIdentifier/GameFontAndFamilyId.cs @@ -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; + +/// +/// Represents a font from the game. +/// +public sealed class GameFontAndFamilyId : IFontId, IFontFamilyId +{ + /// + /// Initializes a new instance of the class. + /// + /// The game font family. + public GameFontAndFamilyId(GameFontFamily family) => this.GameFontFamily = family; + + /// + /// Gets the game font family. + /// + [JsonProperty] + public GameFontFamily GameFontFamily { get; init; } + + /// + [JsonIgnore] + public string EnglishName => $"Game: {Enum.GetName(this.GameFontFamily) ?? throw new NotSupportedException()}"; + + /// + [JsonIgnore] + public IReadOnlyDictionary? LocaleNames => null; + + /// + [JsonIgnore] + public IFontFamilyId Family => this; + + /// + [JsonIgnore] + public int Weight => (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL; + + /// + [JsonIgnore] + public int Stretch => (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL; + + /// + [JsonIgnore] + public int Style => (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL; + + /// + [JsonIgnore] + public IReadOnlyList Fonts => new List { this }.AsReadOnly(); + + public static bool operator ==(GameFontAndFamilyId? left, GameFontAndFamilyId? right) => Equals(left, right); + + public static bool operator !=(GameFontAndFamilyId? left, GameFontAndFamilyId? right) => !Equals(left, right); + + /// + public override bool Equals(object? obj) => + ReferenceEquals(this, obj) || (obj is GameFontAndFamilyId other && this.Equals(other)); + + /// + public override int GetHashCode() => (int)this.GameFontFamily; + + /// + public int FindBestMatch(int weight, int stretch, int style) => 0; + + /// + public override string ToString() => $"{nameof(GameFontAndFamilyId)}:{this.GameFontFamily}"; + + /// + 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; +} diff --git a/Dalamud/Interface/FontIdentifier/IFontFamilyId.cs b/Dalamud/Interface/FontIdentifier/IFontFamilyId.cs new file mode 100644 index 000000000..991716f74 --- /dev/null +++ b/Dalamud/Interface/FontIdentifier/IFontFamilyId.cs @@ -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; + +/// +/// Represents a font family identifier.
+/// Not intended for plugins to implement. +///
+public interface IFontFamilyId : IObjectWithLocalizableName +{ + /// + /// Gets the list of fonts under this family. + /// + [JsonIgnore] + IReadOnlyList Fonts { get; } + + /// + /// Finds the index of the font inside that best matches the given parameters. + /// + /// The weight of the font. + /// The stretch of the font. + /// The style of the font. + /// The index of the font. Guaranteed to be a valid index. + int FindBestMatch(int weight, int stretch, int style); + + /// + /// Gets the list of Dalamud-provided fonts. + /// + /// The list of fonts. + public static List ListDalamudFonts() => + new() + { + new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansJpMedium), + new DalamudAssetFontAndFamilyId(DalamudAsset.InconsolataRegular), + new DalamudAssetFontAndFamilyId(DalamudAsset.FontAwesomeFreeSolid), + }; + + /// + /// Gets the list of Game-provided fonts. + /// + /// The list of fonts. + public static List 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), + }; + + /// + /// Gets the list of System-provided fonts. + /// + /// If true, try to refresh the list. + /// The list of fonts. + public static unsafe List ListSystemFonts(bool refresh) + { + using var dwf = default(ComPtr); + 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); + dwf.Get()->GetSystemFontCollection(sfc.GetAddressOf(), refresh).ThrowOnError(); + + var count = (int)sfc.Get()->GetFontFamilyCount(); + var result = new List(count); + for (var i = 0; i < count; i++) + { + using var ff = default(ComPtr); + if (sfc.Get()->GetFontFamily((uint)i, ff.GetAddressOf()).FAILED) + { + // Ignore errors, if any + continue; + } + + try + { + result.Add(SystemFontFamilyId.FromDWriteFamily(ff)); + } + catch + { + // ignore + } + } + + return result; + } +} diff --git a/Dalamud/Interface/FontIdentifier/IFontId.cs b/Dalamud/Interface/FontIdentifier/IFontId.cs new file mode 100644 index 000000000..4c611edf8 --- /dev/null +++ b/Dalamud/Interface/FontIdentifier/IFontId.cs @@ -0,0 +1,40 @@ +using Dalamud.Interface.ManagedFontAtlas; + +using ImGuiNET; + +namespace Dalamud.Interface.FontIdentifier; + +/// +/// Represents a font identifier.
+/// Not intended for plugins to implement. +///
+public interface IFontId : IObjectWithLocalizableName +{ + /// + /// Gets the associated font family. + /// + IFontFamilyId Family { get; } + + /// + /// Gets the font weight, ranging from 1 to 999. + /// + int Weight { get; } + + /// + /// Gets the font stretch, ranging from 1 to 9. + /// + int Stretch { get; } + + /// + /// Gets the font style. Treat as an opaque value. + /// + int Style { get; } + + /// + /// Adds this font to the given font build toolkit. + /// + /// The font build toolkit. + /// The font configuration. Some parameters may be ignored. + /// The added font. + ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config); +} diff --git a/Dalamud/Interface/FontIdentifier/IFontSpec.cs b/Dalamud/Interface/FontIdentifier/IFontSpec.cs new file mode 100644 index 000000000..4d0719d4e --- /dev/null +++ b/Dalamud/Interface/FontIdentifier/IFontSpec.cs @@ -0,0 +1,52 @@ +using Dalamud.Interface.ManagedFontAtlas; + +using ImGuiNET; + +namespace Dalamud.Interface.FontIdentifier; + +/// +/// Represents a user's choice of font(s).
+/// Not intended for plugins to implement. +///
+public interface IFontSpec +{ + /// + /// Gets the font size in pixels. + /// + float SizePx { get; } + + /// + /// Gets the font size in points. + /// + float SizePt { get; } + + /// + /// Gets the line height in pixels. + /// + float LineHeightPx { get; } + + /// + /// Creates a font handle corresponding to this font specification. + /// + /// The atlas to bind this font handle to. + /// Optional callback to be called after creating the font handle. + /// The new font handle. + /// will be set when is invoked. + /// + IFontHandle CreateFontHandle(IFontAtlas atlas, FontAtlasBuildStepDelegate? callback = null); + + /// + /// Adds this font to the given font build toolkit. + /// + /// The font build toolkit. + /// The font to merge to. + /// The added font. + ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, ImFontPtr mergeFont = default); + + /// + /// Represents this font specification, preferrably in the requested locale. + /// + /// The locale code. Must be in lowercase(invariant). + /// The value. + string ToLocalizedString(string localeCode); +} diff --git a/Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs b/Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs new file mode 100644 index 000000000..2b970a5fd --- /dev/null +++ b/Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; + +using Dalamud.Utility; + +using TerraFX.Interop.DirectX; + +namespace Dalamud.Interface.FontIdentifier; + +/// +/// Represents an object with localizable names. +/// +public interface IObjectWithLocalizableName +{ + /// + /// Gets the name, preferrably in English. + /// + string EnglishName { get; } + + /// + /// Gets the names per locales. + /// + IReadOnlyDictionary? LocaleNames { get; } + + /// + /// Gets the name in the requested locale if available; otherwise, . + /// + /// The locale code. Must be in lowercase(invariant). + /// The value. + 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; + } + + /// + /// Resolves all names per locales. + /// + /// The names. + /// A new dictionary mapping from locale code to localized names. + internal static unsafe IReadOnlyDictionary 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((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; + } +} diff --git a/Dalamud/Interface/FontIdentifier/SingleFontSpec.cs b/Dalamud/Interface/FontIdentifier/SingleFontSpec.cs new file mode 100644 index 000000000..946215b85 --- /dev/null +++ b/Dalamud/Interface/FontIdentifier/SingleFontSpec.cs @@ -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; + +/// +/// Represents a user's choice of a single font. +/// +[SuppressMessage( + "StyleCop.CSharp.OrderingRules", + "SA1206:Declaration keywords should follow order", + Justification = "public required")] +public record SingleFontSpec : IFontSpec +{ + /// + /// Gets the font id. + /// + [JsonProperty] + public required IFontId FontId { get; init; } + + /// + [JsonProperty] + public float SizePx { get; init; } = 16; + + /// + [JsonIgnore] + public float SizePt + { + get => (this.SizePx * 3) / 4; + init => this.SizePx = (value * 4) / 3; + } + + /// + [JsonIgnore] + public float LineHeightPx => MathF.Round(this.SizePx * this.LineHeight); + + /// + /// Gets the line height ratio to the font size. + /// + [JsonProperty] + public float LineHeight { get; init; } = 1f; + + /// + /// Gets the glyph offset in pixels. + /// + [JsonProperty] + public Vector2 GlyphOffset { get; init; } + + /// + /// Gets the letter spacing in pixels. + /// + [JsonProperty] + public float LetterSpacing { get; init; } + + /// + /// Gets the glyph ranges. + /// + [JsonProperty] + public ushort[]? GlyphRanges { get; init; } + + /// + 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(); + } + + /// + public override string ToString() => this.ToLocalizedString("en"); + + /// + public IFontHandle CreateFontHandle(IFontAtlas atlas, FontAtlasBuildStepDelegate? callback = null) => + atlas.NewDelegateFontHandle(tk => + { + tk.OnPreBuild(e => e.Font = this.AddToBuildToolkit(e)); + callback?.Invoke(tk); + }); + + /// + 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; + } +} diff --git a/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs b/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs new file mode 100644 index 000000000..420ee77a4 --- /dev/null +++ b/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs @@ -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; + +/// +/// Represents a font from system. +/// +public sealed class SystemFontFamilyId : IFontFamilyId +{ + [JsonIgnore] + private IReadOnlyList? fontsLazy; + + /// + /// Initializes a new instance of the class. + /// + /// The font name in English. + /// The localized font name for display purposes. + [JsonConstructor] + internal SystemFontFamilyId(string englishName, IReadOnlyDictionary localeNames) + { + this.EnglishName = englishName; + this.LocaleNames = localeNames; + } + + /// + /// Initializes a new instance of the class. + /// + /// The localized font name for display purposes. + internal SystemFontFamilyId(IReadOnlyDictionary 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; + } + + /// + [JsonProperty] + public string EnglishName { get; init; } + + /// + [JsonProperty] + public IReadOnlyDictionary? LocaleNames { get; } + + /// + [JsonIgnore] + public IReadOnlyList 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); + + /// + public int FindBestMatch(int weight, int stretch, int style) + { + using var matchingFont = default(ComPtr); + + 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; + } + + /// + public override string ToString() => $"{nameof(SystemFontFamilyId)}:{this.EnglishName}"; + + /// + public override bool Equals(object? obj) => + ReferenceEquals(this, obj) || (obj is SystemFontFamilyId other && this.Equals(other)); + + /// + public override int GetHashCode() => this.EnglishName.GetHashCode(); + + /// + /// Create a new instance of from an . + /// + /// The family. + /// The new instance. + internal static unsafe SystemFontFamilyId FromDWriteFamily(ComPtr family) + { + using var fn = default(ComPtr); + family.Get()->GetFamilyNames(fn.GetAddressOf()).ThrowOnError(); + return new(IObjectWithLocalizableName.GetLocaleNames(fn)); + } + + private unsafe IReadOnlyList GetFonts() + { + using var dwf = default(ComPtr); + 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); + 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); + sfc.Get()->GetFontFamily(familyIndex, family.GetAddressOf()).ThrowOnError(); + + var fontCount = (int)family.Get()->GetFontCount(); + var fonts = new List(fontCount); + for (var i = 0; i < fontCount; i++) + { + using var font = default(ComPtr); + 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; +} diff --git a/Dalamud/Interface/FontIdentifier/SystemFontId.cs b/Dalamud/Interface/FontIdentifier/SystemFontId.cs new file mode 100644 index 000000000..0a350fc3a --- /dev/null +++ b/Dalamud/Interface/FontIdentifier/SystemFontId.cs @@ -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; + +/// +/// Represents a font installed in the system. +/// +public sealed class SystemFontId : IFontId +{ + /// + /// Initializes a new instance of the class. + /// + /// The parent font family. + /// The font. + internal unsafe SystemFontId(SystemFontFamilyId family, ComPtr 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); + 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 localeNames, IFontFamilyId family) + { + this.EnglishName = englishName; + this.LocaleNames = localeNames; + this.Family = family; + } + + /// + [JsonProperty] + public string EnglishName { get; init; } + + /// + [JsonProperty] + public IReadOnlyDictionary? LocaleNames { get; } + + /// + [JsonProperty] + public IFontFamilyId Family { get; init; } + + /// + [JsonProperty] + public int Weight { get; init; } = (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL; + + /// + [JsonProperty] + public int Stretch { get; init; } = (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL; + + /// + [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); + + /// + public override bool Equals(object? obj) => + ReferenceEquals(this, obj) || (obj is SystemFontId other && this.Equals(other)); + + /// + public override int GetHashCode() => HashCode.Combine(this.Family, this.Weight, this.Stretch, this.Style); + + /// + public override string ToString() => + $"{nameof(SystemFontId)}:{this.Weight}:{this.Stretch}:{this.Style}:{this.Family}"; + + /// + public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config) + { + var (path, index) = this.GetFileAndIndex(); + return tk.AddFontFromFile(path, config with { FontNo = index }); + } + + /// + /// Gets the file containing this font, and the font index within. + /// + /// The path and index. + public unsafe (string Path, int Index) GetFileAndIndex() + { + using var dwf = default(ComPtr); + 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); + 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); + sfc.Get()->GetFontFamily(familyIndex, family.GetAddressOf()).ThrowOnError(); + + using var font = default(ComPtr); + 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); + 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); + fface.Get()->GetFiles((uint*)&fileCount, ffile.GetAddressOf()).ThrowOnError(); + void* refKey; + var refKeySize = 0u; + ffile.Get()->GetReferenceKey(&refKey, &refKeySize).ThrowOnError(); + + using var floader = default(ComPtr); + ffile.Get()->GetLoader(floader.GetAddressOf()).ThrowOnError(); + + using var flocal = default(ComPtr); + 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; +} diff --git a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs new file mode 100644 index 000000000..ca75e5ce0 --- /dev/null +++ b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs @@ -0,0 +1,1115 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Configuration.Internal; +using Dalamud.Interface.Colors; +using Dalamud.Interface.FontIdentifier; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.Utility; +using Dalamud.Utility; + +using ImGuiNET; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.ImGuiFontChooserDialog; + +/// +/// A dialog for choosing a font and its size. +/// +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] +public sealed class SingleFontChooserDialog : IDisposable +{ + private const float MinFontSizePt = 1; + + private const float MaxFontSizePt = 127; + + private static readonly List EmptyIFontList = new(); + + private static readonly (string Name, float Value)[] FontSizeList = + { + ("9.6", 9.6f), + ("10", 10f), + ("12", 12f), + ("14", 14f), + ("16", 16f), + ("18", 18f), + ("18.4", 18.4f), + ("20", 20), + ("23", 23), + ("34", 34), + ("36", 36), + ("40", 40), + ("45", 45), + ("46", 46), + ("68", 68), + ("90", 90), + }; + + private static int counterStatic; + + private readonly int counter; + private readonly byte[] fontPreviewText = new byte[2048]; + private readonly TaskCompletionSource tcs = new(); + private readonly IFontAtlas atlas; + + private string popupImGuiName; + private string title; + + private bool firstDraw = true; + private bool firstDrawAfterRefresh; + private int setFocusOn = -1; + + private bool useAdvancedOptions; + private AdvancedOptionsUiState advUiState; + + private Task>? fontFamilies; + private int selectedFamilyIndex = -1; + private int selectedFontIndex = -1; + private int selectedFontWeight = (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL; + private int selectedFontStretch = (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL; + private int selectedFontStyle = (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL; + + private string familySearch = string.Empty; + private string fontSearch = string.Empty; + private string fontSizeSearch = "12"; + private IFontHandle? fontHandle; + private SingleFontSpec selectedFont; + + /// + /// Initializes a new instance of the class. + /// + /// A new instance of created using + /// as its auto-rebuild mode. + public SingleFontChooserDialog(IFontAtlas newAsyncAtlas) + { + this.counter = Interlocked.Increment(ref counterStatic); + this.title = "Choose a font..."; + this.popupImGuiName = $"{this.title}##{nameof(SingleFontChooserDialog)}[{this.counter}]"; + this.atlas = newAsyncAtlas; + this.selectedFont = new() { FontId = DalamudDefaultFontAndFamilyId.Instance }; + Encoding.UTF8.GetBytes("Font preview.\n0123456789!", this.fontPreviewText); + } + + /// + /// Gets or sets the title of this font chooser dialog popup. + /// + public string Title + { + get => this.title; + set + { + this.title = value; + this.popupImGuiName = $"{this.title}##{nameof(SingleFontChooserDialog)}[{this.counter}]"; + } + } + + /// + /// Gets or sets the preview text. A text too long may be truncated on assignment. + /// + public string PreviewText + { + get + { + var n = this.fontPreviewText.AsSpan().IndexOf((byte)0); + return n < 0 + ? Encoding.UTF8.GetString(this.fontPreviewText) + : Encoding.UTF8.GetString(this.fontPreviewText, 0, n); + } + set => Encoding.UTF8.GetBytes(value, this.fontPreviewText); + } + + /// + /// Gets the task that resolves upon choosing a font or cancellation. + /// + public Task ResultTask => this.tcs.Task; + + /// + /// Gets or sets the selected family and font. + /// + public SingleFontSpec SelectedFont + { + get => this.selectedFont; + set + { + this.selectedFont = value; + + var familyName = value.FontId.Family.ToString() ?? string.Empty; + var fontName = value.FontId.ToString() ?? string.Empty; + this.familySearch = this.ExtractName(value.FontId.Family); + this.fontSearch = this.ExtractName(value.FontId); + if (this.fontFamilies?.IsCompletedSuccessfully is true) + this.UpdateSelectedFamilyAndFontIndices(this.fontFamilies.Result, familyName, fontName); + this.fontSizeSearch = $"{value.SizePt:0.##}"; + this.advUiState = new(value); + this.useAdvancedOptions |= Math.Abs(value.LineHeight - 1f) > 0.000001; + this.useAdvancedOptions |= value.GlyphOffset != default; + this.useAdvancedOptions |= value.LetterSpacing != 0f; + } + } + + /// + /// Gets or sets the font family exclusion filter predicate. + /// + public Predicate? FontFamilyExcludeFilter { get; set; } + + /// + /// Gets or sets a value indicating whether to ignore the global scale on preview text input. + /// + public bool IgnorePreviewGlobalScale { get; set; } + + /// + /// Creates a new instance of that will automatically draw and dispose itself as + /// needed. + /// + /// An instance of . + /// The new instance of . + public static SingleFontChooserDialog CreateAuto(UiBuilder uiBuilder) + { + var fcd = new SingleFontChooserDialog(uiBuilder.CreateFontAtlas(FontAtlasAutoRebuildMode.Async)); + uiBuilder.Draw += fcd.Draw; + fcd.tcs.Task.ContinueWith( + r => + { + _ = r.Exception; + uiBuilder.Draw -= fcd.Draw; + fcd.Dispose(); + }); + + return fcd; + } + + /// + public void Dispose() + { + this.fontHandle?.Dispose(); + this.atlas.Dispose(); + } + + /// + /// Cancels this dialog. + /// + public void Cancel() + { + this.tcs.SetCanceled(); + ImGui.GetIO().WantCaptureKeyboard = false; + ImGui.GetIO().WantTextInput = false; + } + + /// + /// Draws this dialog. + /// + public void Draw() + { + if (this.firstDraw) + ImGui.OpenPopup(this.popupImGuiName); + + ImGui.GetIO().WantCaptureKeyboard = true; + ImGui.GetIO().WantTextInput = true; + if (ImGui.IsKeyPressed(ImGuiKey.Escape)) + { + this.Cancel(); + return; + } + + var open = true; + ImGui.SetNextWindowSize(new(640, 480), ImGuiCond.Appearing); + if (!ImGui.BeginPopupModal(this.popupImGuiName, ref open) || !open) + { + this.Cancel(); + return; + } + + var framePad = ImGui.GetStyle().FramePadding; + var windowPad = ImGui.GetStyle().WindowPadding; + var baseOffset = ImGui.GetCursorPos() - windowPad; + + var actionSize = Vector2.Zero; + actionSize = Vector2.Max(actionSize, ImGui.CalcTextSize("OK")); + actionSize = Vector2.Max(actionSize, ImGui.CalcTextSize("Cancel")); + actionSize = Vector2.Max(actionSize, ImGui.CalcTextSize("Refresh")); + actionSize = Vector2.Max(actionSize, ImGui.CalcTextSize("Reset")); + actionSize += framePad * 2; + + var bodySize = ImGui.GetContentRegionAvail(); + ImGui.SetCursorPos(baseOffset + windowPad); + if (ImGui.BeginChild( + "##choicesBlock", + bodySize with { X = bodySize.X - windowPad.X - actionSize.X }, + false, + ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)) + { + this.DrawChoices(); + } + + ImGui.EndChild(); + + ImGui.SetCursorPos(baseOffset + windowPad + new Vector2(bodySize.X - actionSize.X, 0)); + + if (ImGui.BeginChild("##actionsBlock", bodySize with { X = actionSize.X })) + { + this.DrawActionButtons(actionSize); + } + + ImGui.EndChild(); + + ImGui.EndPopup(); + + this.firstDraw = false; + this.firstDrawAfterRefresh = false; + } + + private void DrawChoices() + { + var lineHeight = ImGui.GetTextLineHeight(); + var previewHeight = (ImGui.GetFrameHeightWithSpacing() - lineHeight) + + Math.Max(lineHeight, this.selectedFont.LineHeightPx * 2); + + var advancedOptionsHeight = ImGui.GetFrameHeightWithSpacing() * (this.useAdvancedOptions ? 4 : 1); + + var tableSize = ImGui.GetContentRegionAvail() - + new Vector2(0, ImGui.GetStyle().WindowPadding.Y + previewHeight + advancedOptionsHeight); + if (ImGui.BeginChild( + "##tableContainer", + tableSize, + false, + ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse) + && ImGui.BeginTable("##table", 3, ImGuiTableFlags.None)) + { + ImGui.PushStyleColor(ImGuiCol.TableHeaderBg, Vector4.Zero); + ImGui.PushStyleColor(ImGuiCol.HeaderHovered, Vector4.Zero); + ImGui.PushStyleColor(ImGuiCol.HeaderActive, Vector4.Zero); + ImGui.TableSetupColumn( + "Font:##familyColumn", + ImGuiTableColumnFlags.WidthStretch, + 0.4f); + ImGui.TableSetupColumn( + "Style:##fontColumn", + ImGuiTableColumnFlags.WidthStretch, + 0.4f); + ImGui.TableSetupColumn( + "Size:##sizeColumn", + ImGuiTableColumnFlags.WidthStretch, + 0.2f); + ImGui.TableHeadersRow(); + ImGui.PopStyleColor(3); + + ImGui.TableNextRow(); + + var pad = (int)MathF.Round(8 * ImGuiHelpers.GlobalScale); + ImGui.PushStyleVar(ImGuiStyleVar.CellPadding, new Vector2(pad)); + ImGui.TableNextColumn(); + var changed = this.DrawFamilyListColumn(); + + ImGui.TableNextColumn(); + changed |= this.DrawFontListColumn(changed); + + ImGui.TableNextColumn(); + changed |= this.DrawSizeListColumn(); + + if (changed) + { + this.fontHandle?.Dispose(); + this.fontHandle = null; + } + + ImGui.PopStyleVar(); + + ImGui.EndTable(); + } + + ImGui.EndChild(); + + ImGui.Checkbox("Show advanced options", ref this.useAdvancedOptions); + if (this.useAdvancedOptions) + { + if (this.DrawAdvancedOptions()) + { + this.fontHandle?.Dispose(); + this.fontHandle = null; + } + } + + if (this.IgnorePreviewGlobalScale) + { + this.fontHandle ??= this.selectedFont.CreateFontHandle( + this.atlas, + tk => tk.OnPreBuild(e => e.SetFontScaleMode(e.Font, FontScaleMode.UndoGlobalScale))); + } + else + { + this.fontHandle ??= this.selectedFont.CreateFontHandle(this.atlas); + } + + if (this.fontHandle is null) + { + ImGui.SetCursorPos(ImGui.GetCursorPos() + ImGui.GetStyle().FramePadding); + ImGui.TextUnformatted("Select a font."); + } + else if (this.fontHandle.LoadException is { } loadException) + { + ImGui.SetCursorPos(ImGui.GetCursorPos() + ImGui.GetStyle().FramePadding); + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + ImGui.TextUnformatted(loadException.Message); + ImGui.PopStyleColor(); + } + else if (!this.fontHandle.Available) + { + ImGui.SetCursorPos(ImGui.GetCursorPos() + ImGui.GetStyle().FramePadding); + ImGui.TextUnformatted("Loading font..."); + } + else + { + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + using (this.fontHandle?.Push()) + { + unsafe + { + fixed (byte* buf = this.fontPreviewText) + fixed (byte* label = "##fontPreviewText"u8) + { + ImGuiNative.igInputTextMultiline( + label, + buf, + (uint)this.fontPreviewText.Length, + ImGui.GetContentRegionAvail(), + ImGuiInputTextFlags.None, + null, + null); + } + } + } + } + } + + private unsafe bool DrawFamilyListColumn() + { + if (this.fontFamilies?.IsCompleted is not true) + { + ImGui.SetScrollY(0); + ImGui.TextUnformatted("Loading..."); + return false; + } + + if (!this.fontFamilies.IsCompletedSuccessfully) + { + ImGui.SetScrollY(0); + ImGui.TextUnformatted("Error: " + this.fontFamilies.Exception); + return false; + } + + var families = this.fontFamilies.Result; + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + + if (this.setFocusOn == 0) + { + this.setFocusOn = -1; + ImGui.SetKeyboardFocusHere(); + } + + var changed = false; + if (ImGui.InputText( + "##familySearch", + ref this.familySearch, + 255, + ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CallbackHistory, + data => + { + if (families.Count == 0) + return 0; + + var baseIndex = this.selectedFamilyIndex; + if (data->SelectionStart == 0 && data->SelectionEnd == data->BufTextLen) + { + switch (data->EventKey) + { + case ImGuiKey.DownArrow: + this.selectedFamilyIndex = (this.selectedFamilyIndex + 1) % families.Count; + changed = true; + break; + case ImGuiKey.UpArrow: + this.selectedFamilyIndex = + (this.selectedFamilyIndex + families.Count - 1) % families.Count; + changed = true; + break; + } + + if (changed) + { + ImGuiHelpers.SetTextFromCallback( + data, + this.ExtractName(families[this.selectedFamilyIndex])); + } + } + else + { + switch (data->EventKey) + { + case ImGuiKey.DownArrow: + this.selectedFamilyIndex = families.FindIndex( + baseIndex + 1, + x => this.TestName(x, this.familySearch)); + if (this.selectedFamilyIndex < 0) + { + this.selectedFamilyIndex = families.FindIndex( + 0, + baseIndex + 1, + x => this.TestName(x, this.familySearch)); + } + + changed = true; + break; + case ImGuiKey.UpArrow: + if (baseIndex > 0) + { + this.selectedFamilyIndex = families.FindLastIndex( + baseIndex - 1, + x => this.TestName(x, this.familySearch)); + } + + if (this.selectedFamilyIndex < 0) + { + if (baseIndex < 0) + baseIndex = 0; + this.selectedFamilyIndex = families.FindLastIndex( + families.Count - 1, + families.Count - baseIndex, + x => this.TestName(x, this.familySearch)); + } + + changed = true; + break; + } + } + + return 0; + })) + { + if (!string.IsNullOrWhiteSpace(this.familySearch) && !changed) + { + this.selectedFamilyIndex = families.FindIndex(x => this.TestName(x, this.familySearch)); + changed = true; + } + } + + if (ImGui.BeginChild("##familyList", ImGui.GetContentRegionAvail())) + { + var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); + var lineHeight = ImGui.GetTextLineHeightWithSpacing(); + + if ((changed || this.firstDrawAfterRefresh) && this.selectedFamilyIndex != -1) + { + ImGui.SetScrollY( + (lineHeight * this.selectedFamilyIndex) - + ((ImGui.GetContentRegionAvail().Y - lineHeight) / 2)); + } + + clipper.Begin(families.Count, lineHeight); + while (clipper.Step()) + { + for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + { + if (i < 0) + { + ImGui.TextUnformatted(" "); + continue; + } + + var selected = this.selectedFamilyIndex == i; + if (ImGui.Selectable( + this.ExtractName(families[i]), + ref selected, + ImGuiSelectableFlags.DontClosePopups)) + { + this.selectedFamilyIndex = families.IndexOf(families[i]); + this.familySearch = this.ExtractName(families[i]); + this.setFocusOn = 0; + changed = true; + } + } + } + + clipper.Destroy(); + } + + if (changed && this.selectedFamilyIndex >= 0) + { + var family = families[this.selectedFamilyIndex]; + using var matchingFont = default(ComPtr); + this.selectedFontIndex = family.FindBestMatch( + this.selectedFontWeight, + this.selectedFontStretch, + this.selectedFontStyle); + this.selectedFont = this.selectedFont with { FontId = family.Fonts[this.selectedFontIndex] }; + } + + ImGui.EndChild(); + return changed; + } + + private unsafe bool DrawFontListColumn(bool changed) + { + if (this.fontFamilies?.IsCompleted is not true) + { + ImGui.TextUnformatted("Loading..."); + return changed; + } + + if (!this.fontFamilies.IsCompletedSuccessfully) + { + ImGui.TextUnformatted("Error: " + this.fontFamilies.Exception); + return changed; + } + + var families = this.fontFamilies.Result; + var family = this.selectedFamilyIndex >= 0 + && this.selectedFamilyIndex < families.Count + ? families[this.selectedFamilyIndex] + : null; + var fonts = family?.Fonts ?? EmptyIFontList; + + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + + if (this.setFocusOn == 1) + { + this.setFocusOn = -1; + ImGui.SetKeyboardFocusHere(); + } + + if (ImGui.InputText( + "##fontSearch", + ref this.fontSearch, + 255, + ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CallbackHistory, + data => + { + if (fonts.Count == 0) + return 0; + + var baseIndex = this.selectedFontIndex; + if (data->SelectionStart == 0 && data->SelectionEnd == data->BufTextLen) + { + switch (data->EventKey) + { + case ImGuiKey.DownArrow: + this.selectedFontIndex = (this.selectedFontIndex + 1) % fonts.Count; + changed = true; + break; + case ImGuiKey.UpArrow: + this.selectedFontIndex = (this.selectedFontIndex + fonts.Count - 1) % fonts.Count; + changed = true; + break; + } + + if (changed) + { + ImGuiHelpers.SetTextFromCallback( + data, + this.ExtractName(fonts[this.selectedFontIndex])); + } + } + else + { + switch (data->EventKey) + { + case ImGuiKey.DownArrow: + this.selectedFontIndex = fonts.FindIndex( + baseIndex + 1, + x => this.TestName(x, this.fontSearch)); + if (this.selectedFontIndex < 0) + { + this.selectedFontIndex = fonts.FindIndex( + 0, + baseIndex + 1, + x => this.TestName(x, this.fontSearch)); + } + + changed = true; + break; + case ImGuiKey.UpArrow: + if (baseIndex > 0) + { + this.selectedFontIndex = fonts.FindLastIndex( + baseIndex - 1, + x => this.TestName(x, this.fontSearch)); + } + + if (this.selectedFontIndex < 0) + { + if (baseIndex < 0) + baseIndex = 0; + this.selectedFontIndex = fonts.FindLastIndex( + fonts.Count - 1, + fonts.Count - baseIndex, + x => this.TestName(x, this.fontSearch)); + } + + changed = true; + break; + } + } + + return 0; + })) + { + if (!string.IsNullOrWhiteSpace(this.fontSearch) && !changed) + { + this.selectedFontIndex = fonts.FindIndex(x => this.TestName(x, this.fontSearch)); + changed = true; + } + } + + if (ImGui.BeginChild("##fontList")) + { + var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); + var lineHeight = ImGui.GetTextLineHeightWithSpacing(); + + if ((changed || this.firstDrawAfterRefresh) && this.selectedFontIndex != -1) + { + ImGui.SetScrollY( + (lineHeight * this.selectedFontIndex) - + ((ImGui.GetContentRegionAvail().Y - lineHeight) / 2)); + } + + clipper.Begin(fonts.Count, lineHeight); + while (clipper.Step()) + { + for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + { + if (i < 0) + { + ImGui.TextUnformatted(" "); + continue; + } + + var selected = this.selectedFontIndex == i; + if (ImGui.Selectable( + this.ExtractName(fonts[i]), + ref selected, + ImGuiSelectableFlags.DontClosePopups)) + { + this.selectedFontIndex = fonts.IndexOf(fonts[i]); + this.fontSearch = this.ExtractName(fonts[i]); + this.setFocusOn = 1; + changed = true; + } + } + } + + clipper.Destroy(); + } + + ImGui.EndChild(); + + if (changed && family is not null && this.selectedFontIndex >= 0) + { + var font = family.Fonts[this.selectedFontIndex]; + this.selectedFontWeight = font.Weight; + this.selectedFontStretch = font.Stretch; + this.selectedFontStyle = font.Style; + this.selectedFont = this.selectedFont with { FontId = font }; + } + + return changed; + } + + private unsafe bool DrawSizeListColumn() + { + var changed = false; + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + + if (this.setFocusOn == 2) + { + this.setFocusOn = -1; + ImGui.SetKeyboardFocusHere(); + } + + if (ImGui.InputText( + "##fontSizeSearch", + ref this.fontSizeSearch, + 255, + ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CallbackHistory | + ImGuiInputTextFlags.CharsDecimal, + data => + { + switch (data->EventKey) + { + case ImGuiKey.DownArrow: + this.selectedFont = this.selectedFont with + { + SizePt = Math.Min(MaxFontSizePt, MathF.Floor(this.selectedFont.SizePt) + 1), + }; + changed = true; + break; + case ImGuiKey.UpArrow: + this.selectedFont = this.selectedFont with + { + SizePt = Math.Max(MinFontSizePt, MathF.Ceiling(this.selectedFont.SizePt) - 1), + }; + changed = true; + break; + } + + if (changed) + ImGuiHelpers.SetTextFromCallback(data, $"{this.selectedFont.SizePt:0.##}"); + + return 0; + })) + { + if (float.TryParse(this.fontSizeSearch, out var fontSizePt1)) + { + this.selectedFont = this.selectedFont with { SizePt = fontSizePt1 }; + changed = true; + } + } + + if (ImGui.BeginChild("##fontSizeList")) + { + var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); + var lineHeight = ImGui.GetTextLineHeightWithSpacing(); + + if (changed && this.selectedFontIndex != -1) + { + ImGui.SetScrollY( + (lineHeight * this.selectedFontIndex) - + ((ImGui.GetContentRegionAvail().Y - lineHeight) / 2)); + } + + clipper.Begin(FontSizeList.Length, lineHeight); + while (clipper.Step()) + { + for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + { + if (i < 0) + { + ImGui.TextUnformatted(" "); + continue; + } + + var selected = Equals(FontSizeList[i].Value, this.selectedFont.SizePt); + if (ImGui.Selectable( + FontSizeList[i].Name, + ref selected, + ImGuiSelectableFlags.DontClosePopups)) + { + this.selectedFont = this.selectedFont with { SizePt = FontSizeList[i].Value }; + this.setFocusOn = 2; + changed = true; + } + } + } + + clipper.Destroy(); + } + + ImGui.EndChild(); + + if (this.selectedFont.SizePt < MinFontSizePt) + { + this.selectedFont = this.selectedFont with { SizePt = MinFontSizePt }; + changed = true; + } + + if (this.selectedFont.SizePt > MaxFontSizePt) + { + this.selectedFont = this.selectedFont with { SizePt = MaxFontSizePt }; + changed = true; + } + + if (changed) + this.fontSizeSearch = $"{this.selectedFont.SizePt:0.##}"; + + return changed; + } + + private bool DrawAdvancedOptions() + { + var changed = false; + + if (!ImGui.BeginTable("##advancedOptions", 4)) + return false; + + var labelWidth = ImGui.CalcTextSize("Letter Spacing:").X; + labelWidth = Math.Max(labelWidth, ImGui.CalcTextSize("Offset:").X); + labelWidth = Math.Max(labelWidth, ImGui.CalcTextSize("Line Height:").X); + labelWidth += ImGui.GetStyle().FramePadding.X; + + var inputWidth = ImGui.CalcTextSize("000.000").X + (ImGui.GetStyle().FramePadding.X * 2); + ImGui.TableSetupColumn( + "##inputLabelColumn", + ImGuiTableColumnFlags.WidthFixed, + labelWidth); + ImGui.TableSetupColumn( + "##input1Column", + ImGuiTableColumnFlags.WidthFixed, + inputWidth); + ImGui.TableSetupColumn( + "##input2Column", + ImGuiTableColumnFlags.WidthFixed, + inputWidth); + ImGui.TableSetupColumn( + "##fillerColumn", + ImGuiTableColumnFlags.WidthStretch, + 1f); + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Offset:"); + + ImGui.TableNextColumn(); + if (FloatInputText( + "##glyphOffsetXInput", + ref this.advUiState.OffsetXText, + this.selectedFont.GlyphOffset.X) is { } newGlyphOffsetX) + { + changed = true; + this.selectedFont = this.selectedFont with + { + GlyphOffset = this.selectedFont.GlyphOffset with { X = newGlyphOffsetX }, + }; + } + + ImGui.TableNextColumn(); + if (FloatInputText( + "##glyphOffsetYInput", + ref this.advUiState.OffsetYText, + this.selectedFont.GlyphOffset.Y) is { } newGlyphOffsetY) + { + changed = true; + this.selectedFont = this.selectedFont with + { + GlyphOffset = this.selectedFont.GlyphOffset with { Y = newGlyphOffsetY }, + }; + } + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Letter Spacing:"); + + ImGui.TableNextColumn(); + if (FloatInputText( + "##letterSpacingXInput", + ref this.advUiState.LetterSpacingText, + this.selectedFont.LetterSpacing) is { } newLetterSpacing) + { + changed = true; + this.selectedFont = this.selectedFont with { LetterSpacing = newLetterSpacing }; + } + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Line Height:"); + + ImGui.TableNextColumn(); + if (FloatInputText( + "##lineHeightInput", + ref this.advUiState.LineHeightText, + this.selectedFont.LineHeight, + 0.05f, + 0.1f, + 3f) is { } newLineHeight) + { + changed = true; + this.selectedFont = this.selectedFont with { LineHeight = newLineHeight }; + } + + ImGui.EndTable(); + return changed; + + static unsafe float? FloatInputText( + string label, ref string buf, float value, float step = 1f, float min = -127, float max = 127) + { + var stylePushed = value < min || value > max || !float.TryParse(buf, out _); + if (stylePushed) + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + + var changed2 = false; + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + var changed1 = ImGui.InputText( + label, + ref buf, + 255, + ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CallbackHistory | + ImGuiInputTextFlags.CharsDecimal, + data => + { + switch (data->EventKey) + { + case ImGuiKey.DownArrow: + changed2 = true; + value = Math.Min(max, (MathF.Round(value / step) * step) + step); + ImGuiHelpers.SetTextFromCallback(data, $"{value:0.##}"); + break; + case ImGuiKey.UpArrow: + changed2 = true; + value = Math.Max(min, (MathF.Round(value / step) * step) - step); + ImGuiHelpers.SetTextFromCallback(data, $"{value:0.##}"); + break; + } + + return 0; + }); + + if (stylePushed) + ImGui.PopStyleColor(); + + if (!changed1 && !changed2) + return null; + + if (!float.TryParse(buf, out var parsed)) + return null; + + if (min > parsed || parsed > max) + return null; + + return parsed; + } + } + + private void DrawActionButtons(Vector2 buttonSize) + { + if (this.fontHandle?.Available is not true + || this.FontFamilyExcludeFilter?.Invoke(this.selectedFont.FontId.Family) is true) + { + ImGui.BeginDisabled(); + ImGui.Button("OK", buttonSize); + ImGui.EndDisabled(); + } + else if (ImGui.Button("OK", buttonSize)) + { + this.tcs.SetResult(this.selectedFont); + } + + if (ImGui.Button("Cancel", buttonSize)) + { + this.Cancel(); + } + + var doRefresh = false; + var isFirst = false; + if (this.fontFamilies?.IsCompleted is not true) + { + isFirst = doRefresh = this.fontFamilies is null; + ImGui.BeginDisabled(); + ImGui.Button("Refresh", buttonSize); + ImGui.EndDisabled(); + } + else if (ImGui.Button("Refresh", buttonSize)) + { + doRefresh = true; + } + + if (doRefresh) + { + this.fontFamilies = + this.fontFamilies?.ContinueWith(_ => RefreshBody()) + ?? Task.Run(RefreshBody); + this.fontFamilies.ContinueWith(_ => this.firstDrawAfterRefresh = true); + + List RefreshBody() + { + var familyName = this.selectedFont.FontId.Family.ToString() ?? string.Empty; + var fontName = this.selectedFont.FontId.ToString() ?? string.Empty; + + var newFonts = new List { DalamudDefaultFontAndFamilyId.Instance }; + newFonts.AddRange(IFontFamilyId.ListDalamudFonts()); + newFonts.AddRange(IFontFamilyId.ListGameFonts()); + var systemFonts = IFontFamilyId.ListSystemFonts(!isFirst); + systemFonts.Sort( + (a, b) => string.Compare( + this.ExtractName(a), + this.ExtractName(b), + StringComparison.CurrentCultureIgnoreCase)); + newFonts.AddRange(systemFonts); + if (this.FontFamilyExcludeFilter is not null) + newFonts.RemoveAll(this.FontFamilyExcludeFilter); + + this.UpdateSelectedFamilyAndFontIndices(newFonts, familyName, fontName); + return newFonts; + } + } + + if (this.useAdvancedOptions) + { + if (ImGui.Button("Reset", buttonSize)) + { + this.selectedFont = this.selectedFont with + { + LineHeight = 1f, + GlyphOffset = default, + LetterSpacing = default, + }; + + this.advUiState = new(this.selectedFont); + this.fontHandle?.Dispose(); + this.fontHandle = null; + } + } + } + + private void UpdateSelectedFamilyAndFontIndices( + IReadOnlyList fonts, + string familyName, + string fontName) + { + this.selectedFamilyIndex = fonts.FindIndex(x => x.ToString() == familyName); + if (this.selectedFamilyIndex == -1) + { + this.selectedFontIndex = -1; + } + else + { + this.selectedFontIndex = -1; + var family = fonts[this.selectedFamilyIndex]; + for (var i = 0; i < family.Fonts.Count; i++) + { + if (family.Fonts[i].ToString() == fontName) + { + this.selectedFontIndex = i; + break; + } + } + + if (this.selectedFontIndex == -1) + this.selectedFontIndex = 0; + this.selectedFont = this.selectedFont with + { + FontId = fonts[this.selectedFamilyIndex].Fonts[this.selectedFontIndex], + }; + } + } + + private string ExtractName(IObjectWithLocalizableName what) => + what.GetLocalizedName(Service.Get().EffectiveLanguage); + // Note: EffectiveLanguage can be incorrect but close enough for now + + private bool TestName(IObjectWithLocalizableName what, string search) => + this.ExtractName(what).Contains(search, StringComparison.CurrentCultureIgnoreCase); + + private struct AdvancedOptionsUiState + { + public string OffsetXText; + public string OffsetYText; + public string LetterSpacingText; + public string LineHeightText; + + public AdvancedOptionsUiState(SingleFontSpec spec) + { + this.OffsetXText = $"{spec.GlyphOffset.X:0.##}"; + this.OffsetYText = $"{spec.GlyphOffset.Y:0.##}"; + this.LetterSpacingText = $"{spec.LetterSpacing:0.##}"; + this.LineHeightText = $"{spec.LineHeight:0.##}"; + } + } +} diff --git a/Dalamud/Interface/Internal/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs index 4654a019d..ace8887f1 100644 --- a/Dalamud/Interface/Internal/DalamudCommands.cs +++ b/Dalamud/Interface/Internal/DalamudCommands.cs @@ -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.Get().ToggleProfilerWindow(); + } } diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 28a9075bd..1ee248b17 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -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); /// public void Dispose() diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index b8ca98584..00bef19af 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -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); + /// /// Gets the number of frames since Dalamud has loaded. /// @@ -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.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)) diff --git a/Dalamud/Interface/Internal/DalamudTextureWrap.cs b/Dalamud/Interface/Internal/DalamudTextureWrap.cs index 9737d9f7b..b49c6f07b 100644 --- a/Dalamud/Interface/Internal/DalamudTextureWrap.cs +++ b/Dalamud/Interface/Internal/DalamudTextureWrap.cs @@ -1,41 +1,14 @@ -using System.Numerics; +using Dalamud.Utility; using ImGuiScene; namespace Dalamud.Interface.Internal; -/// -/// Base TextureWrap interface for all Dalamud-owned texture wraps. -/// Used to avoid referencing ImGuiScene. -/// -public interface IDalamudTextureWrap : IDisposable -{ - /// - /// Gets a texture handle suitable for direct use with ImGui functions. - /// - IntPtr ImGuiHandle { get; } - - /// - /// Gets the width of the texture. - /// - int Width { get; } - - /// - /// Gets the height of the texture. - /// - int Height { get; } - - /// - /// Gets the size vector of the texture using Width, Height. - /// - Vector2 Size => new(this.Width, this.Height); -} - /// /// Safety harness for ImGuiScene textures that will defer destruction until /// the end of the frame. /// -public class DalamudTextureWrap : IDalamudTextureWrap +public class DalamudTextureWrap : IDalamudTextureWrap, IDeferredDisposable { private readonly TextureWrap wrappedWrap; @@ -83,7 +56,7 @@ public class DalamudTextureWrap : IDalamudTextureWrap /// /// Actually dispose the wrapped texture. /// - internal void RealDispose() + void IDeferredDisposable.RealDispose() { this.wrappedWrap.Dispose(); } diff --git a/Dalamud/Interface/Internal/IDalamudTextureWrap.cs b/Dalamud/Interface/Internal/IDalamudTextureWrap.cs new file mode 100644 index 000000000..8e2e56c26 --- /dev/null +++ b/Dalamud/Interface/Internal/IDalamudTextureWrap.cs @@ -0,0 +1,55 @@ +using System.Numerics; + +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.Internal; + +/// +/// Base TextureWrap interface for all Dalamud-owned texture wraps. +/// Used to avoid referencing ImGuiScene. +/// +public interface IDalamudTextureWrap : IDisposable +{ + /// + /// Gets a texture handle suitable for direct use with ImGui functions. + /// + IntPtr ImGuiHandle { get; } + + /// + /// Gets the width of the texture. + /// + int Width { get; } + + /// + /// Gets the height of the texture. + /// + int Height { get; } + + /// + /// Gets the size vector of the texture using Width, Height. + /// + Vector2 Size => new(this.Width, this.Height); + + /// + /// Creates a new reference to the resource being pointed by this instance of . + /// + /// The new reference to this texture wrap. + /// + /// On calling this function, a new instance of will be returned, but with + /// the same . The new instance must be 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.
+ /// 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.
+ /// The default implementation will treat as an . + ///
+ 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); + } +} diff --git a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs index 1746fb1c4..bbf665405 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs @@ -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(); diff --git a/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs b/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs new file mode 100644 index 000000000..f2d6ed244 --- /dev/null +++ b/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs @@ -0,0 +1,133 @@ +using System.Diagnostics; +using System.Linq; +using System.Numerics; + +using Dalamud.Hooking; + +using ImGuiNET; + +namespace Dalamud.Interface.Internal; + +/// +/// 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. +/// +[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 hookImDrawListAddPolyline; + private readonly Hook hookImDrawListAddRectFilled; + + [ServiceManager.ServiceConstructor] + private ImGuiDrawListFixProvider(InterfaceManager.InterfaceManagerWithScene imws) + { + // Force cimgui.dll to be loaded. + _ = ImGui.GetCurrentContext(); + var cimgui = Process.GetCurrentProcess().Modules.Cast() + .First(x => x.ModuleName == "cimgui.dll") + .BaseAddress; + + this.hookImDrawListAddPolyline = Hook.FromAddress( + cimgui + CImGuiImDrawListAddPolyLineOffset, + this.ImDrawListAddPolylineDetour); + this.hookImDrawListAddRectFilled = Hook.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); + + /// + 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(); + } +} diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 6cf4a8b90..3db799be0 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -62,7 +62,7 @@ internal class InterfaceManager : IDisposable, IServiceType /// public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; - private readonly ConcurrentBag deferredDisposeTextures = new(); + private readonly ConcurrentBag deferredDisposeTextures = new(); private readonly ConcurrentBag 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. /// /// The texture. - 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.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.Get().DefaultFontSpec.SizePx, + }))); this.dalamudAtlas.BuildStepChange += e => e.OnPostBuild( tk => { diff --git a/Dalamud/Interface/Internal/ManagedAsserts/ImGuiContextOffsets.cs b/Dalamud/Interface/Internal/ManagedAsserts/ImGuiContextOffsets.cs index fd203192f..89e23ab78 100644 --- a/Dalamud/Interface/Internal/ManagedAsserts/ImGuiContextOffsets.cs +++ b/Dalamud/Interface/Internal/ManagedAsserts/ImGuiContextOffsets.cs @@ -18,4 +18,6 @@ internal static class ImGuiContextOffsets public const int FontStackOffset = 0x7A4; public const int BeginPopupStackOffset = 0x7B8; + + public const int TextStateOffset = 0x4588; } diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs index 14f062e01..d93b90799 100644 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -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.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; diff --git a/Dalamud/Interface/Internal/UnknownTextureWrap.cs b/Dalamud/Interface/Internal/UnknownTextureWrap.cs new file mode 100644 index 000000000..41164f2c3 --- /dev/null +++ b/Dalamud/Interface/Internal/UnknownTextureWrap.cs @@ -0,0 +1,77 @@ +using System.Threading; + +using Dalamud.Utility; + +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.Internal; + +/// +/// A texture wrap that is created by cloning the underlying . +/// +internal sealed unsafe class UnknownTextureWrap : IDalamudTextureWrap, IDeferredDisposable +{ + private IntPtr imGuiHandle; + + /// + /// Initializes a new instance of the class. + /// + /// The pointer to that is suitable for use with + /// . + /// The width of the texture. + /// The height of the texture. + /// If true, call . + 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(); + } + + /// + /// Finalizes an instance of the class. + /// + ~UnknownTextureWrap() => this.Dispose(false); + + /// + public nint ImGuiHandle => + this.imGuiHandle == nint.Zero + ? throw new ObjectDisposedException(nameof(UnknownTextureWrap)) + : this.imGuiHandle; + + /// + public int Width { get; } + + /// + public int Height { get; } + + /// + /// Queue the texture to be disposed once the frame ends. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Actually dispose the wrapped texture. + /// + 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.GetNullable()?.EnqueueDeferredDispose(this); + else + ((IDeferredDisposable)this).RealDispose(); + } +} diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 53821d9df..f36d79222 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -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; /// internal class ConsoleWindow : Window, IDisposable { - private readonly List logText = new(); + private const int LogLinesMinimum = 100; + private const int LogLinesMaximum = 1000000; + + private readonly RollingList logText; + private volatile int newRolledLines; private readonly object renderLock = new(); private readonly List 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 FilteredLogEntries { get; set; } = new(); + private RollingList FilteredLogEntries { get; set; } /// public override void OnOpen() @@ -92,6 +106,7 @@ internal class ConsoleWindow : Window, IDisposable public void Dispose() { SerilogEventSink.Instance.LogLine -= this.OnLogLine; + Service.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved; } /// @@ -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(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; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs index b486cc7d9..8bb999557 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -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; /// internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable { + private static readonly string[] FontScaleModes = + { + nameof(FontScaleMode.Default), + nameof(FontScaleMode.SkipHandling), + nameof(FontScaleMode.UndoGlobalScale), + }; + private ImVectorWrapper testStringBuffer; private IFontAtlas? privateAtlas; + private SingleFontSpec fontSpec = new() { FontId = DalamudDefaultFontAndFamilyId.Instance }; + private IFontHandle? fontDialogHandle; private IReadOnlyDictionary 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.Get().CreateFontAtlas( + $"{nameof(GamePrebakedFontsTestWidget)}:EditorFont", + FontAtlasAutoRebuildMode.Async)); + fcd.SelectedFont = this.fontSpec; + fcd.IgnorePreviewGlobalScale = !this.atlasScaleMode; + Service.Get().Draw += fcd.Draw; + fcd.ResultTask.ContinueWith( + r => Service.Get().RunOnFrameworkThread( + () => + { + Service.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.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() .Where(x => x.GetAttribute() is not null) @@ -149,17 +204,29 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable .ToImmutableDictionary( x => x.Key, x => x.Select( - y => (y, new Lazy( - () => 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 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; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 0cbc401e7..8d6879ac1 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -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()); } } diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 83d819634..95c227662 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -148,7 +148,6 @@ internal class PluginInstallerWindow : Window, IDisposable this.SizeConstraints = new WindowSizeConstraints { MinimumSize = this.Size.Value, - MaximumSize = new Vector2(5000, 5000), }; Service.GetAsync().ContinueWith(pluginManagerTask => diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/ContextMenuAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/ContextMenuAgingStep.cs index 570e362ef..579f8357b 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/ContextMenuAgingStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/ContextMenuAgingStep.cs @@ -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; /// 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 itemSheet; + private ExcelSheet materiaSheet; + private ExcelSheet stainSheet; private enum SubStep { Start, - TestItem, - TestGameObject, - TestSubMenu, - TestMultiple, + TestInventoryAndSubmenu, + TestDefault, Finish, } - */ /// public string Name => "Test Context Menu"; @@ -45,23 +43,24 @@ internal class ContextMenuAgingStep : IAgingStep /// public SelfTestStepResult RunStep() { - /* var contextMenu = Service.Get(); var dataMgr = Service.Get(); + this.itemSheet = dataMgr.GetExcelSheet()!; + this.materiaSheet = dataMgr.GetExcelSheet()!; + this.stainSheet = dataMgr.GetExcelSheet()!; 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()!.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; } - + /// public void CleanUp() { - /* var contextMenu = Service.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(); + 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"})"); } } - */ } diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index c325028e1..47ba2c65f 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -68,11 +68,11 @@ internal class SettingsWindow : Window var interfaceManager = Service.Get(); var fontAtlasFactory = Service.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(); diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 5293e13c4..ea6400121 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -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( - 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.Get().UseAxisOverride = v; - Service.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.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.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.Get(); + faf.DefaultFontSpecOverride = + this.defaultFontSpec = + new SingleFontSpec { FontId = new GameFontAndFamilyId(GameFontFamily.Axis) }; + interfaceManager.RebuildFonts(); + } + } + base.Draw(); } public override void Load() { this.globalUiScale = Service.Get().GlobalUiScale; + this.defaultFontSpec = Service.Get().DefaultFontSpec; base.Load(); } @@ -214,6 +248,7 @@ public class SettingsTabLook : SettingsTab public override void Save() { Service.Get().GlobalUiScale = this.globalUiScale; + Service.Get().DefaultFontSpec = this.defaultFontSpec; base.Save(); } diff --git a/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs b/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs index c202a36ce..9ee4123cd 100644 --- a/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs +++ b/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs @@ -43,7 +43,6 @@ public class StyleEditorWindow : Window this.SizeConstraints = new WindowSizeConstraints { MinimumSize = new Vector2(890, 560), - MaximumSize = new Vector2(10000, 10000), }; } diff --git a/Dalamud/Interface/ManagedFontAtlas/FontScaleMode.cs b/Dalamud/Interface/ManagedFontAtlas/FontScaleMode.cs new file mode 100644 index 000000000..b30d5c26c --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FontScaleMode.cs @@ -0,0 +1,33 @@ +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Specifies how should global font scale affect a font. +/// +public enum FontScaleMode +{ + /// + /// 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. + /// + Default, + + /// + /// 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. + /// + SkipHandling, + + /// + /// 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 + /// and 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. + /// + UndoGlobalScale, +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs index a9c21f94e..0445499c8 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -8,7 +8,8 @@ using ImGuiNET; namespace Dalamud.Interface.ManagedFontAtlas; /// -/// Wrapper for . +/// Wrapper for .
+/// Not intended for plugins to implement. ///
public interface IFontAtlas : IDisposable { @@ -93,11 +94,15 @@ public interface IFontAtlas : IDisposable /// /// Callback for . /// Handle to a font that may or may not be ready yet. + /// + /// Consider calling to support + /// glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language users. + /// /// /// On initialization: /// /// 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); diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs index f75ed4686..158366b12 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs @@ -9,7 +9,8 @@ using ImGuiNET; namespace Dalamud.Interface.ManagedFontAtlas; /// -/// Common stuff for and . +/// Common stuff for and .
+/// Not intended for plugins to implement. ///
public interface IFontAtlasBuildToolkit { diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs index eb7c7e08c..827187063 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs @@ -1,20 +1,23 @@ using Dalamud.Interface.Internal; +using Dalamud.Utility; using ImGuiNET; namespace Dalamud.Interface.ManagedFontAtlas; /// -/// Toolkit for use when the build state is . +/// Toolkit for use when the build state is .
+/// Not intended for plugins to implement. ///
public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit { - /// - /// Gets whether global scaling is ignored for the given font. - /// - /// The font. - /// True if ignored. - bool IsGlobalScaleIgnored(ImFontPtr fontPtr); + /// + [Obsolete($"Use {nameof(this.GetFontScaleMode)}")] + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => this.GetFontScaleMode(fontPtr) == FontScaleMode.UndoGlobalScale; + + /// + FontScaleMode GetFontScaleMode(ImFontPtr fontPtr); ///
/// Stores a texture to be managed with the atlas. diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs index 38d8d2fe8..9b80d27ff 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs @@ -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; /// /// Toolkit for use when the build state is .
+/// Not intended for plugins to implement.
///
/// After returns, /// either must be set, @@ -43,14 +46,43 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit ///
/// The font. /// Same with . - 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); /// /// Gets whether global scaling is ignored for the given font. /// /// The font. /// True if ignored. - bool IsGlobalScaleIgnored(ImFontPtr fontPtr); + [Obsolete($"Use {nameof(this.GetFontScaleMode)}")] + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => this.GetFontScaleMode(fontPtr) == FontScaleMode.UndoGlobalScale; + + /// + /// Sets the scaling mode for the given font. + /// + /// The font, returned from and alike. + /// Note that property is not guaranteed to be automatically updated upon + /// calling font adding functions. Pass the return value from font adding functions, not + /// property. + /// The scaling mode. + /// . + ImFontPtr SetFontScaleMode(ImFontPtr fontPtr, FontScaleMode mode); + + /// + /// Gets the scaling mode for the given font. + /// + /// The font. + /// The scaling mode. + FontScaleMode GetFontScaleMode(ImFontPtr fontPtr); + + /// + /// Registers a function to be run after build. + /// + /// The action to run. + void RegisterPostBuild(Action action); /// /// Adds a font from memory region allocated using .
@@ -134,7 +166,12 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit /// As this involves adding multiple fonts, calling this function will set /// as the return value of this function, if it was empty before. ///
- /// Font size in pixels. + /// + /// Font size in pixels. + /// If a negative value is supplied, + /// (. * ) will be + /// used as the font size. Specify -1 to use the default font size. + /// /// The glyph ranges. Use .ToGlyphRange to build. /// A font returned from . ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges = null); diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index 11c26616b..70799bb9c 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -5,7 +5,8 @@ using ImGuiNET; namespace Dalamud.Interface.ManagedFontAtlas; /// -/// Represents a reference counting handle for fonts. +/// Represents a reference counting handle for fonts.
+/// Not intended for plugins to implement. ///
public interface IFontHandle : IDisposable { diff --git a/Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs b/Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs index 9136d2723..a4cc3afa7 100644 --- a/Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs +++ b/Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs @@ -4,7 +4,8 @@ namespace Dalamud.Interface.ManagedFontAtlas; /// /// The wrapper for , guaranteeing that the associated data will be available as long as -/// this struct is not disposed. +/// this struct is not disposed.
+/// Not intended for plugins to implement. ///
public interface ILockedImFont : IDisposable { diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index e2b096701..55af20329 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -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 registeredPostBuildActions = new(); ///
/// Initializes a new instance of the class. @@ -81,9 +83,9 @@ internal sealed partial class FontAtlasFactory public ImVectorWrapper Fonts => this.data.Fonts; /// - /// Gets the list of fonts to ignore global scale. + /// Gets the font scale modes. /// - public List GlobalScaleExclusions { get; } = new(); + private Dictionary FontScaleModes { get; } = new(); /// public void Dispose() => this.disposeAfterBuild.Dispose(); @@ -149,19 +151,22 @@ internal sealed partial class FontAtlasFactory } /// - public ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr) + public ImFontPtr SetFontScaleMode(ImFontPtr fontPtr, FontScaleMode scaleMode) { - this.GlobalScaleExclusions.Add(fontPtr); + this.FontScaleModes[fontPtr] = scaleMode; return fontPtr; } - /// - public bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => - this.GlobalScaleExclusions.Contains(fontPtr); + /// + public FontScaleMode GetFontScaleMode(ImFontPtr fontPtr) => + this.FontScaleModes.GetValueOrDefault(fontPtr, FontScaleMode.Default); /// public int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError) => this.data.AddNewTexture(textureWrap, disposeOnError); + + /// + public void RegisterPostBuild(Action action) => this.registeredPostBuildActions.Add(action); /// 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 /// 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(); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 4d636b8cf..883fcbbfc 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -46,6 +46,9 @@ internal sealed partial class FontAtlasFactory private class FontAtlasBuiltData : IRefCountable { + // Field for debugging. + private static int numActiveInstances; + private readonly List wraps; private readonly List 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) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 358ccd845..3e0fd1394 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -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 } /// - /// Gets or sets a value indicating whether to override configuration for UseAxis. + /// Gets or sets a value indicating whether to override configuration for . /// - public bool? UseAxisOverride { get; set; } = null; + public IFontSpec? DefaultFontSpecOverride { get; set; } = null; /// - /// Gets a value indicating whether to use AXIS fonts. + /// Gets the default font ID. /// - public bool UseAxis => this.UseAxisOverride ?? Service.Get().UseAxisFontsFromGame; + public IFontSpec DefaultFontSpec => + this.DefaultFontSpecOverride + ?? Service.Get().DefaultFontSpec +#pragma warning disable CS0618 // Type or member is obsolete + ?? (Service.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, + }); /// /// Gets the service instance of . @@ -229,6 +245,25 @@ internal sealed partial class FontAtlasFactory private static T ExtractResult(Task t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult(); + /// + /// Clones a texture wrap, by getting a new reference to the underlying and the + /// texture behind. + /// + /// The to clone from. + /// The cloned . + private static IDalamudTextureWrap CloneTextureWrap(IDalamudTextureWrap wrap) + { + var srv = CppObject.FromPointer(wrap.ImGuiHandle); + using var res = srv.Resource; + using var tex2D = res.QueryInterface(); + var description = tex2D.Description; + return new DalamudTextureWrap( + new D3DTextureWrap( + srv.QueryInterface(), + description.Width, + description.Height)); + } + private static unsafe void ExtractChannelFromB8G8R8A8( Span target, ReadOnlySpan source, @@ -266,25 +301,6 @@ internal sealed partial class FontAtlasFactory } } - /// - /// Clones a texture wrap, by getting a new reference to the underlying and the - /// texture behind. - /// - /// The to clone from. - /// The cloned . - private static IDalamudTextureWrap CloneTextureWrap(IDalamudTextureWrap wrap) - { - var srv = CppObject.FromPointer(wrap.ImGuiHandle); - using var res = srv.Resource; - using var tex2D = res.QueryInterface(); - var description = tex2D.Description; - return new DalamudTextureWrap( - new D3DTextureWrap( - srv.QueryInterface(), - description.Width, - description.Height)); - } - private static unsafe void ExtractChannelFromB4G4R4A4( Span target, ReadOnlySpan source, @@ -317,7 +333,7 @@ internal sealed partial class FontAtlasFactory v |= v << 4; *wptr = (uint)((v << 24) | 0x00FFFFFF); wptr++; - rptr += 4; + rptr += 2; } } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index b6c9817aa..1101e7119 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -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); } diff --git a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs index cb7f7c65a..caa686856 100644 --- a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs +++ b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs @@ -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; } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 55e11dfac..d260868a0 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -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 /// /// Gets the default Dalamud font size in points. /// - public static float DefaultFontSizePt => InterfaceManager.DefaultFontSizePt; + public static float DefaultFontSizePt => Service.Get().DefaultFontSpec.SizePt; /// /// Gets the default Dalamud font size in pixels. /// - public static float DefaultFontSizePx => InterfaceManager.DefaultFontSizePx; + public static float DefaultFontSizePx => Service.Get().DefaultFontSpec.SizePx; /// /// Gets the default Dalamud font - supporting all game languages and icons.
@@ -198,6 +199,11 @@ public sealed class UiBuilder : IDisposable ///
public static ImFontPtr MonoFont => InterfaceManager.MonoFont; + /// + /// Gets the default font specifications. + /// + public IFontSpec DefaultFontSpec => Service.Get().DefaultFontSpec; + /// /// Gets the handle to the default Dalamud font - supporting all game languages and icons. /// @@ -206,7 +212,7 @@ public sealed class UiBuilder : IDisposable /// /// fontAtlas.NewDelegateFontHandle( /// e => e.OnPreBuild( - /// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePt))); + /// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePx))); /// /// 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 }))); ///
/// 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 }))); /// /// public IFontHandle MonoFontHandle => diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index 444463d41..f02effe1d 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -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))); } + + /// + /// Sets the text for a text input, during the callback. + /// + /// The callback data. + /// The new text. + 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); + } /// /// Finds the corresponding ImGui viewport ID for the given window handle. diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 59cb4d570..a7565c294 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -623,15 +623,38 @@ public abstract class Window /// public struct WindowSizeConstraints { + private Vector2 internalMaxSize = new(float.MaxValue); + + /// + /// Initializes a new instance of the struct. + /// + public WindowSizeConstraints() + { + } + /// /// Gets or sets the minimum size of the window. /// - public Vector2 MinimumSize { get; set; } - + public Vector2 MinimumSize { get; set; } = new(0); + /// /// Gets or sets the maximum size of the window. /// - 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; + } } /// diff --git a/Dalamud/Memory/MemoryHelper.cs b/Dalamud/Memory/MemoryHelper.cs index 552817646..09f45e2d3 100644 --- a/Dalamud/Memory/MemoryHelper.cs +++ b/Dalamud/Memory/MemoryHelper.cs @@ -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; /// public static unsafe class MemoryHelper { + private static readonly ObjectPool StringBuilderPool = + ObjectPool.Create(new StringBuilderPooledObjectPolicy()); + + #region Cast + + /// Casts the given memory address as the reference to the live object. + /// The memory address. + /// The unmanaged type. + /// The reference to the live object. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T Cast(nint memoryAddress) where T : unmanaged => ref *(T*)memoryAddress; + + /// Casts the given memory address as the span of the live object(s). + /// The memory address. + /// The number of items. + /// The unmanaged type. + /// The span containing reference to the live object(s). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span Cast(nint memoryAddress, int length) where T : unmanaged => + new((void*)memoryAddress, length); + + /// Casts the given memory address as the span of the live object(s), until it encounters a zero. + /// The memory address. + /// The maximum number of items. + /// The unmanaged type. + /// The span containing reference to the live object(s). + /// If is byte or char and is not + /// specified, consider using or + /// . + public static Span CastNullTerminated(nint memoryAddress, int maxLength = int.MaxValue) + where T : unmanaged, IEquatable + { + var typedPointer = (T*)memoryAddress; + var length = 0; + while (length < maxLength && !default(T).Equals(*typedPointer++)) + length++; + return new((void*)memoryAddress, length); + } + + #endregion + #region Read /// @@ -27,7 +74,9 @@ public static unsafe class MemoryHelper /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. /// The memory address to read from. /// The read in struct. - public static T Read(IntPtr memoryAddress) where T : unmanaged + /// If you do not need to make a copy, use instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Read(nint memoryAddress) where T : unmanaged => Read(memoryAddress, false); /// @@ -37,12 +86,13 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// Set this to true to enable struct marshalling. /// The read in struct. - public static T Read(IntPtr memoryAddress, bool marshal) - { - return marshal - ? Marshal.PtrToStructure(memoryAddress) - : Unsafe.Read((void*)memoryAddress); - } + /// If you do not need to make a copy and is false, + /// use instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Read(nint memoryAddress, bool marshal) => + marshal + ? Marshal.PtrToStructure(memoryAddress) + : Unsafe.Read((void*)memoryAddress); /// /// Reads a byte array from a specified memory address. @@ -50,12 +100,9 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The amount of bytes to read starting from the memoryAddress. /// The read in byte array. - public static byte[] ReadRaw(IntPtr memoryAddress, int length) - { - var value = new byte[length]; - Marshal.Copy(memoryAddress, value, 0, value.Length); - return value; - } + /// If you do not need to make a copy, use instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] ReadRaw(nint memoryAddress, int length) => Cast(memoryAddress, length).ToArray(); /// /// Reads a generic type array from a specified memory address. @@ -64,8 +111,10 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The amount of array items to read. /// The read in struct array. - public static T[] Read(IntPtr memoryAddress, int arrayLength) where T : unmanaged - => Read(memoryAddress, arrayLength, false); + /// If you do not need to make a copy, use instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T[] Read(nint memoryAddress, int arrayLength) where T : unmanaged + => Cast(memoryAddress, arrayLength).ToArray(); /// /// Reads a generic type array from a specified memory address. @@ -75,16 +124,18 @@ public static unsafe class MemoryHelper /// The amount of array items to read. /// Set this to true to enable struct marshalling. /// The read in struct array. - public static T[] Read(IntPtr memoryAddress, int arrayLength, bool marshal) + /// If you do not need to make a copy and is false, + /// use instead. + public static T[] Read(nint memoryAddress, int arrayLength, bool marshal) { var structSize = SizeOf(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 /// /// The memory address to read from. /// The read in byte array. - public static unsafe byte[] ReadRawNullTerminated(IntPtr memoryAddress) - { - var byteCount = 0; - while (*(byte*)(memoryAddress + byteCount) != 0x00) - { - byteCount++; - } - - return ReadRaw(memoryAddress, byteCount); - } + /// If you do not need to make a copy, use instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] ReadRawNullTerminated(nint memoryAddress) => + MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)memoryAddress).ToArray(); #endregion @@ -116,7 +161,9 @@ public static unsafe class MemoryHelper /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. /// The memory address to read from. /// Local variable to receive the read in struct. - public static void Read(IntPtr memoryAddress, out T value) where T : unmanaged + /// If you do not need to make a copy, use instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Read(nint memoryAddress, out T value) where T : unmanaged => value = Read(memoryAddress); /// @@ -126,7 +173,10 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// Local variable to receive the read in struct. /// Set this to true to enable struct marshalling. - public static void Read(IntPtr memoryAddress, out T value, bool marshal) + /// If you do not need to make a copy and is false, + /// use instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Read(nint memoryAddress, out T value, bool marshal) => value = Read(memoryAddress, marshal); /// @@ -135,7 +185,9 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The amount of bytes to read starting from the memoryAddress. /// Local variable to receive the read in bytes. - public static void ReadRaw(IntPtr memoryAddress, int length, out byte[] value) + /// If you do not need to make a copy, use instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadRaw(nint memoryAddress, int length, out byte[] value) => value = ReadRaw(memoryAddress, length); /// @@ -145,7 +197,9 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The amount of array items to read. /// The read in struct array. - public static void Read(IntPtr memoryAddress, int arrayLength, out T[] value) where T : unmanaged + /// If you do not need to make a copy, use instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Read(nint memoryAddress, int arrayLength, out T[] value) where T : unmanaged => value = Read(memoryAddress, arrayLength); /// @@ -156,7 +210,10 @@ public static unsafe class MemoryHelper /// The amount of array items to read. /// Set this to true to enable struct marshalling. /// The read in struct array. - public static void Read(IntPtr memoryAddress, int arrayLength, bool marshal, out T[] value) + /// If you do not need to make a copy and is false, + /// use instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Read(nint memoryAddress, int arrayLength, bool marshal, out T[] value) => value = Read(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(pmem, length); var memCharCount = encoding.GetCharCount(mem); if (memCharCount != charSpan.Length) return false; - Span chars = stackalloc char[memCharCount]; - encoding.GetChars(mem, chars); - return charSpan.SequenceEqual(chars); + if (memCharCount < 1024) + { + Span chars = stackalloc char[memCharCount]; + encoding.GetChars(mem, chars); + return charSpan.SequenceEqual(chars); + } + else + { + var rented = ArrayPool.Shared.Rent(memCharCount); + var chars = rented.AsSpan(0, memCharCount); + encoding.GetChars(mem, chars); + var equals = charSpan.SequenceEqual(chars); + ArrayPool.Shared.Return(rented); + return equals; + } } /// @@ -203,8 +272,9 @@ public static unsafe class MemoryHelper /// /// The memory address to read from. /// The read in string. - 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)); /// /// Read a string with the given encoding from a specified memory address. @@ -215,10 +285,25 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The encoding to use to decode the string. /// The read in string. - 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( + MemoryMarshal.CreateReadOnlySpanFromNullTerminated((char*)memoryAddress))); + case UTF32Encoding: + return encoding.GetString(MemoryMarshal.Cast(CastNullTerminated(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)); + } } /// @@ -228,10 +313,12 @@ public static unsafe class MemoryHelper /// Attention! If this is an , use the applicable helper methods to decode. /// /// The memory address to read from. - /// The maximum length of the string. + /// The maximum number of bytes to read. + /// Note that this is NOT the maximum length of the returned string. /// The read in string. - 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(memoryAddress, maxLength)); /// /// Read a string with the given encoding from a specified memory address. @@ -241,18 +328,32 @@ public static unsafe class MemoryHelper /// /// The memory address to read from. /// The encoding to use to decode the string. - /// The maximum length of the string. + /// The maximum number of bytes to read. + /// Note that this is NOT the maximum length of the returned string. /// The read in string. - 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(memoryAddress, maxLength)); + case UnicodeEncoding: + return encoding.GetString( + MemoryMarshal.Cast(CastNullTerminated(memoryAddress, maxLength / 2))); + case UTF32Encoding: + return encoding.GetString( + MemoryMarshal.Cast(CastNullTerminated(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(memoryAddress, maxLength)); + var eosPos = data.IndexOf('\0'); + return eosPos >= 0 ? data[..eosPos] : data; + } } /// @@ -260,11 +361,9 @@ public static unsafe class MemoryHelper /// /// The memory address to read from. /// The read in string. - 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)); /// /// Read an SeString from a specified memory address. @@ -272,40 +371,165 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The maximum length of the string. /// The read in string. - 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(memoryAddress, maxLength)); /// /// Read an SeString from a specified Utf8String structure. /// /// The memory address to read from. /// The read in string. - public static unsafe SeString ReadSeString(Utf8String* utf8String) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SeString ReadSeString(Utf8String* utf8String) => + utf8String == null ? string.Empty : SeString.Parse(utf8String->AsSpan()); + + /// + /// Reads an SeString from a specified memory address, and extracts the outermost string.
+ /// If the SeString is malformed, behavior is undefined. + ///
+ /// Whether the SeString contained a non-represented payload. + /// The memory address to read from. + /// The maximum length of the string. + /// Stop reading on encountering the first non-represented payload. + /// What payloads are represented via this function may change. + /// Replacement for non-represented payloads. + /// The read in string. + 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(memoryAddress, maxLength).Length); - var ptr = utf8String->StringPtr; - if (ptr == null) - return string.Empty; + // 1 utf-8 codepoint can spill up to 2 characters. + Span 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 /// /// The memory address to read from. /// The read in string. - public static void ReadStringNullTerminated(IntPtr memoryAddress, out string value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadStringNullTerminated(nint memoryAddress, out string value) => value = ReadStringNullTerminated(memoryAddress); /// @@ -332,7 +557,8 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The encoding to use to decode the string. /// The read in string. - 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); /// @@ -344,7 +570,8 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The read in string. /// The maximum length of the string. - 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); /// @@ -357,7 +584,8 @@ public static unsafe class MemoryHelper /// The encoding to use to decode the string. /// The maximum length of the string. /// The read in string. - 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); /// @@ -365,7 +593,8 @@ public static unsafe class MemoryHelper /// /// The memory address to read from. /// The read in SeString. - public static void ReadSeStringNullTerminated(IntPtr memoryAddress, out SeString value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadSeStringNullTerminated(nint memoryAddress, out SeString value) => value = ReadSeStringNullTerminated(memoryAddress); /// @@ -374,7 +603,8 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The maximum length of the string. /// The read in SeString. - 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); /// @@ -382,6 +612,7 @@ public static unsafe class MemoryHelper /// /// The memory address to read from. /// The read in string. + [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 /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. /// The memory address to read from. /// The item to write to the address. - public static void Write(IntPtr memoryAddress, T item) where T : unmanaged + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Write(nint memoryAddress, T item) where T : unmanaged => Write(memoryAddress, item, false); /// @@ -405,7 +637,7 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The item to write to the address. /// Set this to true to enable struct marshalling. - public static void Write(IntPtr memoryAddress, T item, bool marshal) + public static void Write(nint memoryAddress, T item, bool marshal) { if (marshal) Marshal.StructureToPtr(item, memoryAddress, false); @@ -418,10 +650,8 @@ public static unsafe class MemoryHelper /// /// The memory address to read from. /// The bytes to write to memoryAddress. - 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); /// /// Writes a generic type array to a specified memory address. @@ -429,7 +659,8 @@ public static unsafe class MemoryHelper /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. /// The memory address to write to. /// The array of items to write to the address. - public static void Write(IntPtr memoryAddress, T[] items) where T : unmanaged + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Write(nint memoryAddress, T[] items) where T : unmanaged => Write(memoryAddress, items, false); /// @@ -439,7 +670,8 @@ public static unsafe class MemoryHelper /// The memory address to write to. /// The array of items to write to the address. /// Set this to true to enable struct marshalling. - public static void Write(IntPtr memoryAddress, T[] items, bool marshal) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Write(nint memoryAddress, T[] items, bool marshal) { var structSize = SizeOf(marshal); @@ -462,7 +694,8 @@ public static unsafe class MemoryHelper /// /// The memory address to write to. /// The string to write. - public static void WriteString(IntPtr memoryAddress, string value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteString(nint memoryAddress, string? value) => WriteString(memoryAddress, value, Encoding.UTF8); /// @@ -474,14 +707,12 @@ public static unsafe class MemoryHelper /// The memory address to write to. /// The string to write. /// The encoding to use. - 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(memoryAddress, encoding.GetMaxByteCount(value.Length))); + encoding.GetBytes("\0", Cast(memoryAddress + ptr, 4)); } /// @@ -489,7 +720,8 @@ public static unsafe class MemoryHelper /// /// The memory address to write to. /// The SeString to write. - 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 /// /// Amount of bytes to be allocated. /// Address to the newly allocated memory. - 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 /// /// Amount of bytes to be allocated. /// Address to the newly allocated memory. - public static void Allocate(int length, out IntPtr memoryAddress) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Allocate(int length, out nint memoryAddress) => memoryAddress = Allocate(length); /// @@ -535,9 +769,10 @@ public static unsafe class MemoryHelper /// /// The address of the memory to free. /// True if the operation is successful. - 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); } /// @@ -547,9 +782,9 @@ public static unsafe class MemoryHelper /// The region size for which to change permissions for. /// The new permissions to set. /// The old page permissions. - 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 /// The region size for which to change permissions for. /// The new permissions to set. /// The old page permissions. - 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); /// @@ -580,7 +817,9 @@ public static unsafe class MemoryHelper /// The new permissions to set. /// Set to true to calculate the size of the struct after marshalling instead of before. /// The old page permissions. - public static MemoryProtection ChangePermission(IntPtr memoryAddress, ref T baseElement, MemoryProtection newPermissions, bool marshal) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryProtection ChangePermission( + nint memoryAddress, ref T baseElement, MemoryProtection newPermissions, bool marshal) => ChangePermission(memoryAddress, SizeOf(marshal), newPermissions); /// @@ -590,7 +829,8 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The amount of bytes to read starting from the memoryAddress. /// The read in bytes. - 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 /// The memory address to read from. /// The amount of bytes to read starting from the memoryAddress. /// The read in bytes. - 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); /// @@ -613,12 +854,12 @@ public static unsafe class MemoryHelper /// /// The memory address to read from. /// The read in bytes. - 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 /// /// The memory address to write to. /// The bytes to write to memoryAddress. - 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 /// /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. /// The size of the primitive or struct. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int SizeOf() => SizeOf(false); @@ -669,6 +911,7 @@ public static unsafe class MemoryHelper /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. /// If set to true; will return the size of an element after marshalling. /// The size of the primitive or struct. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int SizeOf(bool marshal) => marshal ? Marshal.SizeOf() : Unsafe.SizeOf(); @@ -678,6 +921,7 @@ public static unsafe class MemoryHelper /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. /// The number of array elements present. /// The size of the primitive or struct array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int SizeOf(int elementCount) where T : unmanaged => SizeOf() * elementCount; @@ -688,6 +932,7 @@ public static unsafe class MemoryHelper /// The number of array elements present. /// If set to true; will return the size of an element after marshalling. /// The size of the primitive or struct array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int SizeOf(int elementCount, bool marshal) => SizeOf(marshal) * elementCount; @@ -701,9 +946,10 @@ public static unsafe class MemoryHelper /// Amount of bytes to allocate. /// The alignment of the allocation. /// Pointer to the allocated region. - 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)); } /// @@ -712,9 +958,10 @@ public static unsafe class MemoryHelper /// Amount of bytes to allocate. /// The alignment of the allocation. /// Pointer to the allocated region. - 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)); } /// @@ -723,9 +970,10 @@ public static unsafe class MemoryHelper /// Amount of bytes to allocate. /// The alignment of the allocation. /// Pointer to the allocated region. - 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)); } /// @@ -734,9 +982,10 @@ public static unsafe class MemoryHelper /// Amount of bytes to allocate. /// The alignment of the allocation. /// Pointer to the allocated region. - 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)); } /// @@ -745,9 +994,10 @@ public static unsafe class MemoryHelper /// Amount of bytes to allocate. /// The alignment of the allocation. /// Pointer to the allocated region. - 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)); } /// @@ -756,15 +1006,15 @@ public static unsafe class MemoryHelper /// The memory you are freeing must be allocated with game allocators. /// Position at which the memory to be freed is located. /// Amount of bytes to free. - 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 diff --git a/Dalamud/Plugin/Services/IContextMenu.cs b/Dalamud/Plugin/Services/IContextMenu.cs new file mode 100644 index 000000000..4d792116d --- /dev/null +++ b/Dalamud/Plugin/Services/IContextMenu.cs @@ -0,0 +1,37 @@ +using Dalamud.Game.Gui.ContextMenu; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; + +namespace Dalamud.Plugin.Services; + +/// +/// This class provides methods for interacting with the game's context menu. +/// +public interface IContextMenu +{ + /// + /// A delegate type used for the event. + /// + /// Information about the currently opening menu. + public delegate void OnMenuOpenedDelegate(MenuOpenedArgs args); + + /// + /// Event that gets fired every time the game framework updates. + /// + event OnMenuOpenedDelegate OnMenuOpened; + + /// + /// Adds a menu item to a context menu. + /// + /// The type of context menu to add the item to. + /// The item to add. + void AddMenuItem(ContextMenuType menuType, MenuItem item); + + /// + /// Removes a menu item from a context menu. + /// + /// The type of context menu to remove the item from. + /// The item to add. + /// if the item was removed, if it was not found. + bool RemoveMenuItem(ContextMenuType menuType, MenuItem item); +} diff --git a/Dalamud/Plugin/Services/IGameConfig.cs b/Dalamud/Plugin/Services/IGameConfig.cs index 8e9b48d83..c69fa906a 100644 --- a/Dalamud/Plugin/Services/IGameConfig.cs +++ b/Dalamud/Plugin/Services/IGameConfig.cs @@ -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; /// /// This class represents the game's configuration. /// +/// +/// Accessing -typed properties such as , directly or indirectly +/// via , +/// , or alike will block, if the game is not done loading.
+/// Therefore, avoid accessing configuration from your plugin constructor, especially if your plugin sets +/// to 2 and to true. +/// If property access from the plugin constructor is desired, do the value retrieval asynchronously via +/// ; do not wait for the result right away. +///
public interface IGameConfig { /// diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index 7edb1c61d..69c7c32e8 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -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 /// [Pure] public IDalamudTextureWrap GetDalamudTextureWrap(DalamudAsset asset) => - ExtractResult(this.GetDalamudTextureWrapAsync(asset)); + this.GetDalamudTextureWrapAsync(asset).Result; /// [Pure] @@ -332,8 +334,6 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA } } - private static T ExtractResult(Task t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult(); - private Task TransformImmediate(Task task, Func transformer) { if (task.IsCompletedSuccessfully) diff --git a/Dalamud/Storage/Assets/IDalamudAssetManager.cs b/Dalamud/Storage/Assets/IDalamudAssetManager.cs index 1202891b8..643eef18c 100644 --- a/Dalamud/Storage/Assets/IDalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/IDalamudAssetManager.cs @@ -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. /// -internal interface IDalamudAssetManager +public interface IDalamudAssetManager { /// /// Gets the shared texture wrap for . diff --git a/Dalamud/Utility/ArrayExtensions.cs b/Dalamud/Utility/ArrayExtensions.cs index fa6e3dbe9..5b6ce2332 100644 --- a/Dalamud/Utility/ArrayExtensions.cs +++ b/Dalamud/Utility/ArrayExtensions.cs @@ -97,4 +97,76 @@ internal static class ArrayExtensions /// casted as a if it is one; otherwise the result of . public static IReadOnlyCollection AsReadOnlyCollection(this IEnumerable array) => array as IReadOnlyCollection ?? array.ToArray(); + + /// + public static int FindIndex(this IReadOnlyList list, Predicate match) + => list.FindIndex(0, list.Count, match); + + /// + public static int FindIndex(this IReadOnlyList list, int startIndex, Predicate match) + => list.FindIndex(startIndex, list.Count - startIndex, match); + + /// + public static int FindIndex(this IReadOnlyList list, int startIndex, int count, Predicate 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; + } + + /// + public static int FindLastIndex(this IReadOnlyList list, Predicate match) + => list.FindLastIndex(list.Count - 1, list.Count, match); + + /// + public static int FindLastIndex(this IReadOnlyList list, int startIndex, Predicate match) + => list.FindLastIndex(startIndex, startIndex + 1, match); + + /// + public static int FindLastIndex(this IReadOnlyList list, int startIndex, int count, Predicate 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; + } } diff --git a/Dalamud/Utility/DisposeSafety.cs b/Dalamud/Utility/DisposeSafety.cs index 909c4e932..8ac891e0a 100644 --- a/Dalamud/Utility/DisposeSafety.cs +++ b/Dalamud/Utility/DisposeSafety.cs @@ -39,21 +39,23 @@ public static class DisposeSafety public static IDisposable ToDisposableIgnoreExceptions(this Task 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 + } + } + })); } /// @@ -102,25 +104,26 @@ public static class DisposeSafety if (disposables is not T[] array) array = disposables?.ToArray() ?? Array.Empty(); - return Disposable.Create(() => - { - List exceptions = null; - foreach (var d in array) + return Disposable.Create( + () => { - try + List 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); + }); } /// @@ -137,7 +140,11 @@ public static class DisposeSafety public event Action? AfterDispose; /// - public void EnsureCapacity(int capacity) => this.objects.EnsureCapacity(capacity); + public void EnsureCapacity(int capacity) + { + lock (this.objects) + this.objects.EnsureCapacity(capacity); + } /// /// The parameter. @@ -145,7 +152,10 @@ public static class DisposeSafety public T? Add(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? Add(Func? 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 to be disposed later. /// /// Disposables. - public void AddRange(IEnumerable ds) => - this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d))); + public void AddRange(IEnumerable ds) + { + lock (this.objects) + this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d))); + } /// /// Queue all the given to be run later. /// /// Actions. - public void AddRange(IEnumerable ds) => - this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d))); + public void AddRange(IEnumerable ds) + { + lock (this.objects) + this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d))); + } /// /// Queue all the given returning to be run later. /// /// Func{Task}s. - public void AddRange(IEnumerable?> ds) => - this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d))); + public void AddRange(IEnumerable?> ds) + { + lock (this.objects) + this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d))); + } /// /// Queue all the given to be disposed later. /// /// GCHandles. - public void AddRange(IEnumerable ds) => - this.objects.AddRange(ds.Select(d => (object)this.CheckAdd(d))); + public void AddRange(IEnumerable ds) + { + lock (this.objects) + this.objects.AddRange(ds.Select(d => (object)this.CheckAdd(d))); + } /// /// Cancel all pending disposals. @@ -213,9 +244,12 @@ public static class DisposeSafety /// Use this after successful initialization of multiple disposables. 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(); + } } /// @@ -264,11 +298,17 @@ public static class DisposeSafety this.BeforeDispose?.InvokeSafely(this); List? 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? 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); } } } diff --git a/Dalamud/Utility/EventHandlerExtensions.cs b/Dalamud/Utility/EventHandlerExtensions.cs index d05ad6ea5..9bb35a8f1 100644 --- a/Dalamud/Utility/EventHandlerExtensions.cs +++ b/Dalamud/Utility/EventHandlerExtensions.cs @@ -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 } } + /// + /// Replacement for Invoke() on OnMenuOpenedDelegate to catch exceptions that stop event propagation in case + /// of a thrown Exception inside of an invocation. + /// + /// The OnMenuOpenedDelegate in question. + /// Templated argument for Action. + public static void InvokeSafely(this IContextMenu.OnMenuOpenedDelegate? openedDelegate, MenuOpenedArgs argument) + { + if (openedDelegate == null) + return; + + foreach (var action in openedDelegate.GetInvocationList().Cast()) + { + HandleInvoke(() => action(argument)); + } + } + private static void HandleInvoke(Action act) { try diff --git a/Dalamud/Utility/IDeferredDisposable.cs b/Dalamud/Utility/IDeferredDisposable.cs new file mode 100644 index 000000000..41a7dd8d3 --- /dev/null +++ b/Dalamud/Utility/IDeferredDisposable.cs @@ -0,0 +1,12 @@ +namespace Dalamud.Utility; + +/// +/// An extension of which makes queue +/// to be called at a later time. +/// +internal interface IDeferredDisposable : IDisposable +{ + /// Actually dispose the object. + /// Not to be called from the code that uses the end object. + void RealDispose(); +} diff --git a/Dalamud/Utility/RollingList.cs b/Dalamud/Utility/RollingList.cs new file mode 100644 index 000000000..9ca012be4 --- /dev/null +++ b/Dalamud/Utility/RollingList.cs @@ -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 +{ + /// + /// A list with limited capacity holding items of type . + /// Adding further items will result in the list rolling over. + /// + /// Item type. + /// + /// Implemented as a circular list using a internally. + /// Insertions and Removals are not supported. + /// Not thread-safe. + /// + internal class RollingList : IList + { + private List items; + private int size; + private int firstIndex; + + /// Initializes a new instance of the class. + /// size. + /// Internal initial capacity. + public RollingList(int size, int capacity) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(size), size, 0); + capacity = Math.Min(capacity, size); + this.size = size; + this.items = new List(capacity); + } + + /// Initializes a new instance of the class. + /// size. + public RollingList(int size) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(size), size, 0); + this.size = size; + this.items = new(); + } + + /// Initializes a new instance of the class. + /// Collection where elements are copied from. + /// size. + public RollingList(IEnumerable items, int size) + { + if (!items.TryGetNonEnumeratedCount(out var capacity)) capacity = 4; + capacity = Math.Min(capacity, size); + this.size = size; + this.items = new List(capacity); + this.AddRange(items); + } + + /// Initializes a new instance of the class. + /// Collection where elements are copied from. + /// size. + /// Internal initial capacity. + public RollingList(IEnumerable 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(capacity); + this.AddRange(items); + } + + /// Gets item count. + public int Count => this.items.Count; + + /// Gets or sets the internal list capacity. + public int Capacity + { + get => this.items.Capacity; + set => this.items.Capacity = Math.Min(value, this.size); + } + + /// Gets or sets rolling list size. + public int Size + { + get => this.size; + set + { + if (value == this.size) return; + if (value > this.size) + { + if (this.firstIndex > 0) + { + this.items = new List(this); + this.firstIndex = 0; + } + } + else // value < this._size + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(value), value, 0); + if (value < this.Count) + { + this.items = new List(this.TakeLast(value)); + this.firstIndex = 0; + } + } + + this.size = value; + } + } + + /// Gets a value indicating whether the item is read only. + public bool IsReadOnly => false; + + /// Gets or sets an item by index. + /// Item index. + /// Item at specified index. + 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; + } + } + + /// Adds an item to this . + /// Item to add. + 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"); + } + + /// Add items to this . + /// Items to add. + public void AddRange(IEnumerable items) + { + if (this.size == 0) return; + foreach (var item in items) this.Add(item); + } + + /// Removes all elements from the + public void Clear() + { + this.items.Clear(); + this.firstIndex = 0; + } + + /// Find the index of a specific item. + /// item to find. + /// Index where is found. -1 if not found. + public int IndexOf(T item) + { + var index = this.items.IndexOf(item); + if (index == -1) return -1; + return this.GetVirtualIndex(index); + } + + /// Not supported. + [SuppressMessage("Documentation Rules", "SA1611", Justification = "Not supported")] + void IList.Insert(int index, T item) => throw new NotSupportedException(); + + /// Not supported. + [SuppressMessage("Documentation Rules", "SA1611", Justification = "Not supported")] + void IList.RemoveAt(int index) => throw new NotSupportedException(); + + /// Find wether an item exists. + /// item to find. + /// Wether is found. + public bool Contains(T item) => this.items.Contains(item); + + /// Copies the content of this list into an array. + /// Array to copy into. + /// index to start coping into. + 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]; + } + } + + /// Not supported. + [SuppressMessage("Documentation Rules", "SA1611", Justification = "Not supported")] + [SuppressMessage("Documentation Rules", "SA1615", Justification = "Not supported")] + bool ICollection.Remove(T item) => throw new NotSupportedException(); + + /// Gets an enumerator for this . + /// enumerator. + public IEnumerator GetEnumerator() + { + for (var index = 0; index < this.items.Count; index++) + { + yield return this.items[this.GetRealIndex(index)]; + } + } + + /// Gets an enumerator for this . + /// enumerator. + 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; + } +} diff --git a/Dalamud/Utility/ThrowHelper.cs b/Dalamud/Utility/ThrowHelper.cs new file mode 100644 index 000000000..647aa92c0 --- /dev/null +++ b/Dalamud/Utility/ThrowHelper.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Dalamud.Utility +{ + /// Helper methods for throwing exceptions. + internal static class ThrowHelper + { + /// Throws a with a specified . + /// Message for the exception. + /// Thrown by this method. + [DoesNotReturn] + public static void ThrowArgumentException(string message) => throw new ArgumentException(message); + + /// Throws a with a specified for a specified . + /// Parameter name. + /// Message for the exception. + /// Thrown by this method. + [DoesNotReturn] + public static void ThrowArgumentOutOfRangeException(string paramName, string message) => throw new ArgumentOutOfRangeException(paramName, message); + + /// Throws a if the specified is less than . + /// value type. + /// Parameter name. + /// Value to compare from. + /// Value to compare with. + /// Thrown by this method if is less than . + public static void ThrowArgumentOutOfRangeExceptionIfLessThan(string paramName, T value, T comparand) where T : IComparable + { +#if NET8_0_OR_GREATER + ArgumentOutOfRangeException.ThrowIfLessThan(value, comparand); +#else + if (Comparer.Default.Compare(value, comparand) <= -1) ThrowArgumentOutOfRangeException(paramName, $"{paramName} must be greater than or equal {comparand}"); +#endif + } + + /// Throws a if the specified is greater than or equal to . + /// value type. + /// Parameter name. + /// Value to compare from. + /// Value to compare with. + /// Thrown by this method if is greater than or equal to. + public static void ThrowArgumentOutOfRangeExceptionIfGreaterThanOrEqual(string paramName, T value, T comparand) where T : IComparable + { +#if NET8_0_OR_GREATER + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(value, comparand); +#else + if (Comparer.Default.Compare(value, comparand) >= 0) ThrowArgumentOutOfRangeException(paramName, $"{paramName} must be less than {comparand}"); +#endif + } + } +} diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index d53c2fe19..65196b3ee 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -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; } + /// + /// Throws a corresponding exception if is true. + /// + /// The result value. + internal static void ThrowOnError(this HRESULT hr) + { + if (hr.FAILED) + Marshal.ThrowExceptionForHR(hr.Value); + } + + /// + /// Calls if the task is incomplete. + /// + /// The task. + /// The exception to set. + internal static void SetExceptionIfIncomplete(this TaskCompletionSource t, Exception ex) + { + if (t.Task.IsCompleted) + return; + try + { + t.SetException(ex); + } + catch + { + // ignore + } + } + + /// + /// Calls if the task is incomplete. + /// + /// The type of the result. + /// The task. + /// The exception to set. + internal static void SetExceptionIfIncomplete(this TaskCompletionSource t, Exception ex) + { + if (t.Task.IsCompleted) + return; + try + { + t.SetException(ex); + } + catch + { + // ignore + } + } + /// /// Print formatted GameObject Information to ImGui. /// diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 1930b6fb4..d4e9f0a1c 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -25,7 +25,7 @@ #include #include #include -#include +#include #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(lpBuffer), size / sizeof(LANGANDCODEPAGE)); + const auto langs = std::span(static_cast(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 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(pOpaque); + const auto& info = *static_cast(pOpaque); if (!SetFilePointerEx(info.h, { .QuadPart = static_cast(info.off + file_ofs) }, nullptr, SEEK_SET)) throw_last_error("fnHandleReader: SetFilePointerEx"); if (DWORD read; !ReadFile(info.h, pBuf, static_cast(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> launcherArgs; auto fullDump = false; + + // IFileSaveDialog only works on STA + CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); std::vector 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> 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 help site for more information.)aa" "\n" "\n" - R"aa(Upload this file (click here) if you want to ask for help in our Discord server.)aa" "\n" + R"aa(For further assistance, please upload a troubleshooting pack to our Discord server.)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(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(dwRefData))(hwnd, uNotification, wParam, lParam); }; config.lpCallbackData = reinterpret_cast(&callback); - - if (submitThread.joinable()) { - submitThread.join(); - submitThread = {}; + + if (pProgressDialog) { + pProgressDialog->StopProgressDialog(); + pProgressDialog->Release(); + pProgressDialog = NULL; } if (shutup) { diff --git a/lib/CoreCLR/boot.cpp b/lib/CoreCLR/boot.cpp index e3db99c4f..54276aad1 100644 --- a/lib/CoreCLR/boot.cpp +++ b/lib/CoreCLR/boot.cpp @@ -27,7 +27,7 @@ void ConsoleTeardown() std::optional 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(result)); return result; } logging::I("Done!"); // =========================================================================== // - return 0; + return S_OK; } diff --git a/lib/CoreCLR/boot.h b/lib/CoreCLR/boot.h index f75077edd..33bc58bbf 100644 --- a/lib/CoreCLR/boot.h +++ b/lib/CoreCLR/boot.h @@ -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, diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index e3bd59106..722a2c512 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit e3bd5910678683a718e68f0f940c88b08c24eba5 +Subproject commit 722a2c512238ac4b5324e3d343b316d8c8633a02