diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ea1afcac5..be44afacc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,10 +1,9 @@ name: Build Dalamud on: [push, pull_request, workflow_dispatch] -# Globally blocking because of git pushes in deploy step concurrency: - group: build_dalamud_${{ github.repository_owner }} - cancel-in-progress: false + group: build_dalamud_${{ github.ref_name }} + cancel-in-progress: true jobs: build: diff --git a/Dalamud.Boot/crashhandler_shared.h b/Dalamud.Boot/crashhandler_shared.h index 0308306ce..8d93e4460 100644 --- a/Dalamud.Boot/crashhandler_shared.h +++ b/Dalamud.Boot/crashhandler_shared.h @@ -6,8 +6,6 @@ #define WIN32_LEAN_AND_MEAN #include -#define CUSTOM_EXCEPTION_EXTERNAL_EVENT 0x12345679 - struct exception_info { LPEXCEPTION_POINTERS pExceptionPointers; diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp index 687089f82..80a16f89a 100644 --- a/Dalamud.Boot/dllmain.cpp +++ b/Dalamud.Boot/dllmain.cpp @@ -331,51 +331,6 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { logging::I("VEH was disabled manually"); } - // ============================== CLR Reporting =================================== // - - // This is pretty horrible - CLR just doesn't provide a way for us to handle these events, and the API for it - // was pushed back to .NET 11, so we have to hook ReportEventW and catch them ourselves for now. - // Ideally all of this will go away once they get to it. - static std::shared_ptr> s_report_event_hook; - s_report_event_hook = std::make_shared>( - "advapi32.dll!ReportEventW (global import, hook_clr_report_event)", L"advapi32.dll", "ReportEventW"); - s_report_event_hook->set_detour([hook = s_report_event_hook.get()]( - HANDLE hEventLog, - WORD wType, - WORD wCategory, - DWORD dwEventID, - PSID lpUserSid, - WORD wNumStrings, - DWORD dwDataSize, - LPCWSTR* lpStrings, - LPVOID lpRawData)-> BOOL { - - // Check for CLR Error Event IDs - // https://github.com/dotnet/runtime/blob/v10.0.0/src/coreclr/vm/eventreporter.cpp#L370 - if (dwEventID != 1026 && // ERT_UnhandledException: The process was terminated due to an unhandled exception - dwEventID != 1025 && // ERT_ManagedFailFast: The application requested process termination through System.Environment.FailFast - dwEventID != 1023 && // ERT_UnmanagedFailFast: The process was terminated due to an internal error in the .NET Runtime - dwEventID != 1027 && // ERT_StackOverflow: The process was terminated due to a stack overflow - dwEventID != 1028) // ERT_CodeContractFailed: The application encountered a bug. A managed code contract (precondition, postcondition, object invariant, or assert) failed - { - return hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData); - } - - if (wNumStrings == 0 || lpStrings == nullptr) { - logging::W("ReportEventW called with no strings."); - return hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData); - } - - // In most cases, DalamudCrashHandler will kill us now, so call original here to make sure we still write to the event log. - const BOOL original_ret = hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData); - - const std::wstring error_details(lpStrings[0]); - veh::raise_external_event(error_details); - - return original_ret; - }); - logging::I("ReportEventW hook installed."); - // ============================== Dalamud ==================================== // if (static_cast(g_startInfo.BootWaitMessageBox) & static_cast(DalamudStartInfo::WaitMessageboxFlags::BeforeDalamudEntrypoint)) diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index 50ac9b34c..b0ec1cefa 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -31,8 +31,6 @@ HANDLE g_crashhandler_process = nullptr; HANDLE g_crashhandler_event = nullptr; HANDLE g_crashhandler_pipe_write = nullptr; -wchar_t g_external_event_info[16384] = L""; - std::recursive_mutex g_exception_handler_mutex; std::chrono::time_point g_time_start; @@ -192,11 +190,7 @@ 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 (ex->ExceptionRecord->ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT) - { - stackTrace = std::wstring(g_external_event_info); - } - else if (!g_clr) + if (!g_clr) { stackTrace = L"(no CLR stack trace available)"; } @@ -257,12 +251,6 @@ LONG WINAPI structured_exception_handler(EXCEPTION_POINTERS* ex) LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex) { - // special case for CLR exceptions, always trigger crash handler - if (ex->ExceptionRecord->ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT) - { - return exception_handler(ex); - } - if (ex->ExceptionRecord->ExceptionCode == 0x12345678) { // pass @@ -280,7 +268,7 @@ LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex) if (!is_ffxiv_address(L"ffxiv_dx11.exe", ex->ContextRecord->Rip) && !is_ffxiv_address(L"cimgui.dll", ex->ContextRecord->Rip)) - return EXCEPTION_CONTINUE_SEARCH; + return EXCEPTION_CONTINUE_SEARCH; } return exception_handler(ex); @@ -309,7 +297,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) if (HANDLE hReadPipeRaw, hWritePipeRaw; CreatePipe(&hReadPipeRaw, &hWritePipeRaw, nullptr, 65536)) { hWritePipe.emplace(hWritePipeRaw, &CloseHandle); - + if (HANDLE hReadPipeInheritableRaw; DuplicateHandle(GetCurrentProcess(), hReadPipeRaw, GetCurrentProcess(), &hReadPipeInheritableRaw, 0, TRUE, DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE)) { hReadPipeInheritable.emplace(hReadPipeInheritableRaw, &CloseHandle); @@ -327,9 +315,9 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) } // additional information - STARTUPINFOEXW siex{}; + STARTUPINFOEXW siex{}; PROCESS_INFORMATION pi{}; - + siex.StartupInfo.cb = sizeof siex; siex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW; siex.StartupInfo.wShowWindow = g_startInfo.CrashHandlerShow ? SW_SHOW : SW_HIDE; @@ -397,7 +385,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) argstr.push_back(L' '); } argstr.pop_back(); - + if (!handles.empty() && !UpdateProcThreadAttribute(siex.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &handles[0], std::span(handles).size_bytes(), nullptr, nullptr)) { logging::W("Failed to launch DalamudCrashHandler.exe: UpdateProcThreadAttribute error 0x{:x}", GetLastError()); @@ -412,7 +400,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) TRUE, // Set handle inheritance to FALSE EXTENDED_STARTUPINFO_PRESENT, // lpStartupInfo actually points to a STARTUPINFOEX(W) nullptr, // Use parent's environment block - nullptr, // Use parent's starting directory + nullptr, // Use parent's starting directory &siex.StartupInfo, // Pointer to STARTUPINFO structure &pi // Pointer to PROCESS_INFORMATION structure (removed extra parentheses) )) @@ -428,7 +416,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) } CloseHandle(pi.hThread); - + g_crashhandler_process = pi.hProcess; g_crashhandler_pipe_write = hWritePipe->release(); logging::I("Launched DalamudCrashHandler.exe: PID {}", pi.dwProcessId); @@ -446,10 +434,3 @@ bool veh::remove_handler() } return false; } - -void veh::raise_external_event(const std::wstring& info) -{ - const auto info_size = std::min(info.size(), std::size(g_external_event_info) - 1); - wcsncpy_s(g_external_event_info, info.c_str(), info_size); - RaiseException(CUSTOM_EXCEPTION_EXTERNAL_EVENT, 0, 0, nullptr); -} diff --git a/Dalamud.Boot/veh.h b/Dalamud.Boot/veh.h index 2a02c374e..1905272ea 100644 --- a/Dalamud.Boot/veh.h +++ b/Dalamud.Boot/veh.h @@ -4,5 +4,4 @@ namespace veh { bool add_handler(bool doFullDump, const std::string& workingDirectory); bool remove_handler(); - void raise_external_event(const std::wstring& info); } diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs index 980404940..945197e2b 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -9,6 +9,7 @@ using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Events; @@ -31,21 +32,25 @@ internal unsafe class AddonEventManager : IInternalDisposableService private readonly AddonLifecycleEventListener finalizeEventListener; - private readonly Hook onUpdateCursor; + private readonly AddonEventManagerAddressResolver address; + private readonly Hook onUpdateCursor; private readonly ConcurrentDictionary pluginEventControllers; - private AtkCursor.CursorType? cursorOverride; + private AddonCursorType? cursorOverride; [ServiceManager.ServiceConstructor] - private AddonEventManager() + private AddonEventManager(TargetSigScanner sigScanner) { + this.address = new AddonEventManagerAddressResolver(); + this.address.Setup(sigScanner); + this.pluginEventControllers = new ConcurrentDictionary(); this.pluginEventControllers.TryAdd(DalamudInternalKey, new PluginEventController()); this.cursorOverride = null; - this.onUpdateCursor = Hook.FromAddress(AtkUnitManager.Addresses.UpdateCursor.Value, this.UpdateCursorDetour); + this.onUpdateCursor = Hook.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour); this.finalizeEventListener = new AddonLifecycleEventListener(AddonEvent.PreFinalize, string.Empty, this.OnAddonFinalize); this.addonLifecycle.RegisterListener(this.finalizeEventListener); @@ -53,6 +58,8 @@ internal unsafe class AddonEventManager : IInternalDisposableService this.onUpdateCursor.Enable(); } + private delegate nint UpdateCursorDelegate(RaptureAtkModule* module); + /// void IInternalDisposableService.DisposeService() { @@ -110,7 +117,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService /// Force the game cursor to be the specified cursor. /// /// Which cursor to use. - internal void SetCursor(AddonCursorType cursor) => this.cursorOverride = (AtkCursor.CursorType)cursor; + internal void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor; /// /// Un-forces the game cursor. @@ -161,7 +168,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService } } - private void UpdateCursorDetour(AtkUnitManager* thisPtr) + private nint UpdateCursorDetour(RaptureAtkModule* module) { try { @@ -169,14 +176,13 @@ internal unsafe class AddonEventManager : IInternalDisposableService if (this.cursorOverride is not null && atkStage is not null) { - ref var atkCursor = ref atkStage->AtkCursor; - - if (atkCursor.Type != this.cursorOverride) + var cursor = (AddonCursorType)atkStage->AtkCursor.Type; + if (cursor != this.cursorOverride) { - atkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1); + AtkStage.Instance()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1); } - return; + return nint.Zero; } } catch (Exception e) @@ -184,7 +190,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService Log.Error(e, "Exception in UpdateCursorDetour."); } - this.onUpdateCursor!.Original(thisPtr); + return this.onUpdateCursor!.Original(module); } } diff --git a/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs b/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs new file mode 100644 index 000000000..415e1b169 --- /dev/null +++ b/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs @@ -0,0 +1,21 @@ +namespace Dalamud.Game.Addon.Events; + +/// +/// AddonEventManager memory address resolver. +/// +internal class AddonEventManagerAddressResolver : BaseAddressResolver +{ + /// + /// Gets the address of the AtkModule UpdateCursor method. + /// + public nint UpdateCursor { get; private set; } + + /// + /// Scan for and setup any configured address pointers. + /// + /// The signature scanner to facilitate setup. + protected override void Setup64Bit(ISigScanner scanner) + { + this.UpdateCursor = scanner.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 20 4C 8B F1 E8 ?? ?? ?? ?? 49 8B CE"); // unnamed in CS + } +} diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 1feec4b2f..ec7115ffd 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -119,7 +119,7 @@ std::wstring describe_module(const std::filesystem::path& path) { return std::format(L"", GetLastError()); UINT size = 0; - + std::wstring version = L"v?.?.?.?"; if (LPVOID lpBuffer; VerQueryValueW(block.data(), L"\\", &lpBuffer, &size)) { const auto& v = *static_cast(lpBuffer); @@ -176,7 +176,7 @@ const std::map& get_remote_modules() { std::vector buf(8192); for (size_t i = 0; i < 64; i++) { if (DWORD needed; !EnumProcessModules(g_hProcess, &buf[0], static_cast(std::span(buf).size_bytes()), &needed)) { - std::cerr << std::format("EnumProcessModules error: 0x{:x}", GetLastError()) << std::endl; + std::cerr << std::format("EnumProcessModules error: 0x{:x}", GetLastError()) << std::endl; break; } else if (needed > std::span(buf).size_bytes()) { buf.resize(needed / sizeof(HMODULE) + 16); @@ -201,7 +201,7 @@ const std::map& get_remote_modules() { data[hModule] = nth64.OptionalHeader.SizeOfImage; } - + return data; }(); @@ -292,43 +292,35 @@ std::wstring to_address_string(const DWORD64 address, const bool try_ptrderef = void print_exception_info(HANDLE hThread, const EXCEPTION_POINTERS& ex, const CONTEXT& ctx, std::wostringstream& log) { std::vector exRecs; - if (ex.ExceptionRecord) - { + if (ex.ExceptionRecord) { size_t rec_index = 0; size_t read; - + exRecs.emplace_back(); for (auto pRemoteExRec = ex.ExceptionRecord; - pRemoteExRec && rec_index < 64; - rec_index++) - { - exRecs.emplace_back(); - - if (!ReadProcessMemory(g_hProcess, pRemoteExRec, &exRecs.back(), sizeof exRecs.back(), &read) - || read < offsetof(EXCEPTION_RECORD, ExceptionInformation) - || read < static_cast(reinterpret_cast(&exRecs.back().ExceptionInformation[exRecs. - back().NumberParameters]) - reinterpret_cast(&exRecs.back()))) - { - exRecs.pop_back(); - break; - } + pRemoteExRec + && rec_index < 64 + && ReadProcessMemory(g_hProcess, pRemoteExRec, &exRecs.back(), sizeof exRecs.back(), &read) + && read >= offsetof(EXCEPTION_RECORD, ExceptionInformation) + && read >= static_cast(reinterpret_cast(&exRecs.back().ExceptionInformation[exRecs.back().NumberParameters]) - reinterpret_cast(&exRecs.back())); + rec_index++) { log << std::format(L"\nException Info #{}\n", rec_index); log << std::format(L"Address: {:X}\n", exRecs.back().ExceptionCode); log << std::format(L"Flags: {:X}\n", exRecs.back().ExceptionFlags); log << std::format(L"Address: {:X}\n", reinterpret_cast(exRecs.back().ExceptionAddress)); - if (exRecs.back().NumberParameters) - { - log << L"Parameters: "; - for (DWORD i = 0; i < exRecs.back().NumberParameters; ++i) - { - if (i != 0) - log << L", "; - log << std::format(L"{:X}", exRecs.back().ExceptionInformation[i]); - } + if (!exRecs.back().NumberParameters) + continue; + log << L"Parameters: "; + for (DWORD i = 0; i < exRecs.back().NumberParameters; ++i) { + if (i != 0) + log << L", "; + log << std::format(L"{:X}", exRecs.back().ExceptionInformation[i]); } pRemoteExRec = exRecs.back().ExceptionRecord; + exRecs.emplace_back(); } + exRecs.pop_back(); } log << L"\nCall Stack\n{"; @@ -418,7 +410,7 @@ void print_exception_info_extended(const EXCEPTION_POINTERS& ex, const CONTEXT& std::wstring escape_shell_arg(const std::wstring& arg) { // https://docs.microsoft.com/en-us/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way - + std::wstring res; if (!arg.empty() && arg.find_first_of(L" \t\n\v\"") == std::wstring::npos) { res.append(arg); @@ -512,7 +504,7 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s filePath.emplace(pFilePath); 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; zipa.m_pRead = [](void* pOpaque, mz_uint64 file_ofs, void* pBuf, size_t n) -> size_t { @@ -574,7 +566,7 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s 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) throw_last_error(std::format("indiv. log file: CreateFileW({})", ws_to_u8(logFilePath.wstring()))); - + std::unique_ptr hLogFileClose(hLogFile, &CloseHandle); LARGE_INTEGER size, baseOffset{}; @@ -703,7 +695,7 @@ int main() { // IFileSaveDialog only works on STA CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); - + std::vector args; if (int argc = 0; const auto argv = CommandLineToArgvW(GetCommandLineW(), &argc)) { for (auto i = 0; i < argc; i++) @@ -831,14 +823,14 @@ int main() { hr = pOleWindow->GetWindow(&hwndProgressDialog); if (SUCCEEDED(hr)) { - SetWindowPos(hwndProgressDialog, HWND_TOPMOST, 0, 0, 0, 0, + SetWindowPos(hwndProgressDialog, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); SetForegroundWindow(hwndProgressDialog); } - + pOleWindow->Release(); } - + } else { std::cerr << "Failed to create progress window" << std::endl; @@ -860,14 +852,14 @@ int main() { https://github.com/sumatrapdfreader/sumatrapdf/blob/master/src/utils/DbgHelpDyn.cpp */ - + if (g_bSymbolsAvailable) { SymRefreshModuleList(g_hProcess); } else if(!assetDir.empty()) { auto symbol_search_path = std::format(L".;{}", (assetDir / "UIRes" / "pdb").wstring()); - + g_bSymbolsAvailable = SymInitializeW(g_hProcess, symbol_search_path.c_str(), true); std::wcout << std::format(L"Init symbols with PDB at {}", symbol_search_path) << std::endl; @@ -878,12 +870,12 @@ int main() { g_bSymbolsAvailable = SymInitializeW(g_hProcess, nullptr, true); std::cout << "Init symbols without PDB" << std::endl; } - + if (!g_bSymbolsAvailable) { std::wcerr << std::format(L"SymInitialize error: 0x{:x}", GetLastError()) << std::endl; } - if (pProgressDialog) + if (pProgressDialog) pProgressDialog->SetLine(3, L"Reading troubleshooting data", FALSE, NULL); std::wstring stackTrace(exinfo.dwStackTraceLength, L'\0'); @@ -938,23 +930,13 @@ int main() { } while (false); } - const bool is_external_event = exinfo.ExceptionRecord.ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT; - std::wostringstream log; - - if (!is_external_event) - { - log << std::format(L"Unhandled native exception occurred at {}", to_address_string(exinfo.ContextRecord.Rip, false)) << std::endl; - log << std::format(L"Code: {:X}", exinfo.ExceptionRecord.ExceptionCode) << std::endl; - } - else - { - log << L"CLR error occurred" << std::endl; - } + log << std::format(L"Unhandled native exception occurred at {}", to_address_string(exinfo.ContextRecord.Rip, false)) << std::endl; + log << std::format(L"Code: {:X}", exinfo.ExceptionRecord.ExceptionCode) << std::endl; if (shutup) log << L"======= Crash handler was globally muted(shutdown?) =======" << std::endl; - + if (dumpPath.empty()) log << L"Dump skipped" << std::endl; else if (dumpError.empty()) @@ -967,19 +949,9 @@ int main() { if (pProgressDialog) pProgressDialog->SetLine(3, L"Refreshing Module List", FALSE, NULL); - std::wstring window_log_str; - - // Cut the log here for external events, the rest is unreadable and doesn't matter since we can't get - // symbols for mixed-mode stacks yet. - if (is_external_event) - window_log_str = log.str(); - SymRefreshModuleList(GetCurrentProcess()); print_exception_info(exinfo.hThreadHandle, exinfo.ExceptionPointers, exinfo.ContextRecord, log); - - if (!is_external_event) - window_log_str = log.str(); - + const auto window_log_str = log.str(); print_exception_info_extended(exinfo.ExceptionPointers, exinfo.ContextRecord, log); std::wofstream(logPath) << log.str(); @@ -1031,7 +1003,7 @@ int main() { R"aa(Help | Open log directory | Open log file)aa" ); #endif - + // Can't do this, xiv stops pumping messages here //config.hwndParent = FindWindowA("FFXIVGAME", NULL); @@ -1084,13 +1056,13 @@ int main() { return (*reinterpret_cast(dwRefData))(hwnd, uNotification, wParam, lParam); }; config.lpCallbackData = reinterpret_cast(&callback); - + if (pProgressDialog) { pProgressDialog->StopProgressDialog(); pProgressDialog->Release(); pProgressDialog = NULL; } - + const auto kill_game = [&] { TerminateProcess(g_hProcess, exinfo.ExceptionRecord.ExceptionCode); }; if (shutup) { diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 6f339d8f7..e5dedba42 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 6f339d8f725fa6922449f7e5c584ca6b8fa2fb19 +Subproject commit e5dedba42a3fea8f050ea54ac583a5874bf51c6f