From 0cc28fb39d0f33768f4e391d531359edfff99b73 Mon Sep 17 00:00:00 2001 From: srkizer Date: Tue, 13 Feb 2024 05:56:38 +0900 Subject: [PATCH 01/51] Changes to Dalamud Boot DLL so that it works in WINE (#1111) * Changes to Dalamud Boot DLL so that it works in WINE * Make asm clearer --- Dalamud.Boot/Dalamud.Boot.vcxproj | 14 + Dalamud.Boot/Dalamud.Boot.vcxproj.filters | 10 + Dalamud.Boot/dllmain.cpp | 2 +- Dalamud.Boot/module.def | 5 + Dalamud.Boot/pch.h | 3 - Dalamud.Boot/rewrite_entrypoint.cpp | 301 +++++++++------------ Dalamud.Boot/rewrite_entrypoint_thunks.asm | 82 ++++++ 7 files changed, 235 insertions(+), 182 deletions(-) create mode 100644 Dalamud.Boot/module.def create mode 100644 Dalamud.Boot/rewrite_entrypoint_thunks.asm diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj b/Dalamud.Boot/Dalamud.Boot.vcxproj index ea263d7f9..dd3f57632 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) @@ -70,6 +73,7 @@ false false + module.def @@ -83,9 +87,13 @@ true true + module.def + + + nethost.dll @@ -181,6 +189,12 @@ + + + + + + diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters index 8b4483684..a1b1650e2 100644 --- a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters +++ b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters @@ -147,4 +147,14 @@ + + + Dalamud.Boot DLL + + + + + Dalamud.Boot DLL + + \ No newline at end of file diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp index 2566016e8..cf31b7016 100644 --- a/Dalamud.Boot/dllmain.cpp +++ b/Dalamud.Boot/dllmain.cpp @@ -159,7 +159,7 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { return 0; } -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/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/pch.h b/Dalamud.Boot/pch.h index 3302a44fb..6dda9d03b 100644 --- a/Dalamud.Boot/pch.h +++ b/Dalamud.Boot/pch.h @@ -61,9 +61,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..a47254701 100644 --- a/Dalamud.Boot/rewrite_entrypoint.cpp +++ b/Dalamud.Boot/rewrite_entrypoint.cpp @@ -5,111 +5,87 @@ DWORD 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; @@ -170,10 +146,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,18 +224,6 @@ 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. @@ -263,9 +234,9 @@ std::wstring to_utf16(const std::string& str, UINT codePage = CP_UTF8, bool erro /// 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" DWORD WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath, const wchar_t* pcwzLoadInfo) { try { - const auto base_address = reinterpret_cast(get_mapped_image_base_address(hProcess, pcwzPath)); + const auto base_address = static_cast(get_mapped_image_base_address(hProcess, pcwzPath)); IMAGE_DOS_HEADER dos_header{}; union { @@ -279,60 +250,35 @@ DllExport DWORD WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t* ? 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()); + auto standalone_rewrittenentrypoint = thunks::create_standalone_rewrittenentrypoint(get_path_from_local_module(g_hModule)); + auto entrypoint_replacement = thunks::create_entrypointreplacement(); - 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()); - - auto load_info = from_utf16(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); + std::vector buffer(sizeof(RewrittenEntryPointParameters) + entrypoint_replacement.size() + load_info.size() + standalone_rewrittenentrypoint.size()); // 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]), - }; + 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. + 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()); + + 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. 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); + thunks::fill_placeholders(entrypoint_replacement.data(), remote_buffer + sizeof params + entrypoint_replacement.size() + load_info.size()); + // Overwrite remote process' entry point with a thunk that will load our DLLs and call our trampoline function. + write_process_memory_or_throw(hProcess, entrypoint, entrypoint_replacement.data(), entrypoint_replacement.size()); return 0; } catch (const std::exception& e) { @@ -341,44 +287,43 @@ DllExport DWORD WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t* } } -/// @deprecated -DllExport DWORD WINAPI RewriteRemoteEntryPoint(HANDLE hProcess, const wchar_t* pcwzPath, const char* pcszLoadInfo) { - return RewriteRemoteEntryPointW(hProcess, pcwzPath, to_utf16(pcszLoadInfo).c_str()); +static void AbortRewrittenEntryPoint(DWORD err, const std::wstring& clue) { + 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 (MessageBoxW(nullptr, std::format( + L"Failed to load Dalamud. Load game without Dalamud(yes) or abort(no)?\n\nError: 0x{:08X} {}\n\n{}", + err, pwszMsg ? pwszMsg : L"", clue).c_str(), + L"Dalamud.Boot", MB_OK | MB_YESNO) == IDNO) + ExitProcess(-1); } /// @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) { + const auto pOriginalEntryPointBytes = reinterpret_cast(¶ms) + sizeof(params); + const auto pLoadInfo = pOriginalEntryPointBytes + params.entrypointLength; - // 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); + // Restore original entry point. + // Use WriteProcessMemory instead of memcpy to avoid having to fiddle with VirtualProtect. + if (SIZE_T written; !WriteProcessMemory(GetCurrentProcess(), params.pEntrypoint, pOriginalEntryPointBytes, params.entrypointLength, &written)) + return AbortRewrittenEntryPoint(GetLastError(), L"WriteProcessMemory(entrypoint restoration)"); - // Make a copy of load info, as the whole params will be freed after this code block. - loadInfo = params.pLoadInfo; - } + const auto hMainThreadContinue = CreateEventW(nullptr, true, false, nullptr); + if (!hMainThreadContinue) + return AbortRewrittenEntryPoint(GetLastError(), L"CreateEventW"); - 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); - } - }, ¶ms, 0, nullptr); - if (!params.hMainThread) - ExitProcess(-1); + if (const auto result = InitializeImpl(pLoadInfo, hMainThreadContinue)) + return AbortRewrittenEntryPoint(result, L"InitializeImpl"); - CloseHandle(params.hMainThread); - WaitForSingleObject(params.hMainThreadContinue, INFINITE); - VirtualFree(params.pAllocation, 0, MEM_RELEASE); + 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 From 7e78b6293b0de07083d8c216dd76843e3aafa159 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 13 Feb 2024 07:31:49 +0900 Subject: [PATCH 02/51] Make RewriteRemoteEntryPointW report IErrorInfo, VirtualProtectEx before WriteProcessMemory --- Dalamud.Boot/Dalamud.Boot.vcxproj | 2 +- Dalamud.Boot/dllmain.cpp | 8 +- Dalamud.Boot/pch.h | 3 + Dalamud.Boot/rewrite_entrypoint.cpp | 133 +++++++++++++++++++---- Dalamud.Boot/utils.cpp | 44 ++++++-- Dalamud.Boot/utils.h | 5 + Dalamud.Injector.Boot/main.cpp | 10 +- Dalamud.Injector/EntryPoint.cs | 160 +++++++++++++++------------- lib/CoreCLR/boot.cpp | 10 +- lib/CoreCLR/boot.h | 2 +- 10 files changed, 254 insertions(+), 123 deletions(-) diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj b/Dalamud.Boot/Dalamud.Boot.vcxproj index dd3f57632..ab68c1ec0 100644 --- a/Dalamud.Boot/Dalamud.Boot.vcxproj +++ b/Dalamud.Boot/Dalamud.Boot.vcxproj @@ -199,4 +199,4 @@ - \ No newline at end of file + diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp index cf31b7016..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,7 +156,7 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { entrypoint_fn(lpParam, hMainThreadContinue); logging::I("Done!"); - return 0; + return S_OK; } extern "C" DWORD WINAPI Initialize(LPVOID lpParam) { diff --git a/Dalamud.Boot/pch.h b/Dalamud.Boot/pch.h index 6dda9d03b..a09882c74 100644 --- a/Dalamud.Boot/pch.h +++ b/Dalamud.Boot/pch.h @@ -26,6 +26,9 @@ // MSVC Compiler Intrinsic #include +// COM +#include + // C++ Standard Libraries #include #include diff --git a/Dalamud.Boot/rewrite_entrypoint.cpp b/Dalamud.Boot/rewrite_entrypoint.cpp index a47254701..6ece3665c 100644 --- a/Dalamud.Boot/rewrite_entrypoint.cpp +++ b/Dalamud.Boot/rewrite_entrypoint.cpp @@ -1,8 +1,9 @@ #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 { char* pEntrypoint; @@ -102,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) @@ -227,15 +229,18 @@ void* get_mapped_image_base_address(HANDLE hProcess, const std::filesystem::path /// @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. /// -extern "C" 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 { + 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{}; @@ -244,21 +249,34 @@ extern "C" 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 standalone_rewrittenentrypoint = thunks::create_standalone_rewrittenentrypoint(get_path_from_local_module(g_hModule)); + 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); + + last_operation = L"thunks::create_entrypointreplacement()"; auto entrypoint_replacement = thunks::create_entrypointreplacement(); + last_operation = L"unicode::convert(pcwzLoadInfo)"; auto load_info = unicode::convert(pcwzLoadInfo); load_info.resize(load_info.size() + 1); //ensure null termination - std::vector buffer(sizeof(RewrittenEntryPointParameters) + entrypoint_replacement.size() + load_info.size() + standalone_rewrittenentrypoint.size()); + 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. + 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()); @@ -266,24 +284,51 @@ extern "C" DWORD WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t* params.pEntrypoint = entrypoint; // 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()); + 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()); + // 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()); - return 0; + 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()); + + 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; } } @@ -300,8 +345,9 @@ static void AbortRewrittenEntryPoint(DWORD err, const std::wstring& clue) { nullptr); if (MessageBoxW(nullptr, std::format( - L"Failed to load Dalamud. Load game without Dalamud(yes) or abort(no)?\n\nError: 0x{:08X} {}\n\n{}", - err, pwszMsg ? pwszMsg : L"", clue).c_str(), + L"Failed to load Dalamud. Load game without Dalamud(yes) or abort(no)?\n\n{}\n\n{}", + utils::format_win32_error(err), + clue).c_str(), L"Dalamud.Boot", MB_OK | MB_YESNO) == IDNO) ExitProcess(-1); } @@ -309,21 +355,62 @@ static void AbortRewrittenEntryPoint(DWORD err, const std::wstring& clue) { /// @brief Entry point function "called" instead of game's original main entry point. /// @param params Parameters set up from RewriteRemoteEntryPoint. extern "C" void WINAPI RewrittenEntryPoint_AdjustedStack(RewrittenEntryPointParameters & params) { - const auto pOriginalEntryPointBytes = reinterpret_cast(¶ms) + sizeof(params); - const auto pLoadInfo = pOriginalEntryPointBytes + params.entrypointLength; + HANDLE hMainThreadContinue = nullptr; + auto hr = S_OK; + std::wstring last_operation; + std::wstring exc_msg; + SetLastError(ERROR_SUCCESS); - // Restore original entry point. - // Use WriteProcessMemory instead of memcpy to avoid having to fiddle with VirtualProtect. - if (SIZE_T written; !WriteProcessMemory(GetCurrentProcess(), params.pEntrypoint, pOriginalEntryPointBytes, params.entrypointLength, &written)) - return AbortRewrittenEntryPoint(GetLastError(), L"WriteProcessMemory(entrypoint restoration)"); + try { + const auto pOriginalEntryPointBytes = reinterpret_cast(¶ms) + sizeof(params); + const auto pLoadInfo = pOriginalEntryPointBytes + params.entrypointLength; - const auto hMainThreadContinue = CreateEventW(nullptr, true, false, nullptr); - if (!hMainThreadContinue) - return AbortRewrittenEntryPoint(GetLastError(), L"CreateEventW"); + // 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); - if (const auto result = InitializeImpl(pLoadInfo, hMainThreadContinue)) - return AbortRewrittenEntryPoint(result, L"InitializeImpl"); + 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); + } + + 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); - WaitForSingleObject(hMainThreadContinue, INFINITE); VirtualFree(¶ms, 0, MEM_RELEASE); } diff --git a/Dalamud.Boot/utils.cpp b/Dalamud.Boot/utils.cpp index 62a9d7055..419ee6397 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()); } @@ -654,3 +660,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..fef920f60 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>> @@ -275,4 +278,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.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..9e2b95657 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(); @@ -800,12 +812,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/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, From 0854b6d0257258568bdb21021ccdfd641700f7a5 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 13 Feb 2024 02:12:54 +0100 Subject: [PATCH 03/51] Update ClientStructs (#1643) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index e3bd59106..cb30048d0 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit e3bd5910678683a718e68f0f940c88b08c24eba5 +Subproject commit cb30048d00e9b9d0c24aa9f12c98de3590e72371 From 3b3823d4e6847b2501e1d1cf45882359fd4857fb Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:08:36 +0100 Subject: [PATCH 04/51] Update ClientStructs (#1644) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index cb30048d0..4b13c01e2 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit cb30048d00e9b9d0c24aa9f12c98de3590e72371 +Subproject commit 4b13c01e2f60143f24698a6280255fb1aba7ab63 From 34daa73612b0b6419734c4d8c457a3933b6ca22b Mon Sep 17 00:00:00 2001 From: srkizer Date: Wed, 14 Feb 2024 05:09:46 +0900 Subject: [PATCH 05/51] Implement FontChooserDialog (#1637) * Implement FontChooserDialog * Minor fixes * Fixes 2 * Add Reset default font button * Add failsafe * reduce uninteresting exception message * Add remarks to use AttachExtraGlyphsForDalamudLanguage * Support advanced font configuration options * fixes * Shift ui elements * more fixes * Add To(Localized)String for IFontSpec * Untie GlobalFontScale from default font size * Layout fixes * Make UiBuilder.DefaultFontSize point to user configured value * Update example for NewDelegateFontHandle * Font interfaces: write notes on not intended for plugins to implement * Update default gamma to 1.7 to match closer to prev behavior (1.4**2) * Fix console window layout --- .../Internal/DalamudConfiguration.cs | 9 +- .../DalamudAssetFontAndFamilyId.cs | 87 ++ .../DalamudDefaultFontAndFamilyId.cs | 77 ++ .../FontIdentifier/GameFontAndFamilyId.cs | 81 ++ .../Interface/FontIdentifier/IFontFamilyId.cs | 102 ++ Dalamud/Interface/FontIdentifier/IFontId.cs | 40 + Dalamud/Interface/FontIdentifier/IFontSpec.cs | 50 + .../IObjectWithLocalizableName.cs | 76 ++ .../FontIdentifier/SingleFontSpec.cs | 155 +++ .../FontIdentifier/SystemFontFamilyId.cs | 181 +++ .../Interface/FontIdentifier/SystemFontId.cs | 163 +++ .../SingleFontChooserDialog.cs | 1117 +++++++++++++++++ .../Interface/Internal/InterfaceManager.cs | 9 +- .../Internal/Windows/ConsoleWindow.cs | 21 +- .../Widgets/GamePrebakedFontsTestWidget.cs | 87 +- .../Windows/Settings/SettingsWindow.cs | 4 +- .../Windows/Settings/Tabs/SettingsTabLook.cs | 79 +- .../Interface/ManagedFontAtlas/IFontAtlas.cs | 9 +- .../IFontAtlasBuildToolkit.cs | 3 +- .../IFontAtlasBuildToolkitPostBuild.cs | 3 +- .../IFontAtlasBuildToolkitPreBuild.cs | 15 +- .../Interface/ManagedFontAtlas/IFontHandle.cs | 3 +- .../ManagedFontAtlas/ILockedImFont.cs | 3 +- .../FontAtlasFactory.BuildToolkit.cs | 42 +- .../FontAtlasFactory.Implementation.cs | 3 +- .../Internals/FontAtlasFactory.cs | 24 +- .../ManagedFontAtlas/SafeFontConfig.cs | 2 +- Dalamud/Interface/UiBuilder.cs | 10 +- Dalamud/Interface/Utility/ImGuiHelpers.cs | 19 + Dalamud/Utility/ArrayExtensions.cs | 72 ++ Dalamud/Utility/Util.cs | 13 + 31 files changed, 2478 insertions(+), 81 deletions(-) create mode 100644 Dalamud/Interface/FontIdentifier/DalamudAssetFontAndFamilyId.cs create mode 100644 Dalamud/Interface/FontIdentifier/DalamudDefaultFontAndFamilyId.cs create mode 100644 Dalamud/Interface/FontIdentifier/GameFontAndFamilyId.cs create mode 100644 Dalamud/Interface/FontIdentifier/IFontFamilyId.cs create mode 100644 Dalamud/Interface/FontIdentifier/IFontId.cs create mode 100644 Dalamud/Interface/FontIdentifier/IFontSpec.cs create mode 100644 Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs create mode 100644 Dalamud/Interface/FontIdentifier/SingleFontSpec.cs create mode 100644 Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs create mode 100644 Dalamud/Interface/FontIdentifier/SystemFontId.cs create mode 100644 Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 66c2745c5..957be12b9 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. 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..e4d931605 --- /dev/null +++ b/Dalamud/Interface/FontIdentifier/IFontSpec.cs @@ -0,0 +1,50 @@ +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. + 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..0604b22ea --- /dev/null +++ b/Dalamud/Interface/FontIdentifier/SingleFontSpec.cs @@ -0,0 +1,155 @@ +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( + () => + { + var roundUnit = tk.IsGlobalScaleIgnored(font) ? 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; + } + } + + // `/ roundUnit` = `* scale` + var dax = MathF.Round(this.LetterSpacing / roundUnit / roundUnit) * roundUnit; + var dxy0 = this.GlyphOffset / roundUnit; + + dxy0 /= roundUnit; + dxy0.X = MathF.Round(dxy0.X); + dxy0.Y = 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..410bf7d18 --- /dev/null +++ b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs @@ -0,0 +1,1117 @@ +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.IgnoreGlobalScale(e.Font)) + .OnPostBuild(e => e.Font.AdjustGlyphMetrics(1f / e.Scale))); + } + 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 changed; + + 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/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 6cf4a8b90..6d93b4bd7 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -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/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 53821d9df..1b9890a75 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -152,8 +152,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 +176,10 @@ 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; lock (this.renderLock) { @@ -242,8 +246,7 @@ internal class ConsoleWindow : Window, IDisposable } // 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 +264,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 +283,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(); } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs index b486cc7d9..84682e7c2 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; @@ -24,6 +27,8 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable { 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 useWordWrap; @@ -111,29 +116,32 @@ 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.useGlobalScale; + 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 ??= @@ -141,6 +149,41 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable nameof(GamePrebakedFontsTestWidget), FontAtlasAutoRebuildMode.Async, this.useGlobalScale); + this.fontDialogHandle ??= this.fontSpec.CreateFontHandle(this.privateAtlas); + + fixed (byte* labelPtr = "Test Input"u8) + { + if (!this.useGlobalScale) + 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.useGlobalScale) + ImGuiNative.igSetWindowFontScale(1); + } + this.fontHandles ??= Enum.GetValues() .Where(x => x.GetAttribute() is not null) @@ -227,6 +270,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/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/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..d824eca52 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs @@ -5,7 +5,8 @@ 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 { diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs index 38d8d2fe8..9ab480374 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs @@ -1,6 +1,7 @@ using System.IO; using System.Runtime.InteropServices; +using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Utility; @@ -10,6 +11,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, @@ -52,6 +54,12 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit /// True if ignored. bool IsGlobalScaleIgnored(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 .
/// It WILL crash if you try to use a memory pointer allocated in some other way.
@@ -134,7 +142,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..396c8b26a 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. @@ -162,6 +164,9 @@ internal sealed partial class FontAtlasFactory /// 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( @@ -314,18 +319,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 }); @@ -531,6 +550,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..4968bc891 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -658,7 +658,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 +679,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..d3bc976f2 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 . 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..7a3eb6fb6 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. /// 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/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/Util.cs b/Dalamud/Utility/Util.cs index d53c2fe19..f5ad8b999 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -22,6 +22,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 +687,16 @@ 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); + } + /// /// Print formatted GameObject Information to ImGui. /// From 3283d0cc114867a2e66af8bc420664a4208d03f6 Mon Sep 17 00:00:00 2001 From: srkizer Date: Wed, 14 Feb 2024 06:09:23 +0900 Subject: [PATCH 06/51] Turn IDalamudAssetManager public (#1638) --- Dalamud/Storage/Assets/IDalamudAssetManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 . From 86504dfd9e1989cd28006e32ed56359d78b60ba5 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Tue, 13 Feb 2024 23:25:18 +0100 Subject: [PATCH 07/51] build: 9.0.0.18 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index f58a0c47a..55710cf0b 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.17 + 9.0.0.18 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From cf3091b4099b9ca365360088c8409c93a71a2361 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 14 Feb 2024 13:05:45 +0900 Subject: [PATCH 08/51] If docked, force default font on window decoration ImGui docking functions are called outside our drawing context (from ImGui::NewFrame), which includes most of dock-related drawing calls. However, ImGui::RenderWindowDecoration is called from ImGui::Begin, which may be under the effect of other pushed font. As IG::RWD references to the ImDrawList irrelevant to the global shared state, it was trying to draw a rectangle referring to a pixel that is not guaranteed to be a white pixel. This commit fixes that by forcing the use of the default font for IG::RWD when the window is docked. --- .../ImGuiClipboardFunctionProvider.cs | 1 - .../Internals/ImGuiDockNodeUpdateForceFont.cs | 78 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/ImGuiDockNodeUpdateForceFont.cs 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/ManagedFontAtlas/Internals/ImGuiDockNodeUpdateForceFont.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/ImGuiDockNodeUpdateForceFont.cs new file mode 100644 index 000000000..a2a30429a --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/ImGuiDockNodeUpdateForceFont.cs @@ -0,0 +1,78 @@ +using System.Diagnostics; +using System.Linq; + +using Dalamud.Hooking; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Forces ImGui::RenderWindowDecorations to use the default font. +/// Fixes dock node draw using shared data across different draw lists. +/// TODO: figure out how to synchronize ImDrawList::_Data and ImDrawList::Push/PopTextureID across different instances. +/// It might be better to just special-case that particular function, +/// as no other code touches ImDrawList that is irrelevant to the global shared state, +/// with the exception of Dock... functions which are called from ImGui::NewFrame, +/// which are guaranteed to use the global default font. +/// +[ServiceManager.EarlyLoadedService] +internal class ImGuiRenderWindowDecorationsForceFont : IServiceType, IDisposable +{ + private const int CImGuiRenderWindowDecorationsOffset = 0x461B0; + private const int CImGuiWindowDockIsActiveOffset = 0x401; + + private readonly Hook hook; + + [ServiceManager.ServiceConstructor] + private ImGuiRenderWindowDecorationsForceFont(InterfaceManager.InterfaceManagerWithScene imws) + { + // Effectively waiting for ImGui to become available. + Debug.Assert(ImGuiHelpers.IsImGuiInitialized, "IMWS initialized but IsImGuiInitialized is false?"); + + var cimgui = Process.GetCurrentProcess().Modules.Cast() + .First(x => x.ModuleName == "cimgui.dll") + .BaseAddress; + this.hook = Hook.FromAddress( + cimgui + CImGuiRenderWindowDecorationsOffset, + this.ImGuiRenderWindowDecorationsDetour); + this.hook.Enable(); + } + + private delegate void ImGuiRenderWindowDecorationsDelegate( + nint window, + nint titleBarRectPtr, + byte titleBarIsHighlight, + byte handleBordersAndResizeGrips, + int resizeGripCount, + nint resizeGripColPtr, + float resizeGripDrawSize); + + /// + public void Dispose() => this.hook.Dispose(); + + private unsafe void ImGuiRenderWindowDecorationsDetour( + nint window, + nint titleBarRectPtr, + byte titleBarIsHighlight, + byte handleBordersAndResizeGrips, + int resizeGripCount, + nint resizeGripColPtr, + float resizeGripDrawSize) + { + using ( + ((byte*)window)![CImGuiWindowDockIsActiveOffset] != 0 + ? Service.Get().DefaultFontHandle?.Push() + : null) + { + this.hook.Original( + window, + titleBarRectPtr, + titleBarIsHighlight, + handleBordersAndResizeGrips, + resizeGripCount, + resizeGripColPtr, + resizeGripDrawSize); + } + } +} From 2de9c8ed5b88e4f4ba0ecbc05c1bfc46cc10495b Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 14 Feb 2024 21:59:20 +0900 Subject: [PATCH 09/51] Fix insufficient ImDrawList implementation --- Dalamud/Interface/Internal/DalamudIme.cs | 4 +- .../Internal/ImGuiDrawListFixProvider.cs | 124 ++++++++++++++++++ .../ManagedAsserts/ImGuiContextOffsets.cs | 2 + .../Internals/ImGuiDockNodeUpdateForceFont.cs | 78 ----------- 4 files changed, 128 insertions(+), 80 deletions(-) create mode 100644 Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/ImGuiDockNodeUpdateForceFont.cs 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/ImGuiDrawListFixProvider.cs b/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs new file mode 100644 index 000000000..cdf7ab23e --- /dev/null +++ b/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs @@ -0,0 +1,124 @@ +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) + { + if (rounding < 0 || (flags & ImDrawFlags.RoundCornersMask) == ImDrawFlags.RoundCornersMask) + { + 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 + { + 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/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/ManagedFontAtlas/Internals/ImGuiDockNodeUpdateForceFont.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/ImGuiDockNodeUpdateForceFont.cs deleted file mode 100644 index a2a30429a..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/ImGuiDockNodeUpdateForceFont.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Diagnostics; -using System.Linq; - -using Dalamud.Hooking; -using Dalamud.Interface.Internal; -using Dalamud.Interface.Utility; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Forces ImGui::RenderWindowDecorations to use the default font. -/// Fixes dock node draw using shared data across different draw lists. -/// TODO: figure out how to synchronize ImDrawList::_Data and ImDrawList::Push/PopTextureID across different instances. -/// It might be better to just special-case that particular function, -/// as no other code touches ImDrawList that is irrelevant to the global shared state, -/// with the exception of Dock... functions which are called from ImGui::NewFrame, -/// which are guaranteed to use the global default font. -/// -[ServiceManager.EarlyLoadedService] -internal class ImGuiRenderWindowDecorationsForceFont : IServiceType, IDisposable -{ - private const int CImGuiRenderWindowDecorationsOffset = 0x461B0; - private const int CImGuiWindowDockIsActiveOffset = 0x401; - - private readonly Hook hook; - - [ServiceManager.ServiceConstructor] - private ImGuiRenderWindowDecorationsForceFont(InterfaceManager.InterfaceManagerWithScene imws) - { - // Effectively waiting for ImGui to become available. - Debug.Assert(ImGuiHelpers.IsImGuiInitialized, "IMWS initialized but IsImGuiInitialized is false?"); - - var cimgui = Process.GetCurrentProcess().Modules.Cast() - .First(x => x.ModuleName == "cimgui.dll") - .BaseAddress; - this.hook = Hook.FromAddress( - cimgui + CImGuiRenderWindowDecorationsOffset, - this.ImGuiRenderWindowDecorationsDetour); - this.hook.Enable(); - } - - private delegate void ImGuiRenderWindowDecorationsDelegate( - nint window, - nint titleBarRectPtr, - byte titleBarIsHighlight, - byte handleBordersAndResizeGrips, - int resizeGripCount, - nint resizeGripColPtr, - float resizeGripDrawSize); - - /// - public void Dispose() => this.hook.Dispose(); - - private unsafe void ImGuiRenderWindowDecorationsDetour( - nint window, - nint titleBarRectPtr, - byte titleBarIsHighlight, - byte handleBordersAndResizeGrips, - int resizeGripCount, - nint resizeGripColPtr, - float resizeGripDrawSize) - { - using ( - ((byte*)window)![CImGuiWindowDockIsActiveOffset] != 0 - ? Service.Get().DefaultFontHandle?.Push() - : null) - { - this.hook.Original( - window, - titleBarRectPtr, - titleBarIsHighlight, - handleBordersAndResizeGrips, - resizeGripCount, - resizeGripColPtr, - resizeGripDrawSize); - } - } -} From edc5826fe032c3063b53343e26152fe18a47f333 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 15 Feb 2024 03:55:00 +0900 Subject: [PATCH 10/51] Dalamud.Boot: use unicode::convert --- Dalamud.Boot/utils.cpp | 10 ---------- Dalamud.Boot/utils.h | 2 -- Dalamud.Boot/veh.cpp | 14 +++++++------- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/Dalamud.Boot/utils.cpp b/Dalamud.Boot/utils.cpp index 62a9d7055..9dc296c5f 100644 --- a/Dalamud.Boot/utils.cpp +++ b/Dalamud.Boot/utils.cpp @@ -578,16 +578,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) { diff --git a/Dalamud.Boot/utils.h b/Dalamud.Boot/utils.h index ebf48a294..85509cf38 100644 --- a/Dalamud.Boot/utils.h +++ b/Dalamud.Boot/utils.h @@ -264,8 +264,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. diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index eb27acce7..fc8689af7 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -110,13 +110,13 @@ 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"--logpath=\"" + unicode::convert(g_startInfo.BootLogPath) + L"\""); + 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"--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)); if (g_startInfo.BootShowConsole) args.emplace_back(L"--console"); if (g_startInfo.BootEnableEtw) From cce4f03403aecf5d72bac29f6b71673e390dea86 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 15 Feb 2024 04:14:23 +0900 Subject: [PATCH 11/51] Adjust logDir if logDir points to a .log file --- DalamudCrashHandler/DalamudCrashHandler.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 1930b6fb4..4e2f01708 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -518,6 +518,7 @@ 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; @@ -534,8 +535,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,6 +579,7 @@ 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"); @@ -710,6 +716,13 @@ 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; + } + while (true) { std::cout << "Waiting for crash...\n"; From ea43d656361174ae729e8956bd153a4dbeb2ee55 Mon Sep 17 00:00:00 2001 From: srkizer Date: Thu, 15 Feb 2024 07:52:40 +0900 Subject: [PATCH 12/51] Fix memory ownership on AddFontFromImGuiHeapAllocatedMemory (#1651) --- .../FontAtlasFactory.BuildToolkit.cs | 19 ++++++++++++++++++- .../FontAtlasFactory.Implementation.cs | 6 ++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 396c8b26a..a57e6d036 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -185,6 +185,7 @@ internal sealed partial class FontAtlasFactory dataSize, debugTag); + var font = default(ImFontPtr); try { fontConfig.ThrowOnInvalidValues(); @@ -192,6 +193,7 @@ internal sealed partial class FontAtlasFactory var raw = fontConfig.Raw with { FontData = dataPointer, + FontDataOwnedByAtlas = 1, FontDataSize = dataSize, }; @@ -203,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)); @@ -240,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; } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 4968bc891..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 { From a8bb8cbec5e21517ec5d4fd658f0cda10531200e Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Wed, 14 Feb 2024 16:34:45 -0800 Subject: [PATCH 13/51] feat: Add "deref nullptr in hook" crash item - Move all crash items into submenu --- .../Interface/Internal/DalamudInterface.cs | 63 +++++++++++++------ 1 file changed, 43 insertions(+), 20 deletions(-) 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)) From 914cd363fd1b1b70880d7f0d590876406f0919d5 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 15 Feb 2024 01:45:10 +0100 Subject: [PATCH 14/51] Bump Lumina to 3.16.0 --- Dalamud.CorePlugin/Dalamud.CorePlugin.csproj | 2 +- Dalamud/Dalamud.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index d7eb8499c..bf315d99e 100644 --- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -27,7 +27,7 @@ - + diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 55710cf0b..208e6d4ea 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -68,7 +68,7 @@ - + From c8be22e2848ef451c8f356ea8071134e1376729b Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 15 Feb 2024 15:42:30 +0900 Subject: [PATCH 15/51] Do FlushInstructionCache after WriteProcessMemory While not calling this will work on native x64 machines as it's likely a no-op under x64, it is possible that the function does something under emulated environments. As there is no downside to calling this function, this commit makes the behavior more correct. --- Dalamud.Boot/rewrite_entrypoint.cpp | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/Dalamud.Boot/rewrite_entrypoint.cpp b/Dalamud.Boot/rewrite_entrypoint.cpp index 6ece3665c..3a1672af7 100644 --- a/Dalamud.Boot/rewrite_entrypoint.cpp +++ b/Dalamud.Boot/rewrite_entrypoint.cpp @@ -303,6 +303,7 @@ extern "C" HRESULT WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_ // 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) { @@ -332,26 +333,6 @@ extern "C" HRESULT WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_ } } -static void AbortRewrittenEntryPoint(DWORD err, const std::wstring& clue) { - 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 (MessageBoxW(nullptr, std::format( - L"Failed to load Dalamud. Load game without Dalamud(yes) or abort(no)?\n\n{}\n\n{}", - utils::format_win32_error(err), - clue).c_str(), - L"Dalamud.Boot", MB_OK | MB_YESNO) == IDNO) - ExitProcess(-1); -} - /// @brief Entry point function "called" instead of game's original main entry point. /// @param params Parameters set up from RewriteRemoteEntryPoint. extern "C" void WINAPI RewrittenEntryPoint_AdjustedStack(RewrittenEntryPointParameters & params) { @@ -369,6 +350,7 @@ extern "C" void WINAPI RewrittenEntryPoint_AdjustedStack(RewrittenEntryPointPara // 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); hMainThreadContinue = CreateEventW(nullptr, true, false, nullptr); last_operation = L"hMainThreadContinue = CreateEventW"; From f4af8e509b34ddeca5042badfbba3b9437b74888 Mon Sep 17 00:00:00 2001 From: marzent Date: Thu, 15 Feb 2024 00:05:18 +0100 Subject: [PATCH 16/51] make Dalamud handle top-level SEH --- Dalamud.Boot/veh.cpp | 61 +++++++++++++++++++++++----------- Dalamud.Boot/xivfixes.cpp | 45 ------------------------- Dalamud.Boot/xivfixes.h | 1 - Dalamud.Injector/EntryPoint.cs | 5 ++- 4 files changed, 45 insertions(+), 67 deletions(-) diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index fc8689af7..4eeddba88 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; @@ -143,21 +145,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 +155,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; @@ -178,7 +166,7 @@ LONG exception_handler(EXCEPTION_POINTERS* ex) 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,7 +176,7 @@ 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) @@ -217,13 +205,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 +374,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..39cce53c9 100644 --- a/Dalamud.Boot/xivfixes.cpp +++ b/Dalamud.Boot/xivfixes.cpp @@ -513,50 +513,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]"; @@ -598,7 +554,6 @@ 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 } } ) { diff --git a/Dalamud.Boot/xivfixes.h b/Dalamud.Boot/xivfixes.h index 701913c88..f534ad7dd 100644 --- a/Dalamud.Boot/xivfixes.h +++ b/Dalamud.Boot/xivfixes.h @@ -6,7 +6,6 @@ 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 apply_all(bool bApply); diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index 9e2b95657..c784ec1d1 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -394,7 +394,10 @@ namespace Dalamud.Injector 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 List { + "prevent_devicechange_crashes", "disable_game_openprocess_access_check", + "redirect_openprocess", "backup_userdata_save", "prevent_icmphandle_crashes", + }; startInfo.BootDotnetOpenProcessHookMode = 0; startInfo.BootWaitMessageBox |= args.Contains("--msgbox1") ? 1 : 0; startInfo.BootWaitMessageBox |= args.Contains("--msgbox2") ? 2 : 0; From 1020f8a85bea7fe37c33f2f856cae03895bf5749 Mon Sep 17 00:00:00 2001 From: marzent Date: Thu, 15 Feb 2024 23:39:08 +0100 Subject: [PATCH 17/51] add progress dialog to crash handler --- DalamudCrashHandler/DalamudCrashHandler.cpp | 49 +++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 4e2f01708..258ec923d 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #pragma comment(lib, "comctl32.lib") @@ -670,6 +671,7 @@ int main() { std::filesystem::path assetDir, logDir; std::optional> launcherArgs; auto fullDump = false; + CoInitializeEx(nullptr, COINIT_MULTITHREADED); std::vector args; if (int argc = 0; const auto argv = CommandLineToArgvW(GetCommandLineW(), &argc)) { @@ -753,6 +755,35 @@ 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"); + 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); + } + + 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) @@ -791,6 +822,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)) { @@ -805,6 +839,9 @@ 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"; @@ -857,6 +894,9 @@ 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(); @@ -993,11 +1033,20 @@ int main() { }; config.lpCallbackData = reinterpret_cast(&callback); + if (pProgressDialog) + pProgressDialog->SetLine(3, L"Submitting Metrics", FALSE, NULL); + if (submitThread.joinable()) { submitThread.join(); submitThread = {}; } + if (pProgressDialog) { + pProgressDialog->StopProgressDialog(); + pProgressDialog->Release(); + pProgressDialog = NULL; + } + if (shutup) { TerminateProcess(g_hProcess, exinfo.ExceptionRecord.ExceptionCode); return 0; From 6497c626222b4a639d18f59591609aca40f3560f Mon Sep 17 00:00:00 2001 From: srkizer Date: Sat, 17 Feb 2024 01:16:21 +0900 Subject: [PATCH 18/51] Change MemoryHelper to allocate less (#1657) * Change MemoryHelper to allocate less * Use StringBuilder pool for ReadSeStringAsString * fix * Use CreateReadOnlySpanFromNullTerminated where possible --- Dalamud/Game/Internal/DalamudAtkTweaks.cs | 2 +- Dalamud/Interface/Internal/UiDebug.cs | 27 +- Dalamud/Memory/MemoryHelper.cs | 520 ++++++++++++++++------ 3 files changed, 400 insertions(+), 149 deletions(-) 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/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/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 From 307f0fcbe834f16d879f1d58014ee6f0ec3a2771 Mon Sep 17 00:00:00 2001 From: srkizer Date: Sat, 17 Feb 2024 01:19:42 +0900 Subject: [PATCH 19/51] Warn if font files' hashes are unexpected (#1659) --- .../Notifications/NotificationManager.cs | 68 +++++++- .../Internals/FontAtlasFactory.cs | 165 +++++++++++++++++- 2 files changed, 216 insertions(+), 17 deletions(-) diff --git a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs b/Dalamud/Interface/Internal/Notifications/NotificationManager.cs index 67ad3ee8f..34e07be8f 100644 --- a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs +++ b/Dalamud/Interface/Internal/Notifications/NotificationManager.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Numerics; using Dalamud.Interface.Colors; +using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.Utility; using Dalamud.Utility; using ImGuiNET; @@ -68,15 +69,22 @@ internal class NotificationManager : IServiceType /// The title of the notification. /// The type of the notification. /// The time the notification should be displayed for. - public void AddNotification(string content, string? title = null, NotificationType type = NotificationType.None, uint msDelay = NotifyDefaultDismiss) + /// The added notification. + public Notification AddNotification( + string content, + string? title = null, + NotificationType type = NotificationType.None, + uint msDelay = NotifyDefaultDismiss) { - this.notifications.Add(new Notification + var n = new Notification { Content = content, Title = title, NotificationType = type, DurationMs = msDelay, - }); + }; + this.notifications.Add(n); + return n; } /// @@ -97,6 +105,10 @@ internal class NotificationManager : IServiceType continue; } + using var pushedFont = tn.UseMonospaceFont + ? Service.Get().MonoFontHandle?.Push() + : null; + var opacity = tn.GetFadePercent(); var iconColor = tn.Color; @@ -107,8 +119,12 @@ internal class NotificationManager : IServiceType ImGuiHelpers.ForceNextWindowMainViewport(); ImGui.SetNextWindowBgAlpha(opacity); ImGui.SetNextWindowPos(ImGuiHelpers.MainViewport.Pos + new Vector2(viewportSize.X - NotifyPaddingX, viewportSize.Y - NotifyPaddingY - height), ImGuiCond.Always, Vector2.One); - ImGui.Begin(windowName, NotifyToastFlags); + if (tn.Actions.Count == 0) + ImGui.Begin(windowName, NotifyToastFlags); + else + ImGui.Begin(windowName, NotifyToastFlags & ~ImGuiWindowFlags.NoInputs); + ImGui.PushID(tn.NotificationId); ImGui.PushTextWrapPos(viewportSize.X / 3.0f); var wasTitleRendered = false; @@ -162,10 +178,22 @@ internal class NotificationManager : IServiceType ImGui.TextUnformatted(tn.Content); } + foreach (var (caption, action) in tn.Actions) + { + if (ImGui.Button(caption)) + action.InvokeSafely(); + ImGui.SameLine(); + } + + // break ImGui.SameLine(); + ImGui.TextUnformatted(string.Empty); + ImGui.PopStyleColor(); ImGui.PopTextWrapPos(); + ImGui.PopID(); + height += ImGui.GetWindowHeight() + NotifyPaddingMessageY; ImGui.End(); @@ -177,6 +205,8 @@ internal class NotificationManager : IServiceType /// internal class Notification { + private static int notificationIdCounter; + /// /// Possible notification phases. /// @@ -203,20 +233,40 @@ internal class NotificationManager : IServiceType Expired, } + /// + /// Gets the notification ID. + /// + internal int NotificationId { get; } = notificationIdCounter++; + /// /// Gets the type of the notification. /// internal NotificationType NotificationType { get; init; } /// - /// Gets the title of the notification. + /// Gets or sets a value indicating whether to force the use of monospace font. /// - internal string? Title { get; init; } + internal bool UseMonospaceFont { get; set; } /// - /// Gets the content of the notification. + /// Gets the action buttons to attach to this notification. /// - internal string Content { get; init; } + internal List<(string Text, Action ClickCallback)> Actions { get; } = new(); + + /// + /// Gets or sets a value indicating whether this notification has been dismissed. + /// + internal bool Dismissed { get; set; } + + /// + /// Gets or sets the title of the notification. + /// + internal string? Title { get; set; } + + /// + /// Gets or sets the content of the notification. + /// + internal string? Content { get; set; } /// /// Gets the duration of the notification in milliseconds. @@ -283,7 +333,7 @@ internal class NotificationManager : IServiceType { var elapsed = (int)this.ElapsedTime.TotalMilliseconds; - if (elapsed > NotifyFadeInOutTime + this.DurationMs + NotifyFadeInOutTime) + if (elapsed > NotifyFadeInOutTime + this.DurationMs + NotifyFadeInOutTime || this.Dismissed) return Phase.Expired; else if (elapsed > NotifyFadeInOutTime + this.DurationMs) return Phase.FadeOut; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index d3bc976f2..021fc953f 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -1,6 +1,7 @@ using System.Buffers; using System.Collections.Generic; using System.Collections.Immutable; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -8,9 +9,13 @@ using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.Gui; using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Plugin.Services; using Dalamud.Storage.Assets; using Dalamud.Utility; @@ -18,7 +23,11 @@ using ImGuiNET; using ImGuiScene; +using Lumina.Data; using Lumina.Data.Files; +using Lumina.Misc; + +using Newtonsoft.Json; using SharpDX; using SharpDX.Direct3D11; @@ -33,9 +42,43 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals; internal sealed partial class FontAtlasFactory : IServiceType, GamePrebakedFontHandle.IGameFontTextureProvider, IDisposable { + private static readonly Dictionary KnownFontFileDataHashes = new() + { + ["common/font/AXIS_96.fdt"] = 1486212503, + ["common/font/AXIS_12.fdt"] = 1370045105, + ["common/font/AXIS_14.fdt"] = 645957730, + ["common/font/AXIS_18.fdt"] = 899094094, + ["common/font/AXIS_36.fdt"] = 2537048938, + ["common/font/Jupiter_16.fdt"] = 1642196098, + ["common/font/Jupiter_20.fdt"] = 3053628263, + ["common/font/Jupiter_23.fdt"] = 1536194944, + ["common/font/Jupiter_45.fdt"] = 3473589216, + ["common/font/Jupiter_46.fdt"] = 1370962087, + ["common/font/Jupiter_90.fdt"] = 3661420529, + ["common/font/Meidinger_16.fdt"] = 3700692128, + ["common/font/Meidinger_20.fdt"] = 441419856, + ["common/font/Meidinger_40.fdt"] = 203848091, + ["common/font/MiedingerMid_10.fdt"] = 499375313, + ["common/font/MiedingerMid_12.fdt"] = 1925552591, + ["common/font/MiedingerMid_14.fdt"] = 1919733827, + ["common/font/MiedingerMid_18.fdt"] = 1635778987, + ["common/font/MiedingerMid_36.fdt"] = 1190559864, + ["common/font/TrumpGothic_184.fdt"] = 973994576, + ["common/font/TrumpGothic_23.fdt"] = 1967289381, + ["common/font/TrumpGothic_34.fdt"] = 1777971886, + ["common/font/TrumpGothic_68.fdt"] = 1170173741, + ["common/font/font0.tex"] = 514269927, + ["common/font/font1.tex"] = 3616607606, + ["common/font/font2.tex"] = 4166651000, + ["common/font/font3.tex"] = 1264942640, + ["common/font/font4.tex"] = 3534300885, + ["common/font/font5.tex"] = 1041916216, + ["common/font/font6.tex"] = 1247097672, + }; + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); private readonly CancellationTokenSource cancellationTokenSource = new(); - private readonly IReadOnlyDictionary> fdtFiles; + private readonly IReadOnlyDictionary> fdtFiles; private readonly IReadOnlyDictionary[]>> texFiles; private readonly IReadOnlyDictionary> prebakedTextureWraps; private readonly Task defaultGlyphRanges; @@ -67,7 +110,7 @@ internal sealed partial class FontAtlasFactory this.fdtFiles = gffasInfo.ToImmutableDictionary( x => x.Font, - x => Task.Run(() => dataManager.GetFile(x.Attr.Path)!.Data)); + x => Task.Run(() => dataManager.GetFile(x.Attr.Path)!)); var channelCountsTask = texPaths.ToImmutableDictionary( x => x, x => Task.WhenAll( @@ -79,8 +122,8 @@ internal sealed partial class FontAtlasFactory { unsafe { - using var pin = file.AsMemory().Pin(); - var fdt = new FdtFileView(pin.Pointer, file.Length); + using var pin = file.Data.AsMemory().Pin(); + var fdt = new FdtFileView(pin.Pointer, file.Data.Length); return fdt.MaxTextureIndex; } }))); @@ -101,11 +144,13 @@ internal sealed partial class FontAtlasFactory { unsafe { - using var pin = file.Result.AsMemory().Pin(); - var fdt = new FdtFileView(pin.Pointer, file.Result.Length); + using var pin = file.Result.Data.AsMemory().Pin(); + var fdt = new FdtFileView(pin.Pointer, file.Result.Data.Length); return fdt.ToGlyphRanges(); } }); + + Task.Run(this.CheckSanity); } /// @@ -203,12 +248,12 @@ internal sealed partial class FontAtlasFactory /// /// The font family and size. /// The . - public FdtReader GetFdtReader(GameFontFamilyAndSize gffas) => new(ExtractResult(this.fdtFiles[gffas])); + public FdtReader GetFdtReader(GameFontFamilyAndSize gffas) => new(ExtractResult(this.fdtFiles[gffas]).Data); /// public unsafe MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView) { - var arr = ExtractResult(this.fdtFiles[gffas]); + var arr = ExtractResult(this.fdtFiles[gffas]).Data; var handle = arr.AsMemory().Pin(); try { @@ -340,6 +385,110 @@ internal sealed partial class FontAtlasFactory } } + private async Task CheckSanity() + { + var invalidFiles = new Dictionary(); + var texFileTasks = new Dictionary>(); + var foundHashes = new Dictionary(); + foreach (var (gffas, fdtTask) in this.fdtFiles) + { + var fontAttr = gffas.GetAttribute()!; + try + { + foundHashes[fontAttr.Path] = Crc32.Get((await fdtTask).Data); + + foreach (var (task, index) in + (await this.texFiles[fontAttr.TexPathFormat]).Select((x, i) => (x, i))) + texFileTasks[fontAttr.TexPathFormat.Format(index)] = task; + } + catch (Exception e) + { + invalidFiles[fontAttr.Path] = e; + } + } + + foreach (var (path, texTask) in texFileTasks) + { + try + { + var hc = default(HashCode); + hc.AddBytes((await texTask).Data); + foundHashes[path] = Crc32.Get((await texTask).Data); + } + catch (Exception e) + { + invalidFiles[path] = e; + } + } + + foreach (var (path, hashCode) in foundHashes) + { + if (!KnownFontFileDataHashes.TryGetValue(path, out var expectedHashCode)) + continue; + if (expectedHashCode != hashCode) + { + invalidFiles[path] = new InvalidDataException( + $"Expected 0x{expectedHashCode:X08}; got 0x{hashCode:X08}"); + } + } + + var dconf = await Service.GetAsync(); + var nm = await Service.GetAsync(); + var intm = (await Service.GetAsync()).Manager; + var ggui = await Service.GetAsync(); + var cstate = await Service.GetAsync(); + + if (invalidFiles.Any()) + { + Log.Warning("Found {n} font related file(s) with unexpected hash code values.", invalidFiles.Count); + foreach (var (path, ex) in invalidFiles) + Log.Warning(ex, "\t=> {path}", path); + Log.Verbose(JsonConvert.SerializeObject(foundHashes)); + if (this.DefaultFontSpec is not SingleFontSpec { FontId: GameFontAndFamilyId }) + return; + + this.Framework.Update += FrameworkOnUpdate; + + void FrameworkOnUpdate(IFramework framework) + { + var charaSelect = ggui.GetAddonByName("CharaSelect", 1); + var charaMake = ggui.GetAddonByName("CharaMake", 1); + var titleDcWorldMap = ggui.GetAddonByName("TitleDCWorldMap", 1); + + // Show notification when TSM is visible, so that user can check whether a font looks bad + if (cstate.IsLoggedIn + || charaMake != IntPtr.Zero + || charaSelect != IntPtr.Zero + || titleDcWorldMap != IntPtr.Zero) + return; + + this.Framework.Update -= FrameworkOnUpdate; + + var n = nm.AddNotification( + "Non-default game fonts detected. If things do not look right, you can use a different font. Running repairs from XIVLauncher is recommended.", + "Modded font warning", + NotificationType.Warning, + 10000); + n.UseMonospaceFont = true; + n.Actions.Add( + ( + "Use Noto Sans", + () => + { + dconf.DefaultFontSpec = + new SingleFontSpec + { + FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansJpMedium), + SizePx = 17, + }; + dconf.QueueSave(); + intm.RebuildFonts(); + })); + n.Actions.Add(("Dismiss", () => n.Dismissed = true)); + } + } + } + private IDalamudTextureWrap GetChannelTexture(string texPathFormat, int fileIndex, int channelIndex) { var texFile = ExtractResult(ExtractResult(this.texFiles[texPathFormat])[fileIndex]); From 1c059aae7c33003b28664cae10702d8cf96d1cde Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Fri, 16 Feb 2024 20:30:07 +0100 Subject: [PATCH 20/51] Update ClientStructs (#1648) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 4b13c01e2..b12028fbc 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 4b13c01e2f60143f24698a6280255fb1aba7ab63 +Subproject commit b12028fbca6c950db0cb3d10d3185d959067e901 From 4b601f15c75ff564476576f4d2f663f90967d8e7 Mon Sep 17 00:00:00 2001 From: marzent Date: Fri, 16 Feb 2024 22:19:10 +0100 Subject: [PATCH 21/51] Merge pull request #1660 * make extra sure progress dialog crash handler is in the foregroud --- Dalamud.Boot/veh.cpp | 2 ++ DalamudCrashHandler/DalamudCrashHandler.cpp | 1 + 2 files changed, 3 insertions(+) diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index 4eeddba88..ade295d02 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -188,6 +188,8 @@ LONG exception_handler(EXCEPTION_POINTERS* ex) 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; + AllowSetForegroundWindow(GetProcessId(g_crashhandler_process)); + HANDLE waitHandles[] = { g_crashhandler_process, g_crashhandler_event }; DWORD waitResult = WaitForMultipleObjects(2, waitHandles, FALSE, INFINITE); diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 258ec923d..09e14d722 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -773,6 +773,7 @@ int main() { { SetWindowPos(hwndProgressDialog, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); + SetForegroundWindow(hwndProgressDialog); } pOleWindow->Release(); From 24e6bf3dc8f95524ee8c740b1ea9352fb528759e Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Fri, 16 Feb 2024 13:22:15 -0800 Subject: [PATCH 22/51] Remove metric submission on crashes - Rename the progress dialog, add punctuation to messaging --- DalamudCrashHandler/DalamudCrashHandler.cpp | 57 ++------------------- 1 file changed, 4 insertions(+), 53 deletions(-) diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 09e14d722..03c5c29ee 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -758,9 +758,9 @@ int main() { 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"); - pProgressDialog->SetLine(1, L"The game has crashed", FALSE, NULL); - pProgressDialog->SetLine(2, L"Dalamud is collecting further information", FALSE, NULL); + 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; @@ -904,47 +904,6 @@ int main() { 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[]{ @@ -1033,15 +992,7 @@ int main() { return (*reinterpret_cast(dwRefData))(hwnd, uNotification, wParam, lParam); }; config.lpCallbackData = reinterpret_cast(&callback); - - if (pProgressDialog) - pProgressDialog->SetLine(3, L"Submitting Metrics", FALSE, NULL); - - if (submitThread.joinable()) { - submitThread.join(); - submitThread = {}; - } - + if (pProgressDialog) { pProgressDialog->StopProgressDialog(); pProgressDialog->Release(); From 2afc692eca9dd6f46b5aa26d0f21ecf68f3adeef Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Fri, 16 Feb 2024 15:03:22 -0800 Subject: [PATCH 23/51] Add save tspack button to crash dialog - Require the user manually choose a restart mode before the Restart button can be clicked. - Rename window to Dalamud Crash Handler --- DalamudCrashHandler/DalamudCrashHandler.cpp | 28 +++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 03c5c29ee..e12ecdc50 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -610,6 +610,7 @@ enum { IdRadioRestartWithoutDalamud, IdButtonRestart = 201, + IdButtonSaveTsPack = 202, IdButtonHelp = IDHELP, IdButtonExit = IDCANCEL, }; @@ -907,20 +908,21 @@ int main() { 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 Pack\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"" @@ -928,7 +930,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; @@ -937,10 +939,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 @@ -962,6 +963,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: @@ -983,6 +985,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; From 82d9cd016d93033486ee90f2242ff5e34e667a8b Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Fri, 16 Feb 2024 15:56:51 -0800 Subject: [PATCH 24/51] Prefer "Save Troubleshooting Info" over "... Pack" --- DalamudCrashHandler/DalamudCrashHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index e12ecdc50..74e770ec0 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -916,7 +916,7 @@ int main() { const TASKDIALOG_BUTTON buttons[]{ {IdButtonRestart, L"Restart\nRestart the game with the above-selected option."}, - {IdButtonSaveTsPack, L"Save Troubleshooting Pack\nSave a .tspack file containing information about this crash for analysis."}, + {IdButtonSaveTsPack, L"Save Troubleshooting Info\nSave a .tspack file containing information about this crash for analysis."}, {IdButtonExit, L"Exit\nExit the game."}, }; From 0bb69cbd5f8f4633faef1181d067286ac5fc5116 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Sat, 17 Feb 2024 01:25:43 -0800 Subject: [PATCH 25/51] feat: Add /xlprofiler command (#1662) Make it easier for end users to open the profiler to pull load time reports. --- Dalamud/Interface/Internal/DalamudCommands.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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(); + } } From 47da75df24636d6659fb0fdf770e03848b70a096 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 17 Feb 2024 21:38:43 +0900 Subject: [PATCH 26/51] Some DCH correctness --- Dalamud.Boot/veh.cpp | 20 +++- Dalamud.sln | 118 +------------------- DalamudCrashHandler/DalamudCrashHandler.cpp | 40 +++++-- 3 files changed, 47 insertions(+), 131 deletions(-) diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index ade295d02..059189202 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -163,7 +163,11 @@ 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", @@ -182,11 +186,17 @@ LONG exception_handler(EXCEPTION_POINTERS* ex) 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)); 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/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 74e770ec0..82aa76569 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -26,7 +26,6 @@ #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='*'\"") @@ -153,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; @@ -442,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", @@ -458,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; @@ -483,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; @@ -526,7 +544,7 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s 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)) @@ -586,7 +604,6 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s } 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); @@ -597,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); } } @@ -672,7 +690,9 @@ int main() { std::filesystem::path assetDir, logDir; std::optional> launcherArgs; auto fullDump = false; - CoInitializeEx(nullptr, COINIT_MULTITHREADED); + + // IFileSaveDialog only works on STA + CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); std::vector args; if (int argc = 0; const auto argv = CommandLineToArgvW(GetCommandLineW(), &argc)) { @@ -972,7 +992,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") { From f825e86e861a901dc46164970888ce878e045494 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 17 Feb 2024 22:44:33 +0900 Subject: [PATCH 27/51] Fix B4G4R4A4->B8G8R8A8 channel extraction On systems without support for B4G4R4A4 pixel format (when DirectX feature level 11_1 is missing), the conversion will take place; a copy-and-paste error made the read pointer advance twice as fast as it should have been. --- .../Internals/FontAtlasFactory.cs | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 021fc953f..6a3d3e1a3 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -290,6 +290,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, @@ -327,25 +346,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, @@ -378,7 +378,7 @@ internal sealed partial class FontAtlasFactory v |= v << 4; *wptr = (uint)((v << 24) | 0x00FFFFFF); wptr++; - rptr += 4; + rptr += 2; } } } From 6bb4033b3596df1cac8c0abfcdd8c923ec97ee48 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 17 Feb 2024 15:05:18 +0100 Subject: [PATCH 28/51] crashhandler: only keep the last 3 minidumps --- DalamudCrashHandler/DalamudCrashHandler.cpp | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 82aa76569..3a69198b8 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -746,6 +746,35 @@ int main() { 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"; From cdaa538e1a90e923c4520e1fd0815c650520099b Mon Sep 17 00:00:00 2001 From: srkizer Date: Sat, 17 Feb 2024 23:07:49 +0900 Subject: [PATCH 29/51] Revert "Warn if font files' hashes are unexpected (#1659)" This reverts commit 307f0fcbe834f16d879f1d58014ee6f0ec3a2771. --- .../Notifications/NotificationManager.cs | 68 +------- .../Internals/FontAtlasFactory.cs | 165 +----------------- 2 files changed, 17 insertions(+), 216 deletions(-) diff --git a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs b/Dalamud/Interface/Internal/Notifications/NotificationManager.cs index 34e07be8f..67ad3ee8f 100644 --- a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs +++ b/Dalamud/Interface/Internal/Notifications/NotificationManager.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Numerics; using Dalamud.Interface.Colors; -using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.Utility; using Dalamud.Utility; using ImGuiNET; @@ -69,22 +68,15 @@ internal class NotificationManager : IServiceType /// The title of the notification. /// The type of the notification. /// The time the notification should be displayed for. - /// The added notification. - public Notification AddNotification( - string content, - string? title = null, - NotificationType type = NotificationType.None, - uint msDelay = NotifyDefaultDismiss) + public void AddNotification(string content, string? title = null, NotificationType type = NotificationType.None, uint msDelay = NotifyDefaultDismiss) { - var n = new Notification + this.notifications.Add(new Notification { Content = content, Title = title, NotificationType = type, DurationMs = msDelay, - }; - this.notifications.Add(n); - return n; + }); } /// @@ -105,10 +97,6 @@ internal class NotificationManager : IServiceType continue; } - using var pushedFont = tn.UseMonospaceFont - ? Service.Get().MonoFontHandle?.Push() - : null; - var opacity = tn.GetFadePercent(); var iconColor = tn.Color; @@ -119,12 +107,8 @@ internal class NotificationManager : IServiceType ImGuiHelpers.ForceNextWindowMainViewport(); ImGui.SetNextWindowBgAlpha(opacity); ImGui.SetNextWindowPos(ImGuiHelpers.MainViewport.Pos + new Vector2(viewportSize.X - NotifyPaddingX, viewportSize.Y - NotifyPaddingY - height), ImGuiCond.Always, Vector2.One); - if (tn.Actions.Count == 0) - ImGui.Begin(windowName, NotifyToastFlags); - else - ImGui.Begin(windowName, NotifyToastFlags & ~ImGuiWindowFlags.NoInputs); + ImGui.Begin(windowName, NotifyToastFlags); - ImGui.PushID(tn.NotificationId); ImGui.PushTextWrapPos(viewportSize.X / 3.0f); var wasTitleRendered = false; @@ -178,22 +162,10 @@ internal class NotificationManager : IServiceType ImGui.TextUnformatted(tn.Content); } - foreach (var (caption, action) in tn.Actions) - { - if (ImGui.Button(caption)) - action.InvokeSafely(); - ImGui.SameLine(); - } - - // break ImGui.SameLine(); - ImGui.TextUnformatted(string.Empty); - ImGui.PopStyleColor(); ImGui.PopTextWrapPos(); - ImGui.PopID(); - height += ImGui.GetWindowHeight() + NotifyPaddingMessageY; ImGui.End(); @@ -205,8 +177,6 @@ internal class NotificationManager : IServiceType /// internal class Notification { - private static int notificationIdCounter; - /// /// Possible notification phases. /// @@ -233,40 +203,20 @@ internal class NotificationManager : IServiceType Expired, } - /// - /// Gets the notification ID. - /// - internal int NotificationId { get; } = notificationIdCounter++; - /// /// Gets the type of the notification. /// internal NotificationType NotificationType { get; init; } /// - /// Gets or sets a value indicating whether to force the use of monospace font. + /// Gets the title of the notification. /// - internal bool UseMonospaceFont { get; set; } + internal string? Title { get; init; } /// - /// Gets the action buttons to attach to this notification. + /// Gets the content of the notification. /// - internal List<(string Text, Action ClickCallback)> Actions { get; } = new(); - - /// - /// Gets or sets a value indicating whether this notification has been dismissed. - /// - internal bool Dismissed { get; set; } - - /// - /// Gets or sets the title of the notification. - /// - internal string? Title { get; set; } - - /// - /// Gets or sets the content of the notification. - /// - internal string? Content { get; set; } + internal string Content { get; init; } /// /// Gets the duration of the notification in milliseconds. @@ -333,7 +283,7 @@ internal class NotificationManager : IServiceType { var elapsed = (int)this.ElapsedTime.TotalMilliseconds; - if (elapsed > NotifyFadeInOutTime + this.DurationMs + NotifyFadeInOutTime || this.Dismissed) + if (elapsed > NotifyFadeInOutTime + this.DurationMs + NotifyFadeInOutTime) return Phase.Expired; else if (elapsed > NotifyFadeInOutTime + this.DurationMs) return Phase.FadeOut; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 6a3d3e1a3..3e0fd1394 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -1,7 +1,6 @@ using System.Buffers; using System.Collections.Generic; using System.Collections.Immutable; -using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -9,13 +8,9 @@ using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game; -using Dalamud.Game.ClientState; -using Dalamud.Game.Gui; using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; -using Dalamud.Interface.Internal.Notifications; -using Dalamud.Plugin.Services; using Dalamud.Storage.Assets; using Dalamud.Utility; @@ -23,11 +18,7 @@ using ImGuiNET; using ImGuiScene; -using Lumina.Data; using Lumina.Data.Files; -using Lumina.Misc; - -using Newtonsoft.Json; using SharpDX; using SharpDX.Direct3D11; @@ -42,43 +33,9 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals; internal sealed partial class FontAtlasFactory : IServiceType, GamePrebakedFontHandle.IGameFontTextureProvider, IDisposable { - private static readonly Dictionary KnownFontFileDataHashes = new() - { - ["common/font/AXIS_96.fdt"] = 1486212503, - ["common/font/AXIS_12.fdt"] = 1370045105, - ["common/font/AXIS_14.fdt"] = 645957730, - ["common/font/AXIS_18.fdt"] = 899094094, - ["common/font/AXIS_36.fdt"] = 2537048938, - ["common/font/Jupiter_16.fdt"] = 1642196098, - ["common/font/Jupiter_20.fdt"] = 3053628263, - ["common/font/Jupiter_23.fdt"] = 1536194944, - ["common/font/Jupiter_45.fdt"] = 3473589216, - ["common/font/Jupiter_46.fdt"] = 1370962087, - ["common/font/Jupiter_90.fdt"] = 3661420529, - ["common/font/Meidinger_16.fdt"] = 3700692128, - ["common/font/Meidinger_20.fdt"] = 441419856, - ["common/font/Meidinger_40.fdt"] = 203848091, - ["common/font/MiedingerMid_10.fdt"] = 499375313, - ["common/font/MiedingerMid_12.fdt"] = 1925552591, - ["common/font/MiedingerMid_14.fdt"] = 1919733827, - ["common/font/MiedingerMid_18.fdt"] = 1635778987, - ["common/font/MiedingerMid_36.fdt"] = 1190559864, - ["common/font/TrumpGothic_184.fdt"] = 973994576, - ["common/font/TrumpGothic_23.fdt"] = 1967289381, - ["common/font/TrumpGothic_34.fdt"] = 1777971886, - ["common/font/TrumpGothic_68.fdt"] = 1170173741, - ["common/font/font0.tex"] = 514269927, - ["common/font/font1.tex"] = 3616607606, - ["common/font/font2.tex"] = 4166651000, - ["common/font/font3.tex"] = 1264942640, - ["common/font/font4.tex"] = 3534300885, - ["common/font/font5.tex"] = 1041916216, - ["common/font/font6.tex"] = 1247097672, - }; - private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); private readonly CancellationTokenSource cancellationTokenSource = new(); - private readonly IReadOnlyDictionary> fdtFiles; + private readonly IReadOnlyDictionary> fdtFiles; private readonly IReadOnlyDictionary[]>> texFiles; private readonly IReadOnlyDictionary> prebakedTextureWraps; private readonly Task defaultGlyphRanges; @@ -110,7 +67,7 @@ internal sealed partial class FontAtlasFactory this.fdtFiles = gffasInfo.ToImmutableDictionary( x => x.Font, - x => Task.Run(() => dataManager.GetFile(x.Attr.Path)!)); + x => Task.Run(() => dataManager.GetFile(x.Attr.Path)!.Data)); var channelCountsTask = texPaths.ToImmutableDictionary( x => x, x => Task.WhenAll( @@ -122,8 +79,8 @@ internal sealed partial class FontAtlasFactory { unsafe { - using var pin = file.Data.AsMemory().Pin(); - var fdt = new FdtFileView(pin.Pointer, file.Data.Length); + using var pin = file.AsMemory().Pin(); + var fdt = new FdtFileView(pin.Pointer, file.Length); return fdt.MaxTextureIndex; } }))); @@ -144,13 +101,11 @@ internal sealed partial class FontAtlasFactory { unsafe { - using var pin = file.Result.Data.AsMemory().Pin(); - var fdt = new FdtFileView(pin.Pointer, file.Result.Data.Length); + using var pin = file.Result.AsMemory().Pin(); + var fdt = new FdtFileView(pin.Pointer, file.Result.Length); return fdt.ToGlyphRanges(); } }); - - Task.Run(this.CheckSanity); } /// @@ -248,12 +203,12 @@ internal sealed partial class FontAtlasFactory /// /// The font family and size. /// The . - public FdtReader GetFdtReader(GameFontFamilyAndSize gffas) => new(ExtractResult(this.fdtFiles[gffas]).Data); + public FdtReader GetFdtReader(GameFontFamilyAndSize gffas) => new(ExtractResult(this.fdtFiles[gffas])); /// public unsafe MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView) { - var arr = ExtractResult(this.fdtFiles[gffas]).Data; + var arr = ExtractResult(this.fdtFiles[gffas]); var handle = arr.AsMemory().Pin(); try { @@ -385,110 +340,6 @@ internal sealed partial class FontAtlasFactory } } - private async Task CheckSanity() - { - var invalidFiles = new Dictionary(); - var texFileTasks = new Dictionary>(); - var foundHashes = new Dictionary(); - foreach (var (gffas, fdtTask) in this.fdtFiles) - { - var fontAttr = gffas.GetAttribute()!; - try - { - foundHashes[fontAttr.Path] = Crc32.Get((await fdtTask).Data); - - foreach (var (task, index) in - (await this.texFiles[fontAttr.TexPathFormat]).Select((x, i) => (x, i))) - texFileTasks[fontAttr.TexPathFormat.Format(index)] = task; - } - catch (Exception e) - { - invalidFiles[fontAttr.Path] = e; - } - } - - foreach (var (path, texTask) in texFileTasks) - { - try - { - var hc = default(HashCode); - hc.AddBytes((await texTask).Data); - foundHashes[path] = Crc32.Get((await texTask).Data); - } - catch (Exception e) - { - invalidFiles[path] = e; - } - } - - foreach (var (path, hashCode) in foundHashes) - { - if (!KnownFontFileDataHashes.TryGetValue(path, out var expectedHashCode)) - continue; - if (expectedHashCode != hashCode) - { - invalidFiles[path] = new InvalidDataException( - $"Expected 0x{expectedHashCode:X08}; got 0x{hashCode:X08}"); - } - } - - var dconf = await Service.GetAsync(); - var nm = await Service.GetAsync(); - var intm = (await Service.GetAsync()).Manager; - var ggui = await Service.GetAsync(); - var cstate = await Service.GetAsync(); - - if (invalidFiles.Any()) - { - Log.Warning("Found {n} font related file(s) with unexpected hash code values.", invalidFiles.Count); - foreach (var (path, ex) in invalidFiles) - Log.Warning(ex, "\t=> {path}", path); - Log.Verbose(JsonConvert.SerializeObject(foundHashes)); - if (this.DefaultFontSpec is not SingleFontSpec { FontId: GameFontAndFamilyId }) - return; - - this.Framework.Update += FrameworkOnUpdate; - - void FrameworkOnUpdate(IFramework framework) - { - var charaSelect = ggui.GetAddonByName("CharaSelect", 1); - var charaMake = ggui.GetAddonByName("CharaMake", 1); - var titleDcWorldMap = ggui.GetAddonByName("TitleDCWorldMap", 1); - - // Show notification when TSM is visible, so that user can check whether a font looks bad - if (cstate.IsLoggedIn - || charaMake != IntPtr.Zero - || charaSelect != IntPtr.Zero - || titleDcWorldMap != IntPtr.Zero) - return; - - this.Framework.Update -= FrameworkOnUpdate; - - var n = nm.AddNotification( - "Non-default game fonts detected. If things do not look right, you can use a different font. Running repairs from XIVLauncher is recommended.", - "Modded font warning", - NotificationType.Warning, - 10000); - n.UseMonospaceFont = true; - n.Actions.Add( - ( - "Use Noto Sans", - () => - { - dconf.DefaultFontSpec = - new SingleFontSpec - { - FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansJpMedium), - SizePx = 17, - }; - dconf.QueueSave(); - intm.RebuildFonts(); - })); - n.Actions.Add(("Dismiss", () => n.Dismissed = true)); - } - } - } - private IDalamudTextureWrap GetChannelTexture(string texPathFormat, int fileIndex, int channelIndex) { var texFile = ExtractResult(ExtractResult(this.texFiles[texPathFormat])[fileIndex]); From e21b64969f4ae3ac164790dbd3827c5283318a2e Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sat, 17 Feb 2024 15:22:27 +0100 Subject: [PATCH 30/51] build: 9.0.0.19 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 208e6d4ea..aadf736a2 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.18 + 9.0.0.19 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From c19e1f0fcd224ec4b3cef14a48d921e34eff8375 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Sat, 17 Feb 2024 10:06:41 -0800 Subject: [PATCH 31/51] Default Minimum/Maximum WindowSizeConstraints (#1574) * feat: Default Minimum/Maximum WindowSizeConstraints If `MinimumSize` or `MaximumSize` are not set when defining a `WindowSizeConstraints`, they will be effectively unbounded. * chore: Make internal windows unbounded on max size * Ignore max value if it's smaller than minimum in any dimension --- .../Internal/Windows/ConsoleWindow.cs | 1 - .../PluginInstaller/PluginInstallerWindow.cs | 1 - .../Windows/StyleEditor/StyleEditorWindow.cs | 1 - Dalamud/Interface/Windowing/Window.cs | 29 +++++++++++++++++-- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 1b9890a75..63924365d 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -71,7 +71,6 @@ 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; 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/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/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; + } } /// From 7da47a8a334a7cdd9553b2d8074c015c80008710 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sat, 17 Feb 2024 19:07:18 +0100 Subject: [PATCH 32/51] build: 9.0.0.20 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index aadf736a2..321ee30a0 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.19 + 9.0.0.20 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 7dc99c9307dc950f5442d0d06ac8cbaa6594aaf4 Mon Sep 17 00:00:00 2001 From: srkizer Date: Sun, 18 Feb 2024 16:03:51 +0900 Subject: [PATCH 33/51] Fix AddRectFilledDetour typo (#1667) * Fix AddRectFilledDetour typo * Skip drawing if zero opacity is specified for drawing --- .../Interface/Internal/ImGuiDrawListFixProvider.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs b/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs index cdf7ab23e..f2d6ed244 100644 --- a/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs @@ -83,8 +83,14 @@ internal sealed unsafe class ImGuiDrawListFixProvider : IServiceType, IDisposabl float rounding, ImDrawFlags flags) { - if (rounding < 0 || (flags & ImDrawFlags.RoundCornersMask) == ImDrawFlags.RoundCornersMask) + // 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) @@ -98,6 +104,9 @@ internal sealed unsafe class ImGuiDrawListFixProvider : IServiceType, IDisposabl } 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); } From 2d8b71c647e1b9ccea9f8746146fd8eaafee2791 Mon Sep 17 00:00:00 2001 From: srkizer Date: Sun, 18 Feb 2024 23:08:07 +0900 Subject: [PATCH 34/51] Add SetFontScaleMode(ImFontPtr, FontScaleMode) (#1666) * Add SetFontScaleMode(ImFontPtr, FontScaleMode) `IgnoreGlobalScale` was advertised as "excludes the given font from global scaling", but the intent I had in mind was "excludes the given font from being scaled in any manner". As the latter functionality is needed, obsoleted `IgnoreGlobalScale` and added `SetFontScaleMode`. * Make it correct * Name consistency --- Dalamud/Interface/FontIdentifier/IFontSpec.cs | 2 + .../FontIdentifier/SingleFontSpec.cs | 13 ++-- .../SingleFontChooserDialog.cs | 6 +- .../Widgets/GamePrebakedFontsTestWidget.cs | 68 +++++++++++++------ .../ManagedFontAtlas/FontScaleMode.cs | 33 +++++++++ .../IFontAtlasBuildToolkitPostBuild.cs | 14 ++-- .../IFontAtlasBuildToolkitPreBuild.cs | 28 +++++++- .../FontAtlasFactory.BuildToolkit.cs | 22 +++--- .../Internals/GamePrebakedFontHandle.cs | 46 +++++++++---- 9 files changed, 166 insertions(+), 66 deletions(-) create mode 100644 Dalamud/Interface/ManagedFontAtlas/FontScaleMode.cs diff --git a/Dalamud/Interface/FontIdentifier/IFontSpec.cs b/Dalamud/Interface/FontIdentifier/IFontSpec.cs index e4d931605..4d0719d4e 100644 --- a/Dalamud/Interface/FontIdentifier/IFontSpec.cs +++ b/Dalamud/Interface/FontIdentifier/IFontSpec.cs @@ -31,6 +31,8 @@ public interface IFontSpec /// 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); /// diff --git a/Dalamud/Interface/FontIdentifier/SingleFontSpec.cs b/Dalamud/Interface/FontIdentifier/SingleFontSpec.cs index 0604b22ea..946215b85 100644 --- a/Dalamud/Interface/FontIdentifier/SingleFontSpec.cs +++ b/Dalamud/Interface/FontIdentifier/SingleFontSpec.cs @@ -109,7 +109,9 @@ public record SingleFontSpec : IFontSpec tk.RegisterPostBuild( () => { - var roundUnit = tk.IsGlobalScaleIgnored(font) ? 1 : 1 / tk.Scale; + // 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; @@ -129,13 +131,10 @@ public record SingleFontSpec : IFontSpec } } - // `/ roundUnit` = `* scale` - var dax = MathF.Round(this.LetterSpacing / roundUnit / roundUnit) * roundUnit; - var dxy0 = this.GlyphOffset / roundUnit; - + var dax = MathF.Round((this.LetterSpacing * scale) / roundUnit) * roundUnit; + var dxy0 = this.GlyphOffset * scale; dxy0 /= roundUnit; - dxy0.X = MathF.Round(dxy0.X); - dxy0.Y = MathF.Round(dxy0.Y); + dxy0 = new(MathF.Round(dxy0.X), MathF.Round(dxy0.Y)); dxy0 *= roundUnit; dxy0.Y += shiftDown; diff --git a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs index 410bf7d18..ca75e5ce0 100644 --- a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs +++ b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs @@ -342,9 +342,7 @@ public sealed class SingleFontChooserDialog : IDisposable { this.fontHandle ??= this.selectedFont.CreateFontHandle( this.atlas, - tk => - tk.OnPreBuild(e => e.IgnoreGlobalScale(e.Font)) - .OnPostBuild(e => e.Font.AdjustGlyphMetrics(1f / e.Scale))); + tk => tk.OnPreBuild(e => e.SetFontScaleMode(e.Font, FontScaleMode.UndoGlobalScale))); } else { @@ -837,7 +835,7 @@ public sealed class SingleFontChooserDialog : IDisposable var changed = false; if (!ImGui.BeginTable("##advancedOptions", 4)) - return changed; + return false; var labelWidth = ImGui.CalcTextSize("Letter Spacing:").X; labelWidth = Math.Max(labelWidth, ImGui.CalcTextSize("Offset:").X); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs index 84682e7c2..8bb999557 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -25,12 +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; @@ -52,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(); } } @@ -124,7 +134,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable $"{nameof(GamePrebakedFontsTestWidget)}:EditorFont", FontAtlasAutoRebuildMode.Async)); fcd.SelectedFont = this.fontSpec; - fcd.IgnorePreviewGlobalScale = !this.useGlobalScale; + fcd.IgnorePreviewGlobalScale = !this.atlasScaleMode; Service.Get().Draw += fcd.Draw; fcd.ResultTask.ContinueWith( r => Service.Get().RunOnFrameworkThread( @@ -148,12 +158,14 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable Service.Get().CreateFontAtlas( nameof(GamePrebakedFontsTestWidget), FontAtlasAutoRebuildMode.Async, - this.useGlobalScale); - this.fontDialogHandle ??= this.fontSpec.CreateFontHandle(this.privateAtlas); + 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.useGlobalScale) + if (!this.atlasScaleMode) ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale); using (this.fontDialogHandle.Push()) { @@ -180,7 +192,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable } } - if (!this.useGlobalScale) + if (!this.atlasScaleMode) ImGuiNative.igSetWindowFontScale(1); } @@ -192,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); @@ -230,7 +254,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable } else { - if (!this.useGlobalScale) + if (!this.atlasScaleMode) ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale); if (counter++ % 2 == 0) { @@ -251,8 +275,8 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable } finally { - ImGuiNative.igPopTextWrapPos(); ImGuiNative.igSetWindowFontScale(1); + ImGuiNative.igPopTextWrapPos(); } } } 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/IFontAtlasBuildToolkitPostBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs index d824eca52..827187063 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs @@ -1,4 +1,5 @@ using Dalamud.Interface.Internal; +using Dalamud.Utility; using ImGuiNET; @@ -10,12 +11,13 @@ namespace Dalamud.Interface.ManagedFontAtlas; /// 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 9ab480374..9b80d27ff 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs @@ -4,6 +4,7 @@ using System.Runtime.InteropServices; using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Utility; +using Dalamud.Utility; using ImGuiNET; @@ -45,14 +46,37 @@ 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. diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index a57e6d036..55af20329 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -83,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(); @@ -151,15 +151,15 @@ 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) => @@ -496,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; @@ -536,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) 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); } From 2909c83521f19781875ca4f4cd78a19fa2bb7d12 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sun, 18 Feb 2024 17:16:35 +0100 Subject: [PATCH 35/51] build: 9.0.0.21 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 321ee30a0..205681cb8 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.20 + 9.0.0.21 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From ac59f73b59ba497d9f83e1443839f350976644ec Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sun, 18 Feb 2024 19:15:55 +0100 Subject: [PATCH 36/51] [master] Update ClientStructs (#1669) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index b12028fbc..d5673fcb4 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit b12028fbca6c950db0cb3d10d3185d959067e901 +Subproject commit d5673fcb479b414d0c760213cda5f2a98d297cbf From c27422384f66868991814a0fd905c3bada90c271 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 20 Feb 2024 15:37:54 +0900 Subject: [PATCH 37/51] IGameConfig: fix load-time race condition As some public properties of `IGameConfig` are being set on the first `Framework` tick, there was a short window that those properties were null, which goes against the interface declaration. This commit fixes that, by making those properties block for the full initialization of the class. A possible side effect is that a plugin that is set to block the game from loading until it loads will now hang the game if it tries to access the game configuration from its constructor, instead of throwing a `NullReferenceException`. As it would mean that the plugin was buggy at the first place and it would have sometimes failed to load anyway, it might as well be a non-breaking change. --- Dalamud/Game/Config/GameConfig.cs | 87 ++++++++++++++++++++------ Dalamud/Plugin/Services/IGameConfig.cs | 22 ++++++- Dalamud/Utility/Util.cs | 40 ++++++++++++ 3 files changed, 126 insertions(+), 23 deletions(-) diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index b82d64f24..94a92a4da 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); + } }); } @@ -59,13 +82,16 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable #pragma warning restore 67 /// - public GameConfigSection System { get; private set; } + 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 +195,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(); } @@ -226,9 +257,16 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig 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(); } /// @@ -243,6 +281,9 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig /// public event EventHandler? UiControlChanged; + /// + public Task InitializationTask { get; } + /// public GameConfigSection System => this.gameConfigService.System; @@ -256,9 +297,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/Plugin/Services/IGameConfig.cs b/Dalamud/Plugin/Services/IGameConfig.cs index 8e9b48d83..8249aed76 100644 --- a/Dalamud/Plugin/Services/IGameConfig.cs +++ b/Dalamud/Plugin/Services/IGameConfig.cs @@ -1,14 +1,20 @@ -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. /// +/// +/// 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 { /// @@ -31,6 +37,16 @@ public interface IGameConfig /// public event EventHandler UiControlChanged; + /// + /// Gets a task representing the initialization state of this instance of . + /// + /// + /// Accessing -typed properties such as , directly or indirectly + /// via , + /// , or alike will block, if this task is incomplete. + /// + public Task InitializationTask { get; } + /// /// Gets the collection of config options that persist between characters. /// diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index f5ad8b999..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; @@ -697,6 +698,45 @@ public static class Util 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. /// From da969dec5cb4f7682acc1965dc8ddf77be8c129f Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 20 Feb 2024 15:42:49 +0900 Subject: [PATCH 38/51] DAssetM cleanup --- Dalamud/Storage/Assets/DalamudAssetManager.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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) From 3909fb13fad3e030a511a598f79b2d0677726df5 Mon Sep 17 00:00:00 2001 From: AzureGem Date: Tue, 20 Feb 2024 14:19:24 -0500 Subject: [PATCH 39/51] Fix ConsoleWindow regex handling (#1674) --- Dalamud/Interface/Internal/Windows/ConsoleWindow.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 63924365d..bf559c4d7 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -731,8 +731,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(); } @@ -741,6 +739,7 @@ internal class ConsoleWindow : Window, IDisposable { lock (this.renderLock) { + this.regexError = false; this.FilteredLogEntries = this.logText.Where(this.IsFilterApplicable).ToList(); } } From a3217bb86d7b270ef4ac802dbae0c5953ef70f59 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 21 Feb 2024 16:34:53 +0900 Subject: [PATCH 40/51] Remove InitialiationTask from interface --- Dalamud/Game/Config/GameConfig.cs | 13 +++++++------ Dalamud/Plugin/Services/IGameConfig.cs | 15 ++++----------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index 94a92a4da..162df9417 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -81,7 +81,9 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable public event EventHandler? UiControlChanged; #pragma warning restore 67 - /// + /// + /// Gets a task representing the initialization state of this class. + /// public Task InitializationTask => this.tcsInitialization.Task; /// @@ -251,13 +253,15 @@ 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.InitializationTask = this.gameConfigService.InitializationTask.ContinueWith( + this.initializationTask = this.gameConfigService.InitializationTask.ContinueWith( r => { if (!r.IsCompletedSuccessfully) @@ -281,9 +285,6 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig /// public event EventHandler? UiControlChanged; - /// - public Task InitializationTask { get; } - /// public GameConfigSection System => this.gameConfigService.System; @@ -297,7 +298,7 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig public void Dispose() { this.gameConfigService.Changed -= this.ConfigChangedForward; - this.InitializationTask.ContinueWith( + this.initializationTask.ContinueWith( r => { if (!r.IsCompletedSuccessfully) diff --git a/Dalamud/Plugin/Services/IGameConfig.cs b/Dalamud/Plugin/Services/IGameConfig.cs index 8249aed76..c69fa906a 100644 --- a/Dalamud/Plugin/Services/IGameConfig.cs +++ b/Dalamud/Plugin/Services/IGameConfig.cs @@ -10,7 +10,10 @@ namespace Dalamud.Plugin.Services; /// This class represents the game's configuration. /// /// -/// Avoid accessing configuration from your plugin constructor, especially if your plugin sets +/// 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. @@ -37,16 +40,6 @@ public interface IGameConfig ///
public event EventHandler UiControlChanged; - /// - /// Gets a task representing the initialization state of this instance of . - /// - /// - /// Accessing -typed properties such as , directly or indirectly - /// via , - /// , or alike will block, if this task is incomplete. - /// - public Task InitializationTask { get; } - /// /// Gets the collection of config options that persist between characters. /// From bf34dd2817e5c5b1c472ed2ee2f68b6cd5ac3eac Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Thu, 22 Feb 2024 05:04:16 +0100 Subject: [PATCH 41/51] Update ClientStructs (#1670) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index d5673fcb4..efea20c57 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit d5673fcb479b414d0c760213cda5f2a98d297cbf +Subproject commit efea20c57e4fed3e2e70ef25dacebae2558e1bbf From db17a8658702d81bc40d5b5b96450d271926ab0a Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Thu, 22 Feb 2024 08:00:19 +0100 Subject: [PATCH 42/51] Update ClientStructs (#1677) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index efea20c57..7f1f3bcaa 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit efea20c57e4fed3e2e70ef25dacebae2558e1bbf +Subproject commit 7f1f3bcaae6090d7779b34f62ae8b4b570bb5165 From 94cf1c82c49c44c5ac05b65a48929f96b27f127a Mon Sep 17 00:00:00 2001 From: srkizer Date: Fri, 23 Feb 2024 13:27:07 +0900 Subject: [PATCH 43/51] Synchronize DalamudStartInfo between cpp and cs (#1679) Dalamud Boot was using BootLogPath in place of LogPath, resulting in wrong log path. --- Dalamud.Boot/DalamudStartInfo.cpp | 7 +++++-- Dalamud.Boot/DalamudStartInfo.h | 7 +++++-- Dalamud.Boot/veh.cpp | 5 ++++- Dalamud.Common/DalamudStartInfo.cs | 1 + Dalamud.Injector/EntryPoint.cs | 1 + DalamudCrashHandler/DalamudCrashHandler.cpp | 1 - 6 files changed, 16 insertions(+), 6 deletions(-) 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/veh.cpp b/Dalamud.Boot/veh.cpp index 059189202..58234783a 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -112,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=\"" + unicode::convert(g_startInfo.BootLogPath) + L"\""); 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) 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.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index c784ec1d1..2d776b043 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -389,6 +389,7 @@ namespace Dalamud.Injector #else startInfo.LogPath ??= xivlauncherDir; #endif + startInfo.LogName ??= string.Empty; // Set boot defaults startInfo.BootShowConsole = args.Contains("--console"); diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 3a69198b8..d4e9f0a1c 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -896,7 +896,6 @@ int main() { 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; From c1c85e52366d29d23a30fb63e0fe3663981a6035 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sun, 25 Feb 2024 12:23:01 +0100 Subject: [PATCH 44/51] [master] Update ClientStructs (#1680) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 7f1f3bcaa..4cafdfead 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 7f1f3bcaae6090d7779b34f62ae8b4b570bb5165 +Subproject commit 4cafdfead3e22bfe4ad811dfb32401f2faea428b From f6be80a5fb309a0aaf64feac786d7f11944dabeb Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 25 Feb 2024 21:21:50 +0900 Subject: [PATCH 45/51] Make IDalamudTextureWrap ICloneable --- .../Interface/Internal/DalamudTextureWrap.cs | 33 +------- .../Interface/Internal/IDalamudTextureWrap.cs | 47 +++++++++++ .../Interface/Internal/InterfaceManager.cs | 4 +- .../Interface/Internal/UnknownTextureWrap.cs | 77 +++++++++++++++++++ .../Windows/Data/Widgets/TexWidget.cs | 4 + Dalamud/Utility/IDeferredDisposable.cs | 12 +++ 6 files changed, 145 insertions(+), 32 deletions(-) create mode 100644 Dalamud/Interface/Internal/IDalamudTextureWrap.cs create mode 100644 Dalamud/Interface/Internal/UnknownTextureWrap.cs create mode 100644 Dalamud/Utility/IDeferredDisposable.cs 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..60d96534d --- /dev/null +++ b/Dalamud/Interface/Internal/IDalamudTextureWrap.cs @@ -0,0 +1,47 @@ +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, ICloneable +{ + /// + /// 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 this texture wrap. + /// + /// The new reference to this texture wrap. + /// The default implementation will treat as an . + new unsafe IDalamudTextureWrap Clone() + { + // Dalamud specific: IDalamudTextureWrap always points to an ID3D11ShaderResourceView. + var handle = (IUnknown*)this.ImGuiHandle; + return new UnknownTextureWrap(handle, this.Width, this.Height, true); + } + + /// + object ICloneable.Clone() => this.Clone(); +} diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 6d93b4bd7..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); } 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/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 0cbc401e7..173e5409a 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.Clone()); } } 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(); +} From 9629a555be670fe87db8c2d51bceb285fe44776d Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 03:20:28 +0900 Subject: [PATCH 46/51] Rename to CreateWrapSharingLowLevelResource --- .../Interface/Internal/IDalamudTextureWrap.cs | 22 +++++++++++++------ .../Windows/Data/Widgets/TexWidget.cs | 2 +- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/Dalamud/Interface/Internal/IDalamudTextureWrap.cs b/Dalamud/Interface/Internal/IDalamudTextureWrap.cs index 60d96534d..8e2e56c26 100644 --- a/Dalamud/Interface/Internal/IDalamudTextureWrap.cs +++ b/Dalamud/Interface/Internal/IDalamudTextureWrap.cs @@ -8,7 +8,7 @@ namespace Dalamud.Interface.Internal; /// Base TextureWrap interface for all Dalamud-owned texture wraps. /// Used to avoid referencing ImGuiScene. ///
-public interface IDalamudTextureWrap : IDisposable, ICloneable +public interface IDalamudTextureWrap : IDisposable { /// /// Gets a texture handle suitable for direct use with ImGui functions. @@ -31,17 +31,25 @@ public interface IDalamudTextureWrap : IDisposable, ICloneable Vector2 Size => new(this.Width, this.Height); /// - /// Creates a new reference to this texture wrap. + /// Creates a new reference to the resource being pointed by this instance of . /// /// The new reference to this texture wrap. - /// The default implementation will treat as an . - new unsafe IDalamudTextureWrap Clone() + /// + /// 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); } - - /// - object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 173e5409a..8d6879ac1 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -122,7 +122,7 @@ internal class TexWidget : IDataWindowWidget ImGui.SameLine(); if (ImGui.Button($"Clone##{i}")) - this.addedTextures.Add(tex.Clone()); + this.addedTextures.Add(tex.CreateWrapSharingLowLevelResource()); } } From 12e2fd3f60f733f5d20775a0b4dc1e8a91727273 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 27 Feb 2024 19:48:32 +0900 Subject: [PATCH 47/51] Miscellaneous fixes --- Dalamud/Interface/UiBuilder.cs | 6 +- Dalamud/Utility/DisposeSafety.cs | 159 ++++++++++++++++++++----------- 2 files changed, 109 insertions(+), 56 deletions(-) diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 7a3eb6fb6..d260868a0 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -212,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 => @@ -231,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 => @@ -251,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/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); } } } From e6c97f0f181e518278264e2ba92f5319a446c70e Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 27 Feb 2024 18:03:09 +0100 Subject: [PATCH 48/51] Update ClientStructs (#1685) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 4cafdfead..722a2c512 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 4cafdfead3e22bfe4ad811dfb32401f2faea428b +Subproject commit 722a2c512238ac4b5324e3d343b316d8c8633a02 From 0651c643b151389d0d313f1188cee256ab81af62 Mon Sep 17 00:00:00 2001 From: AzureGem Date: Tue, 27 Feb 2024 13:15:11 -0500 Subject: [PATCH 49/51] Limit console log lines held in memory (#1683) * Add AG.Collections.RollingList * Use RollingList for logs + Adaption changes * Create Dalamud.Utility.ThrowHelper * Create Dalamud.Utility.RollingList * ConsoleWindow: Remove dependency * Remove NuGet Dependency * Add Log Lines Limit configuration * Use Log Lines Limit configuration and handle changes * Make log lines limit configurable --- .../Internal/DalamudConfiguration.cs | 5 + .../Internal/Windows/ConsoleWindow.cs | 99 ++++++-- Dalamud/Utility/RollingList.cs | 234 ++++++++++++++++++ Dalamud/Utility/ThrowHelper.cs | 52 ++++ 4 files changed, 375 insertions(+), 15 deletions(-) create mode 100644 Dalamud/Utility/RollingList.cs create mode 100644 Dalamud/Utility/ThrowHelper.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 957be12b9..85a9507c9 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -215,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/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index bf559c4d7..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; @@ -74,9 +81,17 @@ internal class ConsoleWindow : Window, IDisposable }; 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() @@ -91,6 +106,7 @@ internal class ConsoleWindow : Window, IDisposable public void Dispose() { SerilogEventSink.Instance.LogLine -= this.OnLogLine; + Service.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved; } /// @@ -180,6 +196,9 @@ internal class ConsoleWindow : Window, IDisposable 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) { clipper.Begin(this.FilteredLogEntries.Count); @@ -187,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(); @@ -228,6 +248,10 @@ internal class ConsoleWindow : Window, IDisposable ImGui.SetCursorPosX(cursorLogLine); ImGui.TextUnformatted(line.Line); + + var currentLinePosY = ImGui.GetCursorPosY(); + logLineHeight = currentLinePosY - lastLinePosY; + lastLinePosY = currentLinePosY; } } @@ -239,6 +263,12 @@ 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); @@ -363,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(); @@ -447,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; @@ -686,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) @@ -740,7 +801,7 @@ internal class ConsoleWindow : Window, IDisposable lock (this.renderLock) { this.regexError = false; - this.FilteredLogEntries = this.logText.Where(this.IsFilterApplicable).ToList(); + this.FilteredLogEntries = new RollingList(this.logText.Where(this.IsFilterApplicable), Math.Max(LogLinesMinimum, this.logLinesLimit)); } } @@ -789,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/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 + } + } +} From 3d59fa3da0a2d5292d0517c5d5ba59bdc26a3638 Mon Sep 17 00:00:00 2001 From: srkizer Date: Fri, 1 Mar 2024 08:13:33 +0900 Subject: [PATCH 50/51] Sanitize PDB root name from loaded modules (#1687) --- Dalamud.Boot/Dalamud.Boot.vcxproj | 4 +- Dalamud.Boot/Dalamud.Boot.vcxproj.filters | 6 ++ Dalamud.Boot/hooks.cpp | 32 +------ Dalamud.Boot/hooks.h | 1 - Dalamud.Boot/ntdll.cpp | 15 +++ Dalamud.Boot/ntdll.h | 33 +++++++ Dalamud.Boot/pch.h | 7 ++ Dalamud.Boot/xivfixes.cpp | 109 +++++++++++++++++++++- Dalamud.Boot/xivfixes.h | 1 + Dalamud.Injector/EntryPoint.cs | 12 ++- 10 files changed, 181 insertions(+), 39 deletions(-) create mode 100644 Dalamud.Boot/ntdll.cpp create mode 100644 Dalamud.Boot/ntdll.h diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj b/Dalamud.Boot/Dalamud.Boot.vcxproj index ab68c1ec0..298edbcbc 100644 --- a/Dalamud.Boot/Dalamud.Boot.vcxproj +++ b/Dalamud.Boot/Dalamud.Boot.vcxproj @@ -58,7 +58,7 @@ Windows true false - Version.lib;%(AdditionalDependencies) + Version.lib;Shlwapi.lib;%(AdditionalDependencies) ..\lib\CoreCLR;%(AdditionalLibraryDirectories) @@ -137,6 +137,7 @@ NotUsing NotUsing + NotUsing NotUsing @@ -176,6 +177,7 @@ + diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters index a1b1650e2..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 + 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/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 a09882c74..c2194c157 100644 --- a/Dalamud.Boot/pch.h +++ b/Dalamud.Boot/pch.h @@ -15,14 +15,20 @@ #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 @@ -30,6 +36,7 @@ #include // C++ Standard Libraries +#include #include #include #include diff --git a/Dalamud.Boot/xivfixes.cpp b/Dalamud.Boot/xivfixes.cpp index 39cce53c9..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) { @@ -546,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> { @@ -554,7 +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 }, - { "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 f534ad7dd..afe2edb45 100644 --- a/Dalamud.Boot/xivfixes.h +++ b/Dalamud.Boot/xivfixes.h @@ -7,6 +7,7 @@ namespace xivfixes { void redirect_openprocess(bool bApply); void backup_userdata_save(bool bApply); void prevent_icmphandle_crashes(bool bApply); + void symbol_load_patches(bool bApply); void apply_all(bool bApply); } diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index 2d776b043..9085eae04 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -395,9 +395,15 @@ namespace Dalamud.Injector 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; From 5f62c703bff4137a1f887553fc1e0bd932d6dc6e Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Thu, 29 Feb 2024 15:15:02 -0800 Subject: [PATCH 51/51] Add IContextMenu service (#1682) --- Dalamud/Game/Gui/ContextMenu/ContextMenu.cs | 560 ++++++++++++++++++ .../Game/Gui/ContextMenu/ContextMenuType.cs | 18 + Dalamud/Game/Gui/ContextMenu/MenuArgs.cs | 77 +++ Dalamud/Game/Gui/ContextMenu/MenuItem.cs | 91 +++ .../Gui/ContextMenu/MenuItemClickedArgs.cs | 44 ++ .../Game/Gui/ContextMenu/MenuOpenedArgs.cs | 34 ++ Dalamud/Game/Gui/ContextMenu/MenuTarget.cs | 9 + .../Game/Gui/ContextMenu/MenuTargetDefault.cs | 67 +++ .../Gui/ContextMenu/MenuTargetInventory.cs | 36 ++ Dalamud/Game/Inventory/GameInventoryItem.cs | 12 +- .../Structures/InfoProxy/CharacterData.cs | 197 ++++++ .../AgingSteps/ContextMenuAgingStep.cs | 333 ++++++----- Dalamud/Plugin/Services/IContextMenu.cs | 37 ++ Dalamud/Utility/EventHandlerExtensions.cs | 18 + 14 files changed, 1387 insertions(+), 146 deletions(-) create mode 100644 Dalamud/Game/Gui/ContextMenu/ContextMenu.cs create mode 100644 Dalamud/Game/Gui/ContextMenu/ContextMenuType.cs create mode 100644 Dalamud/Game/Gui/ContextMenu/MenuArgs.cs create mode 100644 Dalamud/Game/Gui/ContextMenu/MenuItem.cs create mode 100644 Dalamud/Game/Gui/ContextMenu/MenuItemClickedArgs.cs create mode 100644 Dalamud/Game/Gui/ContextMenu/MenuOpenedArgs.cs create mode 100644 Dalamud/Game/Gui/ContextMenu/MenuTarget.cs create mode 100644 Dalamud/Game/Gui/ContextMenu/MenuTargetDefault.cs create mode 100644 Dalamud/Game/Gui/ContextMenu/MenuTargetInventory.cs create mode 100644 Dalamud/Game/Network/Structures/InfoProxy/CharacterData.cs create mode 100644 Dalamud/Plugin/Services/IContextMenu.cs 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/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/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/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/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